diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2f7bda6..10509c4 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,7 +16,6 @@ jobs: with: scandir: . severity: warning - ignore_paths: macOS json-validate: runs-on: ubuntu-latest @@ -36,6 +35,14 @@ jobs: done exit $failed + dryrun-smoke: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Root setup.sh dry-run on every platform + run: bash scripts/dryrun-smoke.sh + zsh-syntax: runs-on: ubuntu-latest steps: @@ -47,7 +54,7 @@ jobs: - name: Run zsh -n on zsh scripts run: | failed=0 - for f in $(find . -not -path './.git/*' \( \( -name '*.sh' -path '*/macOS/*' \) -o -name '*.zsh' -o -name 'zshrc.example' \)); do + for f in $(find . -not -path './.git/*' \( -name '*.zsh' -o -name 'zshrc.example' \)); do if zsh -n "$f" 2>&1; then echo "✓ $f" else diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 23abdf9..e869012 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,6 @@ repos: hooks: - id: shellcheck args: [--severity=warning] - exclude: ^macOS/ - repo: local hooks: @@ -13,7 +12,7 @@ repos: language: system entry: zsh -n types: [file] - files: (^macOS/.*\.sh$|\.zsh$|zshrc\.example$) + files: (\.zsh$|zshrc\.example$) - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..dca419b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,56 @@ +# Computer-Setup + +Personal machine-provisioning repo: one unified installer for macOS, Linux +desktop (Ubuntu/Arch), and an Ubuntu Server LTS home server, plus per-platform +configs and docs. (A Raspberry Pi — Debian proper — is a future target, +tracked in TODO.md.) + +## Entrypoints + +- `setup.sh` — installs everything for the detected platform. + Flags: `--optional --work --personal --dry-run --platform --profile `. + The server platform is never auto-detected (`--profile server` or `--platform server` required). +- `verify.sh` — read-only health check mirroring `setup.sh`'s selection logic. + Flags: `--optional --work --personal --all --platform ` + (no `--dry-run`; `--platform server`/`--profile server` is rejected — nothing + legacy existed to port). Unknown flags warn and are ignored, they don't abort. + +## Architecture + +- `packages.json` — single source of truth for all package data. Managers are + keyed by platform (`{macos, ubuntu, arch, server}`); `_name` + overrides the install token; `environment` tags gate on `--work`/`--personal`; + `custom` managers carry `install_command` (string or per-platform object) — + run by the engine when `handled_by_setup` is true, otherwise printed as a + manual-install reminder. +- `lib/core.sh` — shared engine: arg parsing, platform detection, env filter, + jq selection, install loops, config deploys. `lib/verify.sh` — check engine. +- `platforms/.sh` — per-platform quirks only (bootstrap, manager + invocations). +- `dotfiles/` — configs shared across platforms (`tmux.conf`, + `ghostty.config`, `zshrc.example`, `zsh_plugins.txt`); the engine deploys + them from here. The zshrc base is cross-platform — macOS-specific bits are + guarded on `/opt/homebrew` or `$OSTYPE` — and has an override system + (`deploy_zshrc`): a platform folder shipping its own `zshrc.example` wins + over the dotfiles base. Only `linux-server/` does (headless: no + Ghostty/fastfetch/notification hooks). +- `macOS/`, `linux-desktop/`, `linux-server/` — platform-specific configs + (server zshrc override, p10k), docs, and thin shim scripts that exec the + root entrypoints. +- `scripts/dryrun-smoke.sh` — runs `setup.sh --dry-run` for every platform and + asserts it exits clean with install actions; also run in CI. + +`UNIFICATION.md` is the design doc for this layout; `TODO.md` tracks remaining +phases. + +## Conventions + +- Pre-commit runs `shellcheck --severity=warning` on all shell scripts; + `zsh -n` checks `.zsh` files and `zshrc.example`. +- Probe semantics in `lib/verify.sh` are platform-faithful ports — macOS has no + `command -v` fallback for casks/pipx/app-store, Linux falls back everywhere. + Don't "fix" the asymmetry without checking `UNIFICATION.md` history. +- `--dry-run` must print every command without executing anything; it is the + primary cross-platform test mechanism (only one platform can run live). +- App-store packages and `priority: "none"` entries are reminders only — never + auto-installed. diff --git a/README.md b/README.md index 02603f4..6c77b53 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,29 @@ Useful programs and packages for a computer. +## Setup + +One entrypoint installs everything on macOS, Linux desktop (Ubuntu/Arch), and the +Linux server; the platform is auto-detected: + +```bash +bash setup.sh [--optional] [--work] [--personal] [--dry-run] +bash setup.sh --profile server # headless server (never auto-detected) +bash setup.sh --platform # force platform +``` + +Afterwards, check what's installed (read-only): + +```bash +bash verify.sh [--optional] [--work] [--personal] [--all] +``` + +Package data lives in [`packages.json`](packages.json); the engine is +[`lib/core.sh`](lib/core.sh) with per-platform quirks in +[`platforms/`](platforms/). The per-folder `setup.sh` scripts (and the +macOS/linux-desktop `verify.sh`) are thin shims onto these root scripts. See +[UNIFICATION.md](UNIFICATION.md) for the design. + For OS-specific software and install instructions, see: - [macOS/README.md](macOS/README.md) - [linux-desktop/README.md](linux-desktop/README.md) — CachyOS diff --git a/SSH_and_GPG/README.md b/SSH_and_GPG/README.md index 5085d76..526eeda 100644 --- a/SSH_and_GPG/README.md +++ b/SSH_and_GPG/README.md @@ -28,13 +28,13 @@ NAME="Jane Doe" EMAIL="jane@example.com" EXPIRY="1y" bash create_gpg_key.sh ### `create_ssh_key.sh` -Creates an Ed25519 SSH key for authenticating to a Git host (GitHub, GitLab, Bitbucket, Hugging Face, Forgejo, or custom). +Creates an Ed25519 SSH key for authenticating to a Git host (GitHub, GitLab, Bitbucket, Hugging Face, self-hosted, or custom). **What it does:** - Prompts for an email (used as the key comment), Git host, key filename, and optional passphrase - Generates the key in `~/.ssh/` (skips generation if the key already exists, with an overwrite prompt) - Adds the key to `ssh-agent` and updates `~/.ssh/config` with a `Host` block (idempotent) -- For Forgejo: writes a `forgejo` alias with `HostName`, `Port 2222`, and `User git` so you can clone as `git clone forgejo:/.git` +- For self-hosted servers: writes an alias with `HostName`, `Port 2222`, and `User git` so you can clone as `git clone :/.git` - Prints the public key for pasting into the Git host's settings - Tests the SSH connection after you confirm the key has been added @@ -43,8 +43,8 @@ Creates an Ed25519 SSH key for authenticating to a Git host (GitHub, GitLab, Bit bash create_ssh_key.sh # or pre-fill inputs via env vars: EMAIL="jane@example.com" GIT_HOST="github.com" KEY_NAME="github" bash create_ssh_key.sh -# Forgejo (self-hosted): -EMAIL="jane@example.com" IS_FORGEJO=true FORGEJO_HOSTNAME="hostname.ts.net" bash create_ssh_key.sh +# Self-hosted Git server: +EMAIL="jane@example.com" IS_SELF_HOSTED=true GIT_HOSTNAME="hostname.ts.net" GIT_HOST="gitserver" bash create_ssh_key.sh ``` --- diff --git a/SSH_and_GPG/create_ssh_key.sh b/SSH_and_GPG/create_ssh_key.sh index 44717e1..435cb82 100755 --- a/SSH_and_GPG/create_ssh_key.sh +++ b/SSH_and_GPG/create_ssh_key.sh @@ -28,8 +28,8 @@ prompt() { prompt EMAIL "Email (comment in key)" # ---- Git host selection ---- -IS_FORGEJO="${IS_FORGEJO:-}" -FORGEJO_HOSTNAME="${FORGEJO_HOSTNAME:-}" +IS_SELF_HOSTED="${IS_SELF_HOSTED:-}" +GIT_HOSTNAME="${GIT_HOSTNAME:-}" if [[ -z "$GIT_HOST" ]]; then echo "" @@ -38,7 +38,7 @@ if [[ -z "$GIT_HOST" ]]; then echo " 2) gitlab.com" echo " 3) bitbucket.org" echo " 4) hf.co" - echo " 5) Forgejo (self-hosted)" + echo " 5) Self-Hosted Git Server" echo " 0) Custom" read -r -p "Choice [1]: " host_choice case "${host_choice:-1}" in @@ -47,15 +47,18 @@ if [[ -z "$GIT_HOST" ]]; then 3) GIT_HOST="bitbucket.org" ;; 4) GIT_HOST="hf.co" ;; 5) - IS_FORGEJO=true - GIT_HOST="forgejo" - if [[ -z "$FORGEJO_HOSTNAME" ]]; then - read -r -p "Forgejo server hostname (e.g. hostname.ts.net): " FORGEJO_HOSTNAME - if [[ -z "$FORGEJO_HOSTNAME" ]]; then - echo "Error: FORGEJO_HOSTNAME cannot be empty." >&2 + IS_SELF_HOSTED=true + if [[ -z "$GIT_HOSTNAME" ]]; then + read -r -p "Server hostname (e.g. hostname.ts.net): " GIT_HOSTNAME + if [[ -z "$GIT_HOSTNAME" ]]; then + echo "Error: GIT_HOSTNAME cannot be empty." >&2 exit 1 fi fi + if [[ -z "$GIT_HOST" ]]; then + read -r -p "SSH alias for this server [gitserver]: " GIT_HOST + GIT_HOST="${GIT_HOST:-gitserver}" + fi ;; 0) read -r -p "Git host: " GIT_HOST @@ -148,11 +151,11 @@ awk -v host="$GIT_HOST" ' ' "$CFG_PATH" > "$tmp_cfg" mv "$tmp_cfg" "$CFG_PATH" -if [[ -n "$IS_FORGEJO" ]]; then +if [[ -n "$IS_SELF_HOSTED" ]]; then { echo "" echo "Host $GIT_HOST" - echo " HostName $FORGEJO_HOSTNAME" + echo " HostName $GIT_HOSTNAME" echo " Port 2222" echo " User git" echo " AddKeysToAgent yes" @@ -182,8 +185,8 @@ read -r -p "Press [Enter] after adding the public key to your $GIT_HOST account. # ---- Test SSH connection ---- # -T avoids trying to open a shell; -v optional for debugging. echo "" -if [[ -n "$IS_FORGEJO" ]]; then - echo "Testing SSH connection to Forgejo ($FORGEJO_HOSTNAME:2222) ..." +if [[ -n "$IS_SELF_HOSTED" ]]; then + echo "Testing SSH connection to self-hosted server ($GIT_HOSTNAME:2222) ..." ssh -T "$GIT_HOST" || true else echo "Testing SSH connection to git@$GIT_HOST ..." diff --git a/TODO.md b/TODO.md index b2b0eb6..210d0cb 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,6 @@ # TODO -## Setup-script unification (separate PR) +## Setup-script unification (separate PR) — [#36](https://github.com/ulises-c/Computer-Setup/issues/36) Collapse the three diverged setup stacks (`macOS/`, `linux-desktop/`, `linux-server/`) into one root `setup.sh` + one `packages.json`, with platform quirks in `platforms/` @@ -9,27 +9,103 @@ and a shared `lib/core.sh`. Full spec, schema, and phased breakdown in keyed by `{macos,ubuntu,arch,server}`), root dispatcher + platform modules, incremental migration gated on dry-run parity. -- [ ] Phase 1 — Author unified root `packages.json`; build `scripts/parity-check.sh` +- [x] Phase 1 — Author unified root `packages.json`; build `scripts/parity-check.sh` proving per-platform install lists match the current per-folder scripts -- [ ] Phase 2 — Extract `lib/core.sh` + `platforms/{macos,arch,ubuntu,server}.sh`; add + — 231 checks passed (platform × manager × priority × work/personal combos; + gate deleted in Phase 5 along with its legacy-JSON inputs) +- [x] Phase 2 — Extract `lib/core.sh` + `platforms/{macos,arch,ubuntu,server}.sh`; add root `setup.sh` dispatcher; gate on `--dry-run` parity vs old scripts -- [ ] Phase 3 — Unify `verify.sh` the same way (shared core + platform checks) -- [ ] Phase 4 — Convert per-folder `setup.sh`/`verify.sh` into thin shims; update + — `scripts/dryrun-parity.sh` passed 22/22 platform × flag combos (gate deleted in Phase 4) +- [x] Phase 3 — Unify `verify.sh` the same way (shared core + platform checks) + — `scripts/verify-parity.sh` passed 21/21 platform × flag combos (gate deleted in Phase 4) +- [x] Phase 4 — Convert per-folder `setup.sh`/`verify.sh` into thin shims; update `README.md` / `CLAUDE.md` for the root entrypoint -- [ ] Phase 5 — Delete the three old per-folder package JSONs once parity is proven -- [ ] Resolve open questions: server-as-platform vs profile; `install_command` + — shim output diffed byte-identical vs direct root invocation; the script-level + gates (`dryrun-parity.sh`, `verify-parity.sh`) self-compare post-shim and were + deleted (last green run at a8d4149); `scripts/dryrun-smoke.sh` (root dry-run on + all four platforms, also in CI) took their place; `parity-check.sh` stayed + through Phase 5's final green run +- [x] Phase 5 — Delete the three old per-folder package JSONs once parity is proven + — legacy JSONs + `scripts/parity-check.sh` (whose inputs they were) deleted; + server claude-code install folded into `packages.json` (`server: custom`, + `handled_by_setup: true`); custom-package reminders generalized into + `lib/core.sh`, so ubuntu now prints manual-install commands for `git-xet` / + `claude-desktop` instead of silently skipping them (the legacy gap); + dry-run output diffed vs pre-Phase-5 baseline — identical except that addition +- [x] Resolve open questions: server-as-platform vs profile; `install_command` string vs object; keep `priority: "none"` tier? (see UNIFICATION.md) - -## Ghostty config standardization - -The ghostty config lives in two places (`macOS/ghostty.config`, `linux-desktop/ghostty.config`) -with identical content. Consider extracting a universal `cross-platform/ghostty.config` for shared -settings (`term = xterm-256color`, `theme`, `shell-integration`) and keeping OS-specific -configs additive-only (e.g., macOS font settings, Linux-specific tweaks). - -- [ ] Create `cross-platform/ghostty.config` (universal base) -- [ ] Refactor OS configs to extend/override the universal base -- [ ] Update both `setup.sh` scripts to deploy the new layout (universal + OS overlay) + — resolved: server is a platform key (option A, locked in Phase 1); + `install_command` supports both string and per-platform object (`icfor`); + `priority: "none"` tier kept — reminder/`--all`-only, never auto-installs +- [x] Phase 6 — Merge main (PRs #28 p10k/LACT, #35 railguard, #38 claude-hud) into + the branch; port the features that landed on the legacy stack into the unified + one: `zsh-theme-powerlevel10k` + `lact` entries into root `packages.json`, + `~/.p10k.zsh` deploy (`deploy_config`) into the shared desktop flow. + Legacy `linux-desktop/setup.sh` stayed a shim; the legacy JSON stayed deleted. + Gate: per-platform dry-run diff vs pre-merge baseline — only the intended + additions (two arch packages, p10k deploy lines) +- [x] Phase 7 — Dotfiles consolidation (details in the section below): shared + `tmux.conf`/`ghostty.config` moved to `dotfiles/`, all engine deploys + repointed, macOS gains the previously missing tmux deploy. + Gate: per-platform dry-run diff vs pre-change baseline — identical except + macOS's new tmux step + +## Dotfiles consolidation — DONE as unification Phase 7 (PR #37) + +Shared configs were duplicated per-folder and could drift silently across devices. +Folded into #37 once the root engine owned all deploys. + +- [x] Create `dotfiles/` and move the byte-identical files: `tmux.conf`, `ghostty.config` +- [x] Point the root engine's deploy steps (`lib/core.sh`, `platforms/macos.sh`, + `platforms/server.sh`) at the new paths; drop the per-folder copies +- [x] Deploy tmux.conf on macOS too (was the missing platform — Macs showed the + default green bottom bar while Linux boxes got the blue top bar) +- [x] Ghostty: single universal config in `dotfiles/` (the two copies were already + byte-identical); no overlay mechanism until an OS-specific setting actually + exists +- [x] zshrc: `dotfiles/zshrc.example` is the shared base (`deploy_zshrc`); a platform + folder shipping its own `zshrc.example` overrides it — only `linux-server/` + does, because it's headless (no Ghostty/fastfetch/notification hooks) +- [x] macOS zsh unification: the dotfiles zshrc is cross-platform (macOS bits guarded + on `/opt/homebrew`/`$OSTYPE` — Homebrew PATH+FPATH, brew p10k/antidote/fzf + paths, `ls -G`, `brewup`, `PIPX_DEFAULT_PYTHON`, nvm `--delete-prefix`); macOS + deploys it + `dotfiles/zsh_plugins.txt` via the engine instead of appending + lines to `~/.zshrc` (`add_to_zshrc` deleted). antidote/zoxide/fzf/bat/fd/ + terminal-notifier gained `macos: brew` entries; the brew + zsh-autosuggestions/zsh-syntax-highlighting entries were dropped (antidote + manages the plugins now — `brew uninstall` them on the Mac after migrating). + `macOS/zshrc.example` + `zshrc-upgrade.md` deleted (plan absorbed) + (e.g., macOS font settings, Linux-specific tweaks) +- [ ] Mac mini live-run cleanup (from the 2026-06 `brew leaves` audit): `brew + uninstall` the testing leftovers `forgejo`, `tea`, and `python@3.12` + (project Pythons come from pyenv/uv), plus `zsh-autosuggestions` / + `zsh-syntax-highlighting` / `powerlevel10k` (antidote manages them now), + and `brew uninstall --cask claude-code` (repo installs it via curl) +- [ ] MBP live-run cleanup (same audit): `brew uninstall tea python-tk@3.11 + python@3.11 zsh-autosuggestions zsh-syntax-highlighting powerlevel10k`; + pre-existing casks (anki, ghostty, obsidian) get picked up by the cask + `--adopt` flag +- [x] Ubuntu desktop live run (2026-06, at `eb0fe49`): gh migrated to the + official cli.github.com apt repo, micro/obsidian snaps in, uv via pipx, + shared zshrc + p10k + ghostty deployed (zshrc auto-backup worked). + `verify.sh --work` green except the by-design manual installs + (forgejo-cli, opencode, zen-browser, anki) +- [x] Ubuntu live-run follow-up: p10k never loaded — the only installer was the + arch-only `zsh-theme-powerlevel10k` yay entry, so Ubuntu/macOS fell back to + vcs_info. Dropped the entry; `romkatv/powerlevel10k` is now an antidote + plugin on all desktops and the zshrc guards the fallback on + `$+functions[p10k]` +- [ ] Dropped when PR #38 auto-closed #34: track the claude-hud display config + (`~/.claude/plugins/claude-hud/config.json`) under `agentic-ai/Claude/` and + symlink it from `install.sh` (#34 task 2). Task 3 — the statusLine + `/usr/bin/node` hardcode — is fixed on this branch (runtime `command -v + node` with an nvm-glob fallback) +- [ ] Ubuntu desktop leftover: `sudo apt remove micro` — the stale apt 2.0.13 + still shadows the snap (`/usr/bin` precedes `/snap/bin` in PATH) +- [ ] Caveat for the remaining live runs (CachyOS, both Macs): setup migrates + install methods but never uninstalls the old copy — after each run, + `command -v` every migrated tool to catch shadowed binaries +- [ ] Later: consider base + per-platform overlay for zshrc (desktop vs server vs macOS) ## OpenCode local models @@ -45,24 +121,6 @@ Still to explore: - [ ] Consider `small_model` for lightweight tasks (title gen, etc.) - [ ] Install `opencode-local` via install.sh and verify PATH -## macOS zshrc modernization - -Port the linux-desktop zsh enhancements to the macOS config. See -[macOS/zshrc-upgrade.md](macOS/zshrc-upgrade.md) for the full plan. - -- [ ] Switch from manual Homebrew plugin sourcing to antidote -- [ ] Add eza aliases with `--icons --group-directories-first` -- [ ] Add RPROMPT with command duration -- [ ] Add colorized man pages via bat -- [ ] Add zsh-notify for desktop notifications on long commands -- [ ] Add fish-style abbreviations via zsh-abbr -- [ ] Add zsh-history-substring-search with Up/Down keybindings -- [ ] Add grouped completion descriptions -- [ ] Add path underlining in syntax highlighting -- [ ] Add zoxide init -- [ ] Add FZF keybindings and completion (Homebrew paths) -- [ ] Create macOS zsh_plugins.txt - ## linux-desktop (personal) — CachyOS / Arch Test and validate the linux-desktop setup on the personal CachyOS desktop diff --git a/agentic-ai/Claude/hooks/post-test-runner.sh b/agentic-ai/Claude/hooks/post-test-runner.sh index 4fdde0a..b15fdb1 100755 --- a/agentic-ai/Claude/hooks/post-test-runner.sh +++ b/agentic-ai/Claude/hooks/post-test-runner.sh @@ -17,6 +17,7 @@ ROOT=$(git -C "$(dirname "$FILE")" rev-parse --show-toplevel 2>/dev/null) || exi # Discover test command: .claude/test-cmd override → auto-detect → skip TEST_CMD="" +PROBE_CMD="" if [[ -f "$ROOT/.claude/test-cmd" ]]; then TEST_CMD=$(< "$ROOT/.claude/test-cmd") elif [[ -f "$ROOT/Cargo.toml" ]]; then @@ -24,7 +25,14 @@ elif [[ -f "$ROOT/Cargo.toml" ]]; then elif [[ -f "$ROOT/go.mod" ]]; then TEST_CMD="go test ./..." elif [[ -f "$ROOT/pyproject.toml" || -f "$ROOT/pytest.ini" || -f "$ROOT/setup.py" ]]; then - TEST_CMD="pytest" + # python -m: console-script bins are absent in uv --no-sync venvs (#30); + # --no-sync so the hook never re-resolves an env with pinned torch builds + if [[ -f "$ROOT/uv.lock" ]]; then + TEST_CMD="uv run --no-sync python -m pytest" + else + TEST_CMD="python3 -m pytest" + fi + PROBE_CMD="${TEST_CMD% -m pytest} -c 'import pytest'" elif [[ -f "$ROOT/package.json" ]]; then _npm_test=$(jq -r '.scripts.test // ""' "$ROOT/package.json" 2>/dev/null) || true [[ -n "$_npm_test" && "$_npm_test" != *'no test specified'* ]] && TEST_CMD="npm test" @@ -36,14 +44,31 @@ fi # Runner not installed (e.g. pytest on a fresh box) = no runnable suite, not a failure command -v "${TEST_CMD%% *}" >/dev/null 2>&1 || exit 0 +if [[ -n "$PROBE_CMD" ]]; then + (cd "$ROOT" && bash -c "$PROBE_CMD") >/dev/null 2>&1 || exit 0 +fi TMPFILE=$(mktemp) trap 'rm -f "$TMPFILE"' EXIT -START_MS=$(date +%s%3N) -(cd "$ROOT" && timeout 60 bash -c "$TEST_CMD") > "$TMPFILE" 2>&1 +# BSD date has no %N (#33); python3 fallback covers macOS +now_ms() { + local ms + ms=$(date +%s%3N 2>/dev/null) + [[ "$ms" == *N* || -z "$ms" ]] && ms=$(python3 -c 'import time; print(int(time.time() * 1000))') + printf '%s\n' "$ms" +} +# macOS ships timeout only as coreutils' gtimeout (#33) +TIMEOUT_BIN=$(command -v timeout || command -v gtimeout || true) + +START_MS=$(now_ms) +if [[ -n "$TIMEOUT_BIN" ]]; then + (cd "$ROOT" && "$TIMEOUT_BIN" 60 bash -c "$TEST_CMD") > "$TMPFILE" 2>&1 +else + (cd "$ROOT" && bash -c "$TEST_CMD") > "$TMPFILE" 2>&1 +fi EXIT_CODE=$? -END_MS=$(date +%s%3N) +END_MS=$(now_ms) ELAPSED_MS=$(( END_MS - START_MS )) ELAPSED_FMT=$(printf '%d.%03ds' $(( ELAPSED_MS / 1000 )) $(( ELAPSED_MS % 1000 ))) diff --git a/agentic-ai/Claude/install.sh b/agentic-ai/Claude/install.sh index ccfbbc6..6b6c917 100755 --- a/agentic-ai/Claude/install.sh +++ b/agentic-ai/Claude/install.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash -# Idempotent setup: symlinks this repo's Claude config into ~/.claude/ +# Idempotent setup: deploys this repo's Claude config into ~/.claude/ +# (settings.json is copied, everything else symlinked). # Safe to re-run. Backs up any existing settings.json before replacing it. set -euo pipefail @@ -11,15 +12,21 @@ SETTINGS="$CLAUDE_DIR/settings.json" printf 'Installing from: %s\n' "$REPO_DIR" -# Back up settings.json if it exists and is not already one of ours -if [[ -e "$SETTINGS" && ! -L "$SETTINGS" ]]; then +# settings.json is COPIED, not symlinked: Claude Code rewrites its user +# settings at runtime (model switches, plugin installs re-serialize the file), +# and a symlink funnels that machine state into the repo as permanent dirt. +# The repo file is the template; the live copy is machine state. Re-running +# resets the live copy to the template (after a backup) — re-pick your model +# afterwards. settings-drift.sh reports when the two diverge. +if [[ -L "$SETTINGS" ]]; then + rm "$SETTINGS" # old symlink layout — live content was the repo file itself +elif [[ -e "$SETTINGS" ]] && ! cmp -s "$REPO_DIR/settings.json" "$SETTINGS"; then BACKUP="$SETTINGS.bak.$(date +%Y%m%d%H%M%S)" printf 'Backing up existing settings.json → %s\n' "$BACKUP" - mv "$SETTINGS" "$BACKUP" + cp "$SETTINGS" "$BACKUP" fi - -ln -sf "$REPO_DIR/settings.json" "$SETTINGS" -printf 'Linked: settings.json\n' +cp "$REPO_DIR/settings.json" "$SETTINGS" +printf 'Copied: settings.json\n' # Symlink CLAUDE.md ln -sf "$REPO_DIR/CLAUDE.md" "$CLAUDE_DIR/CLAUDE.md" @@ -64,14 +71,11 @@ if ! [[ -x "$RAILGUARD_BIN" ]]; then fi # Always configure railguard (idempotent; picks up policy changes on re-run) -# railguard install rewrites settings.json with machine-specific absolute paths; -# save and restore it so the repo version (portable ~ paths) wins. -SETTINGS_TMP=$(mktemp) -cp "$REPO_DIR/settings.json" "$SETTINGS_TMP" +# railguard install rewrites the live settings.json with machine-specific +# absolute paths; redeploy the template afterwards so portable ~ paths win. printf 'Configuring railguard...\n' "$RAILGUARD_BIN" install -cp "$SETTINGS_TMP" "$REPO_DIR/settings.json" -rm -f "$SETTINGS_TMP" +cp "$REPO_DIR/settings.json" "$SETTINGS" # Warn on Ubuntu 24.04+ if the bwrap AppArmor profile isn't set up if grep -qi 'ubuntu' /etc/os-release 2>/dev/null && ! [[ -f /etc/apparmor.d/bwrap ]]; then diff --git a/agentic-ai/Claude/settings-drift.sh b/agentic-ai/Claude/settings-drift.sh new file mode 100755 index 0000000..3fe029f --- /dev/null +++ b/agentic-ai/Claude/settings-drift.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Compares the repo settings.json template against the live ~/.claude copy. +# Volatile machine state is ignored: .model is set per machine and changes +# with every model release. Key order is normalized (jq -S) because Claude +# Code re-serializes the live file on plugin installs and /config changes. +set -euo pipefail + +REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TEMPLATE="$REPO_DIR/settings.json" +LIVE="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json" + +if [[ ! -f "$LIVE" ]]; then + printf 'error: no live settings at %s — run install.sh first\n' "$LIVE" >&2 + exit 1 +fi + +normalize() { jq -S 'del(.model)' "$1"; } + +if drift=$(diff -u --label template --label live <(normalize "$TEMPLATE") <(normalize "$LIVE")); then + printf 'settings in sync (ignored volatile keys: model)\n' +else + printf 'settings drift — commit it to the template or re-run install.sh:\n%s\n' "$drift" + exit 1 +fi diff --git a/agentic-ai/Claude/settings.json b/agentic-ai/Claude/settings.json index 28ad685..347ae1d 100644 --- a/agentic-ai/Claude/settings.json +++ b/agentic-ai/Claude/settings.json @@ -115,13 +115,12 @@ ] } }, - "advisorModel": "opus", "skipDangerousModePermissionPrompt": true, "enabledPlugins": { "claude-hud@claude-hud": true }, "statusLine": { "type": "command", - "command": "bash -c ' cols=$(stty size /dev/null | awk '\"'\"'{print $2}'\"'\"'); export COLUMNS=$(( ${cols:-120} > 4 ? ${cols:-120} - 4 : 1 )); plugin_dir=$(ls -d \"${CLAUDE_CONFIG_DIR:-$HOME/.claude}\"/plugins/cache/*/claude-hud/*/ 2>/dev/null | awk -F/ '\"'\"'{ print $(NF-1) \"\\t\" $0 }'\"'\"' | grep -E '\"'\"'^[0-9]+\\.[0-9]+\\.[0-9]+[[:space:]]'\"'\"' | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1 | cut -f2-); exec \"/usr/bin/node\" \"${plugin_dir}dist/index.js\"'" + "command": "bash -c ' cols=$(stty size /dev/null | awk '\"'\"'{print $2}'\"'\"'); export COLUMNS=$(( ${cols:-120} > 4 ? ${cols:-120} - 4 : 1 )); plugin_dir=$(ls -d \"${CLAUDE_CONFIG_DIR:-$HOME/.claude}\"/plugins/cache/*/claude-hud/*/ 2>/dev/null | awk -F/ '\"'\"'{ print $(NF-1) \"\\t\" $0 }'\"'\"' | grep -E '\"'\"'^[0-9]+\\.[0-9]+\\.[0-9]+[[:space:]]'\"'\"' | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1 | cut -f2-); node_bin=$(command -v node || ls \"$HOME\"/.nvm/versions/node/*/bin/node 2>/dev/null | sort -V | tail -1); exec \"$node_bin\" \"${plugin_dir}dist/index.js\"'" } } diff --git a/dotfiles/ghostty.config b/dotfiles/ghostty.config new file mode 100644 index 0000000..2a9b2b3 --- /dev/null +++ b/dotfiles/ghostty.config @@ -0,0 +1,11 @@ +# Ghostty Config + +theme = Dark Pastel +font-family = MesloLGS Nerd Font Mono +shell-integration = zsh +term = xterm-256color + +# High-res scroll wheels emit ~7-10 events per notch and Ghostty reports each +# as a full click, flooding TUIs (micro inserts the fragments as text). +# https://github.com/ghostty-org/ghostty/discussions/4259 +mouse-scroll-multiplier = 0.75 diff --git a/linux-desktop/p10k.zsh.example b/dotfiles/p10k.zsh.example similarity index 99% rename from linux-desktop/p10k.zsh.example rename to dotfiles/p10k.zsh.example index 7e7519a..f1e1d8f 100644 --- a/linux-desktop/p10k.zsh.example +++ b/dotfiles/p10k.zsh.example @@ -1719,7 +1719,7 @@ # - verbose: Enable instant prompt and print a warning when detecting console output during # zsh initialization. Choose this if you've never tried instant prompt, haven't # seen the warning, or if you are unsure what this all means. - typeset -g POWERLEVEL9K_INSTANT_PROMPT=verbose + typeset -g POWERLEVEL9K_INSTANT_PROMPT=quiet # Hot reload allows you to change POWERLEVEL9K options after Powerlevel10k has been initialized. # For example, you can type POWERLEVEL9K_BACKGROUND=red and see your prompt turn red. Hot reload diff --git a/linux-desktop/tmux.conf b/dotfiles/tmux.conf similarity index 100% rename from linux-desktop/tmux.conf rename to dotfiles/tmux.conf diff --git a/linux-desktop/zsh_plugins.txt b/dotfiles/zsh_plugins.txt similarity index 90% rename from linux-desktop/zsh_plugins.txt rename to dotfiles/zsh_plugins.txt index 145b2e2..cb06916 100644 --- a/linux-desktop/zsh_plugins.txt +++ b/dotfiles/zsh_plugins.txt @@ -21,3 +21,6 @@ olets/zsh-abbr # Desktop notifications for long-running commands (like fish done.fish) marzocchi/zsh-notify + +# Powerlevel10k prompt theme (settings in ~/.p10k.zsh; rerun `p10k configure` to change) +romkatv/powerlevel10k diff --git a/linux-desktop/zshrc.example b/dotfiles/zshrc.example similarity index 82% rename from linux-desktop/zshrc.example rename to dotfiles/zshrc.example index f99e71b..15e9a68 100644 --- a/linux-desktop/zshrc.example +++ b/dotfiles/zshrc.example @@ -1,9 +1,12 @@ # ═══════════════════════════════════════════════════════════════ -# ~/.zshrc — Linux desktop zsh config (Ubuntu / Arch) +# ~/.zshrc — shared zsh config (macOS / Ubuntu / Arch) # ═══════════════════════════════════════════════════════════════ -# Requires: zsh, zsh-antidote, zoxide, fastfetch, fzf, bat, fd, eza +# Base zshrc deployed by setup.sh; a platform folder can ship its own +# zshrc.example to override it (linux-server does — headless, no Ghostty). +# macOS-specific bits are guarded on /opt/homebrew (Apple Silicon) or $OSTYPE. +# Requires: zsh, antidote, zoxide, fastfetch, fzf, bat, fd, eza # Plugins managed by antidote — see ~/.zsh_plugins.txt -# Install: bash linux-desktop/setup.sh +# Install: bash setup.sh # ── Instant prompt (Powerlevel10k — keep at very top) ────────── [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]] && \ @@ -13,7 +16,11 @@ # ═══════════════════════════════════════════════════════════════ # PATH # ═══════════════════════════════════════════════════════════════ +# macOS: Homebrew (Apple Silicon) +[[ -d /opt/homebrew/bin ]] && export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:$PATH" export PATH="$HOME/.local/bin:$PATH" +# cargo install artifacts (rustup toolchains proxy via /usr/bin) +[[ -d "$HOME/.cargo/bin" ]] && export PATH="$HOME/.cargo/bin:$PATH" # ═══════════════════════════════════════════════════════════════ @@ -25,9 +32,18 @@ export PYENV_ROOT="$HOME/.pyenv" [[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH" command -v pyenv &>/dev/null && eval "$(pyenv init -)" +# macOS: point pipx at the pyenv python — brew's Python bottles are broken +# on Tahoe (missing libexpat symbols) +[[ "$OSTYPE" == darwin* && -x $PYENV_ROOT/shims/python3 ]] && \ + export PIPX_DEFAULT_PYTHON="$PYENV_ROOT/shims/python3" + export NVM_DIR="$HOME/.nvm" [[ -s $NVM_DIR/nvm.sh ]] && source "$NVM_DIR/nvm.sh" [[ -s $NVM_DIR/bash_completion ]] && source "$NVM_DIR/bash_completion" +# macOS: brew's node may shadow nvm's — force the default alias, dropping any +# prefix conflict +[[ "$OSTYPE" == darwin* ]] && command -v nvm &>/dev/null && \ + nvm use --delete-prefix default --silent 2>/dev/null # pnpm — enabled by setup.sh via corepack (the pnpm binary itself is a corepack # shim on PATH via nvm). Global CLI binaries land in $PNPM_HOME/bin. @@ -53,15 +69,21 @@ setopt HIST_REDUCE_BLANKS # antidote sources all plugins listed in ~/.zsh_plugins.txt: # zsh-completions, zsh-autosuggestions, zsh-syntax-highlighting, # zsh-history-substring-search, zsh-abbr -if [[ -f /usr/share/zsh-antidote/antidote.zsh ]]; then +if [[ -f /usr/share/zsh-antidote/antidote.zsh ]]; then # Linux source /usr/share/zsh-antidote/antidote.zsh antidote load +elif [[ -f /opt/homebrew/opt/antidote/share/antidote/antidote.zsh ]]; then # macOS (brew) + source /opt/homebrew/opt/antidote/share/antidote/antidote.zsh + antidote load fi # ═══════════════════════════════════════════════════════════════ # COMPLETION # ═══════════════════════════════════════════════════════════════ +# macOS: brew-provided completion definitions (must precede compinit) +[[ -d /opt/homebrew/share/zsh/site-functions ]] && \ + FPATH="/opt/homebrew/share/zsh/site-functions:$FPATH" autoload -Uz compinit && compinit zstyle ':completion:*' menu select zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}' @@ -82,10 +104,9 @@ setopt NO_BEEP # ═══════════════════════════════════════════════════════════════ # PROMPT (Powerlevel10k if available, vcs_info fallback) # ═══════════════════════════════════════════════════════════════ -# Arch: pacman -S zsh-theme-powerlevel10k | run: p10k configure -if [[ -f /usr/share/zsh-theme-powerlevel10k/powerlevel10k.zsh-theme ]]; then - source /usr/share/zsh-theme-powerlevel10k/powerlevel10k.zsh-theme -else +# Powerlevel10k loads via antidote (romkatv/powerlevel10k in ~/.zsh_plugins.txt); +# settings come from ~/.p10k.zsh sourced at the bottom of this file. +if ! (( $+functions[p10k] )); then autoload -Uz vcs_info _prompt_precmd() { @@ -130,7 +151,14 @@ fi # ═══════════════════════════════════════════════════════════════ # ALIASES # ═══════════════════════════════════════════════════════════════ -alias ls='ls --color=auto' +if [[ "$OSTYPE" == darwin* ]]; then + alias ls='ls -G' # BSD ls has no --color=auto + # App Store Tailscale bundles the CLI inside the app — not on PATH (#22) + [[ -x "/Applications/Tailscale.app/Contents/MacOS/Tailscale" ]] && \ + alias tailscale="/Applications/Tailscale.app/Contents/MacOS/Tailscale" +else + alias ls='ls --color=auto' +fi if command -v eza &>/dev/null; then alias ll='eza -lah --git --icons --group-directories-first' alias la='eza -a --icons --group-directories-first' @@ -144,6 +172,10 @@ alias grep='grep --color=auto' alias df='df -h' alias du='du -sh' alias reload='source ~/.zshrc' +command -v fastfetch &>/dev/null && alias cf='clear && fastfetch' + +# macOS: Homebrew maintenance +command -v brew &>/dev/null && alias brewup='brew update && brew upgrade && brew cleanup' # Ubuntu binary name shims (bat → batcat, fd → fdfind) [[ -x /usr/bin/batcat ]] && ! command -v bat &>/dev/null && alias bat='batcat' @@ -176,6 +208,7 @@ fi export EDITOR='micro' export LANG='en_US.UTF-8' export LC_ALL='en_US.UTF-8' +export GPG_TTY=$(tty) # ═══════════════════════════════════════════════════════════════ @@ -197,6 +230,11 @@ command -v zoxide &>/dev/null && eval "$(zoxide init zsh)" source /usr/share/fzf/key-bindings.zsh [[ -f /usr/share/fzf/completion.zsh ]] && \ source /usr/share/fzf/completion.zsh +# macOS (brew) +[[ -f /opt/homebrew/opt/fzf/shell/key-bindings.zsh ]] && \ + source /opt/homebrew/opt/fzf/shell/key-bindings.zsh +[[ -f /opt/homebrew/opt/fzf/shell/completion.zsh ]] && \ + source /opt/homebrew/opt/fzf/shell/completion.zsh # ═══════════════════════════════════════════════════════════════ @@ -219,7 +257,8 @@ bindkey '^[OB' history-substring-search-down # ═══════════════════════════════════════════════════════════════ # NOTIFY (desktop notification after long commands) # ═══════════════════════════════════════════════════════════════ -# Requires: notify-send (libnotify), xdotool or wmctrl (focus detection) +# Linux: notify-send (libnotify) + xdotool for focus detection. +# macOS: terminal-notifier; focus detection is native AppleScript. # Loaded via antidote (marzocchi/zsh-notify) zstyle ':notify:*' command-complete-timeout 30 zstyle ':notify:*' error-title "Command failed" diff --git a/lib/core.sh b/lib/core.sh new file mode 100755 index 0000000..a0ac06c --- /dev/null +++ b/lib/core.sh @@ -0,0 +1,623 @@ +#!/usr/bin/env bash +# Shared engine for the unified root setup.sh (UNIFICATION.md, issue #36). +# Sourced by setup.sh after SETUP_ROOT is set. Platform modules in +# platforms/.sh provide platform_main() plus the hooks used by +# desktop_main(): platform_bootstrap, platform_install_tier, +# platform_pyenv_build_deps, platform_tailscale_step, platform_docker_optional. + +PACKAGES_JSON="$SETUP_ROOT/packages.json" + +PYTHON_VERSION="3.12.13" + +PLATFORM="" +INCLUDE_OPTIONAL=false +INCLUDE_WORK=false +INCLUDE_PERSONAL=false +DRY_RUN=false + +core_parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + --optional) INCLUDE_OPTIONAL=true ;; + --work) INCLUDE_WORK=true ;; + --personal) INCLUDE_PERSONAL=true ;; + --dry-run) DRY_RUN=true ;; + --distro|--platform) PLATFORM="${2:-}"; shift ;; + --profile) + case "${2:-}" in + server) PLATFORM="server" ;; + desktop) ;; + *) printf 'ERROR: --profile must be desktop or server (got %s).\n' "${2:-}" >&2; exit 1 ;; + esac + shift ;; + *) printf 'Unknown argument: %s\n' "$1" >&2 ;; + esac + shift + done +} + +core_detect_platform() { + if [[ -z "$PLATFORM" ]]; then + if [[ "$(uname -s)" == "Darwin" ]]; then + PLATFORM="macos" + else + local id="" id_like="" + if [[ -r /etc/os-release ]]; then + # shellcheck disable=SC1091 + id="$(. /etc/os-release && echo "${ID:-}")" + # shellcheck disable=SC1091 + id_like="$(. /etc/os-release && echo "${ID_LIKE:-}")" + fi + case "$id" in + ubuntu|debian|linuxmint|pop) PLATFORM="ubuntu" ;; + arch|cachyos|manjaro|endeavouros|garuda|arcolinux) PLATFORM="arch" ;; + *) + case "$id_like" in + *debian*|*ubuntu*) PLATFORM="ubuntu" ;; + *arch*) PLATFORM="arch" ;; + esac ;; + esac + fi + fi + case "$PLATFORM" in + macos|ubuntu|arch|server) ;; + *) + printf 'ERROR: Unsupported platform (got %s).\n' "${PLATFORM:-unknown}" >&2 + printf ' Re-run with --platform macos|ubuntu|arch|server to override.\n' >&2 + exit 1 ;; + esac +} + +# ── Run helpers ─────────────────────────────────────────────────────────────── + +run() { + if [[ "$DRY_RUN" == true ]]; then + printf ' [dry-run] %s\n' "$*" + else + "$@" + fi +} + +run_eval() { + if [[ "$DRY_RUN" == true ]]; then + printf ' [dry-run] eval: %s\n' "$1" + else + eval "$1" + fi +} + +# npm supply-chain cooldown: refuse to install package versions younger than +# NPM_MIN_RELEASE_AGE days. Compromised releases of popular packages (e.g. the +# axios RAT, Mar 2026) are typically caught and yanked within hours, so a short +# cooldown blocks them while barely delaying legit updates. See issue #23. +NPM_MIN_RELEASE_AGE=7 + +configure_npm_cooldown() { + local npmrc="$HOME/.npmrc" key="min-release-age" tmp + if [[ "$DRY_RUN" == true ]]; then + printf ' [dry-run] set %s=%s in %s\n' "$key" "$NPM_MIN_RELEASE_AGE" "$npmrc" + return 0 + fi + touch "$npmrc" + # Drop any existing entry, then append the desired value (idempotent, portable). + tmp="$(mktemp)" + grep -v "^${key}=" "$npmrc" > "$tmp" 2>/dev/null || true + printf '%s=%s\n' "$key" "$NPM_MIN_RELEASE_AGE" >> "$tmp" + mv "$tmp" "$npmrc" + printf ' ✓ npm %s=%s (%s-day supply-chain cooldown)\n' "$key" "$NPM_MIN_RELEASE_AGE" "$NPM_MIN_RELEASE_AGE" +} + +# Enable pnpm (our daily-driver package manager) via corepack — corepack ships +# with Node — and apply the same supply-chain cooldown. pnpm measures +# minimumReleaseAge in MINUTES and enforces it strictly only when set explicitly +# (the built-in 1-day default is non-strict), so we set it on purpose. Issue #23. +configure_pnpm() { + local age_minutes=$((NPM_MIN_RELEASE_AGE * 24 * 60)) + if [[ "$DRY_RUN" == true ]]; then + printf ' [dry-run] corepack enable && corepack prepare pnpm@latest --activate\n' + printf ' [dry-run] pnpm config set minimumReleaseAge %s --location=user\n' "$age_minutes" + return 0 + fi + if ! command -v corepack &>/dev/null; then + printf ' ⚠ corepack not found (needs Node ≥16) — skipping pnpm setup\n' + return 0 + fi + corepack enable + corepack prepare pnpm@latest --activate + export PNPM_HOME="$HOME/.local/share/pnpm" + export PATH="$PNPM_HOME/bin:$PATH" + if command -v pnpm &>/dev/null; then + pnpm config set minimumReleaseAge "$age_minutes" --location=user + printf ' ✓ pnpm enabled, minimumReleaseAge=%s min (%s-day cooldown)\n' "$age_minutes" "$NPM_MIN_RELEASE_AGE" + else + printf ' ⚠ pnpm not on PATH after corepack — skipped cooldown config\n' + fi +} + +# ── Package selection (unified packages.json) ───────────────────────────────── + +# shellcheck disable=SC2016 # $vars below are jq variables, not shell expansions +CORE_JQ_DEFS=' +def envok($w; $p): + (.environment == null) or + (($w == "true") and (.environment | index("work"))) or + (($p == "true") and (.environment | index("personal"))); +def icfor($plat): + (.install_command | if type == "object" then .[$plat] else . end); +def pname($plat): (.[$plat + "_name"] // .name); +' + +# Space-joined install tokens for one manager × priority tier. +pkg_names() { + local manager="$1" priority="$2" + # shellcheck disable=SC2016 + jq -r --arg plat "$PLATFORM" --arg m "$manager" --arg pr "$priority" \ + --arg w "$INCLUDE_WORK" --arg p "$INCLUDE_PERSONAL" \ + "$CORE_JQ_DEFS"'[.[] | select( + .package_manager[$plat] == $m and .priority == $pr and envok($w; $p) + ) | pname($plat)] | join(" ")' "$PACKAGES_JSON" +} + +# install_command of a single package for this platform. +custom_cmd() { + # shellcheck disable=SC2016 + jq -r --arg plat "$PLATFORM" --arg n "$1" \ + "$CORE_JQ_DEFS"'.[] | select(.name == $n and .package_manager[$plat] != null) + | icfor($plat) // empty' "$PACKAGES_JSON" +} + +# Manual-install reminders for one tier's custom packages that the engine does +# not run itself (handled_by_setup != true). +print_custom_reminders() { + local priority="$1" items + # shellcheck disable=SC2016 + items=$(jq -r --arg plat "$PLATFORM" --arg pr "$priority" \ + --arg w "$INCLUDE_WORK" --arg p "$INCLUDE_PERSONAL" \ + "$CORE_JQ_DEFS"'.[] | select( + .package_manager[$plat] == "custom" and .priority == $pr and + (.handled_by_setup != true) and envok($w; $p) + ) | " - \(.name)\n \(.description)\n Install: \(icfor($plat))"' \ + "$PACKAGES_JSON") + [[ -n "$items" ]] && printf '%s\n' "$items" + return 0 +} + +custom_reminders_section() { + local reminders low + reminders="$(print_custom_reminders medium)" + if [[ "$INCLUDE_OPTIONAL" == true ]]; then + low="$(print_custom_reminders low)" + if [[ -n "$low" ]]; then + [[ -n "$reminders" ]] && reminders+=$'\n' + reminders+="$low" + fi + fi + if [[ -n "$reminders" ]]; then + printf '\n' + printf 'Install these manually (require their own repo setup):\n' + printf '%s\n' "$reminders" + fi + return 0 +} + +pipx_install_tier() { + local priority="$1" + # shellcheck disable=SC2016 + jq -r --arg plat "$PLATFORM" --arg pr "$priority" \ + --arg w "$INCLUDE_WORK" --arg p "$INCLUDE_PERSONAL" \ + "$CORE_JQ_DEFS"'.[] | select( + .package_manager[$plat] == "pipx" and .priority == $pr and envok($w; $p) + ) | (icfor($plat) // ("pipx install " + .name))' "$PACKAGES_JSON" | + while read -r cmd; do + run_eval "$cmd" + done +} + +pnpm_install_tier() { + local priority="$1" names + # shellcheck disable=SC2016 + names=$(jq -r --arg plat "$PLATFORM" --arg pr "$priority" \ + --arg w "$INCLUDE_WORK" --arg p "$INCLUDE_PERSONAL" \ + "$CORE_JQ_DEFS"'[.[] | select( + .package_manager[$plat] == "pnpm" and .priority == $pr and envok($w; $p) + ) | .name] | join(" ")' "$PACKAGES_JSON") + [[ -z "$names" ]] && return 0 + # shellcheck disable=SC2086 + run pnpm add -g $names +} + +# ── Shared post-install steps ───────────────────────────────────────────────── + +# Deploy ~/.zshrc from the shared dotfiles base unless the platform folder +# ships its own zshrc.example override (linux-server does — headless, so its +# zshrc drops Ghostty/fastfetch/notification hooks). +deploy_zshrc() { + local src="$SETUP_ROOT/dotfiles/zshrc.example" from="dotfiles/zshrc.example" + if [[ -f "$CONFIG_SRC_DIR/zshrc.example" ]]; then + src="$CONFIG_SRC_DIR/zshrc.example" + from="${CONFIG_SRC_DIR##*/}/zshrc.example (platform override)" + fi + deploy_config "$src" "$HOME/.zshrc" "$from" yes +} + +# Copy a config file into place, backing up a differing existing one. +deploy_config() { + local src="$1" dst="$2" from="${3:-}" backup="${4:-yes}" + local label="~${dst#"$HOME"}" suffix="" bkp + [[ -n "$from" ]] && suffix=" from $from" + if [[ ! -f "$dst" ]]; then + run cp "$src" "$dst" + printf '==> %s installed%s\n' "$label" "$suffix" + elif ! diff -q "$src" "$dst" &>/dev/null; then + if [[ "$backup" == yes ]]; then + bkp="$dst.bak.$(date +%Y%m%d_%H%M%S)" + run cp "$dst" "$bkp" + run cp "$src" "$dst" + printf '==> %s updated (backup saved to %s)\n' "$label" "$bkp" + else + run cp "$src" "$dst" + printf '==> %s updated\n' "$label" + fi + else + printf '==> %s already up to date\n' "$label" + fi +} + +# Use the real login shell from /etc/passwd — $SHELL can be an inherited +# interactive shell (e.g. zsh launched over an SSH session whose login shell is fish). +set_default_shell() { + local zsh_bin current + zsh_bin="$(command -v zsh 2>/dev/null || true)" + [[ -z "$zsh_bin" ]] && return 0 + current="$(getent passwd "$USER" | cut -d: -f7)" + if [[ "$current" != "$zsh_bin" ]]; then + printf '\n==> Setting zsh as default shell (was: %s)...\n' "${current:-unknown}" + run sudo usermod -s "$zsh_bin" "$USER" + else + printf '\n==> zsh already the default login shell\n' + fi +} + +# bat / fd-find symlinks (Ubuntu/Debian install them as batcat / fdfind) +setup_bat_fd_symlinks() { + printf '\n==> Setting up bat and fd symlinks...\n' + local pair src dst + for pair in "batcat bat" "fdfind fd"; do + src="${pair%% *}"; dst="${pair##* }" + if command -v "$src" &>/dev/null && ! command -v "$dst" &>/dev/null; then + run sudo ln -sf "$(command -v "$src")" /usr/local/bin/"$dst" + printf ' linked %s → %s\n' "$dst" "$src" + else + printf ' ✓ %s\n' "$dst" + fi + done +} + +# apt update + jq bootstrap + external apt repos (shared by ubuntu and server). +apt_bootstrap() { + printf '==> Updating package list...\n' + run sudo apt update + + if ! command -v jq &>/dev/null; then + printf '==> Bootstrapping jq...\n' + run sudo apt install -y jq + fi + + local need_update=false + + # fastfetch — not in standard Ubuntu repos + if ! apt-cache show fastfetch &>/dev/null 2>&1; then + printf '==> Adding fastfetch PPA...\n' + run sudo add-apt-repository -y ppa:zhangsongcui3371/fastfetch + need_update=true + fi + + # eza — not in standard Ubuntu repos + if ! apt-cache show eza &>/dev/null 2>&1; then + printf '==> Adding eza apt repo...\n' + if [[ "$DRY_RUN" != true ]]; then + sudo apt install -y gpg + wget -qO- https://raw.githubusercontent.com/eza-community/eza/main/deb.asc \ + | sudo gpg --dearmor -o /etc/apt/keyrings/gierens.gpg + echo "deb [signed-by=/etc/apt/keyrings/gierens.gpg] http://deb.gierens.de stable main" \ + | sudo tee /etc/apt/sources.list.d/gierens.list > /dev/null + sudo chmod 644 /etc/apt/keyrings/gierens.gpg /etc/apt/sources.list.d/gierens.list + else + printf ' [dry-run] add eza community apt repo\n' + fi + need_update=true + fi + + # obs-studio — noble's archive lags; OBS officially recommends their PPA. + # Ubuntu-only: the server doesn't install obs-studio. + if [[ "$PLATFORM" == "ubuntu" ]] && ! grep -rq obsproject /etc/apt/sources.list.d/ 2>/dev/null; then + printf '==> Adding OBS Studio PPA...\n' + run sudo add-apt-repository -y ppa:obsproject/obs-studio + need_update=true + fi + + # gh — GitHub CLI apt repo. Gate on the repo file, not the binary: a stale + # distro-repo gh (e.g. Ubuntu universe 2.45) must still get migrated (#25). + if [[ ! -f /etc/apt/sources.list.d/github-cli.list ]]; then + printf '==> Adding GitHub CLI apt repo...\n' + if [[ "$DRY_RUN" != true ]]; then + wget -qO- https://cli.github.com/packages/githubcli-archive-keyring.gpg \ + | sudo gpg --dearmor -o /usr/share/keyrings/githubcli-archive-keyring.gpg + sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ + | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null + else + printf ' [dry-run] add GitHub CLI apt repo\n' + fi + need_update=true + fi + + [[ "$need_update" == true ]] && run sudo apt update + return 0 +} + +# Shared by the Ubuntu desktop and the Ubuntu Server profile (both ship snapd). +snap_install_tier() { + local priority="$1" regular_snaps custom_snaps snap_name name cmd + # shellcheck disable=SC2016 + regular_snaps=$(jq -r --arg plat "$PLATFORM" --arg pr "$priority" \ + --arg w "$INCLUDE_WORK" --arg p "$INCLUDE_PERSONAL" \ + "$CORE_JQ_DEFS"'[.[] | select( + .package_manager[$plat] == "snap" and .priority == $pr and + envok($w; $p) and (icfor($plat) == null) + ) | pname($plat)] | join(" ")' "$PACKAGES_JSON") + # Snaps needing flags (e.g. --classic) carry their full install_command. + # shellcheck disable=SC2016 + custom_snaps=$(jq -r --arg plat "$PLATFORM" --arg pr "$priority" \ + --arg w "$INCLUDE_WORK" --arg p "$INCLUDE_PERSONAL" \ + "$CORE_JQ_DEFS"'.[] | select( + .package_manager[$plat] == "snap" and .priority == $pr and + envok($w; $p) and (icfor($plat) != null) + ) | "\(.name)|\(icfor($plat))"' "$PACKAGES_JSON") + + if [[ -n "$regular_snaps" ]]; then + for snap_name in $regular_snaps; do + run sudo snap install "$snap_name" + done + fi + + if [[ -n "$custom_snaps" ]]; then + while IFS='|' read -r name cmd; do + printf ' %s (custom snap)...\n' "$name" + run_eval "$cmd" + done <<< "$custom_snaps" + fi +} + +# Echo the other repo setup scripts (those present on disk) for discoverability. +# One list for all platforms — root verify.sh auto-detects, so nothing diverges. +RELATED_SCRIPTS=( + "agentic-ai/Claude/install.sh|Claude Code config — deploy settings, hooks, and CLAUDE.md into ~/.claude" + "SSH_and_GPG/create_ssh_key.sh|Generate an SSH key (and add it to GitHub)" + "SSH_and_GPG/create_gpg_key.sh|Generate a GPG key for signed commits" + "verify.sh|Verify this install (read-only health check; auto-detects platform)" +) + +print_related_scripts() { + local entry rel desc shown=false + for entry in "${RELATED_SCRIPTS[@]}"; do + rel="${entry%%|*}"; desc="${entry#*|}" + [[ -f "$SETUP_ROOT/$rel" ]] || continue + if [[ "$shown" == false ]]; then + printf ' Other setup scripts in this repo:\n' + shown=true + fi + printf ' • %s\n' "$desc" + printf ' bash %s\n' "$rel" + done +} + +# ── Linux desktop flow (shared by ubuntu and arch modules) ──────────────────── + +linux_pyenv_flow() { + printf '\n' + if [[ "$DRY_RUN" == true ]]; then + printf '==> pyenv...\n' + printf ' [dry-run] eval: curl https://pyenv.run | bash\n' + printf ' [dry-run] pyenv install %s && pyenv global %s\n' "$PYTHON_VERSION" "$PYTHON_VERSION" + return 0 + fi + if [[ ! -d "$HOME/.pyenv" ]]; then + printf '==> Installing pyenv...\n' + eval "$(custom_cmd pyenv)" + else + printf '==> pyenv already installed\n' + fi + + export PYENV_ROOT="$HOME/.pyenv" + export PATH="$PYENV_ROOT/bin:$PATH" + if command -v pyenv &>/dev/null; then + # Force bash output — pyenv otherwise detects the shell from $SHELL and may + # emit fish/zsh syntax that this bash script's eval rejects (aborts under set -e). + eval "$(pyenv init - bash)" + if ! pyenv versions 2>/dev/null | grep -q "$PYTHON_VERSION"; then + printf '==> Installing pyenv build dependencies...\n' + platform_pyenv_build_deps + printf '==> Installing Python %s via pyenv...\n' "$PYTHON_VERSION" + pyenv install "$PYTHON_VERSION" + pyenv global "$PYTHON_VERSION" + else + printf '==> Python %s already installed\n' "$PYTHON_VERSION" + fi + fi +} + +linux_nvm_flow() { + printf '\n' + if [[ "$DRY_RUN" == true ]]; then + printf '==> nvm...\n' + printf ' [dry-run] install nvm via curl\n' + printf ' [dry-run] nvm install lts/* && nvm alias default lts/*\n' + configure_npm_cooldown + configure_pnpm + return 0 + fi + if [ ! -d "$HOME/.nvm" ]; then + printf '==> Installing nvm...\n' + eval "$(custom_cmd nvm)" + else + printf '==> nvm already installed\n' + fi + + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + + if command -v nvm &>/dev/null; then + nvm install 'lts/*' + nvm alias default 'lts/*' + nvm use default + fi + + # Apply the supply-chain cooldown before any package install runs below. + configure_npm_cooldown + configure_pnpm +} + +# rustup ships no toolchain — rustc/cargo are proxies that error until +# 'rustup default stable' picks one. +rust_toolchain_step() { + command -v rustup &>/dev/null || return 0 + printf '\n' + if [[ "$DRY_RUN" == true ]]; then + printf ' [dry-run] rustup default stable (if no default toolchain)\n' + elif ! rustup show active-toolchain &>/dev/null; then + printf '==> Installing stable Rust toolchain...\n' + run rustup default stable + else + printf '==> Rust toolchain already configured (%s)\n' "$(rustup show active-toolchain 2>/dev/null | head -1)" + fi +} + +claude_code_step() { + printf '\n' + if [[ "$DRY_RUN" == true ]]; then + command -v claude &>/dev/null \ + && printf '==> claude-code already installed\n' \ + || printf ' [dry-run] eval: curl -fsSL https://claude.ai/install.sh | bash\n' + elif ! command -v claude &>/dev/null; then + printf '==> Installing claude-code...\n' + eval "$(custom_cmd claude-code)" + else + printf '==> claude-code already installed\n' + fi +} + +ghostty_deploy_linux() { + local cfg_dir="${XDG_CONFIG_HOME:-$HOME/.config}/ghostty" + local cfg="$cfg_dir/config" + local src="$SETUP_ROOT/dotfiles/ghostty.config" + printf '\n' + if [[ "$DRY_RUN" == true ]]; then + printf ' [dry-run] mkdir -p %s\n' "$cfg_dir" + printf ' [dry-run] cp ghostty.config → %s\n' "$cfg" + return 0 + fi + mkdir -p "$cfg_dir" + if [[ -f "$cfg" ]] && ! diff -q "$src" "$cfg" &>/dev/null; then + cp "$cfg" "${cfg}.bak" + cp "$src" "$cfg" + printf '==> Ghostty config updated (backup saved)\n' + elif [[ ! -f "$cfg" ]]; then + cp "$src" "$cfg" + printf '==> Ghostty config installed to %s\n' "$cfg" + else + printf '==> Ghostty config already up to date\n' + fi +} + +desktop_pipx_section() { + printf '\n==> Installing pipx packages...\n' + if command -v pipx &>/dev/null; then + pipx ensurepath + pipx_install_tier "medium" + else + printf ' pipx not found — skipping\n' + fi +} + +desktop_pnpm_section() { + printf '\n==> Installing pnpm packages...\n' + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + export PNPM_HOME="$HOME/.local/share/pnpm" + export PATH="$PNPM_HOME/bin:$PATH" + if command -v pnpm &>/dev/null; then + pnpm_install_tier "medium" + else + printf ' pnpm not found — skipping\n' + fi +} + +desktop_footer() { + printf '\n' + if [[ "$DRY_RUN" == true ]]; then + printf 'Dry run complete — nothing was installed.\n' + return 0 + fi + printf '================================================================\n' + printf ' Done. A few manual steps remain:\n' + printf '================================================================\n' + printf '\n' + printf ' 1. Log out and back in — activates zsh (and docker group if installed)\n' + printf '\n' + printf ' 2. Open a new Ghostty terminal — antidote will clone plugins\n' + printf ' on first launch (takes ~10 seconds)\n' + printf '\n' + printf ' 3. Authenticate Tailscale:\n' + printf ' sudo tailscale up\n' + printf '\n' + printf ' 4. Optional — run these from the repo root as needed:\n' + printf '\n' + print_related_scripts + printf '\n' + printf '================================================================\n' +} + +desktop_main() { + printf '==> Detected distro family: %s\n' "$PLATFORM" + CONFIG_SRC_DIR="$SETUP_ROOT/linux-desktop" + platform_bootstrap + + printf '\n' + platform_install_tier high + + linux_pyenv_flow + linux_nvm_flow + + printf '\n' + platform_install_tier medium + + platform_tailscale_step + claude_code_step + rust_toolchain_step + desktop_pipx_section + desktop_pnpm_section + + set_default_shell + printf '\n' + deploy_zshrc + printf '\n' + deploy_config "$SETUP_ROOT/dotfiles/tmux.conf" "$HOME/.tmux.conf" "tmux.conf" yes + printf '\n' + deploy_config "$SETUP_ROOT/dotfiles/zsh_plugins.txt" "$HOME/.zsh_plugins.txt" "" no + printf '\n' + deploy_config "$SETUP_ROOT/dotfiles/p10k.zsh.example" "$HOME/.p10k.zsh" "p10k.zsh.example" yes + ghostty_deploy_linux + + if [[ "$INCLUDE_OPTIONAL" == true ]]; then + printf '\n==> Installing optional (low) packages...\n' + platform_install_tier low + pipx_install_tier low + pnpm_install_tier low + platform_docker_optional + fi + + custom_reminders_section + + desktop_footer +} diff --git a/lib/verify.sh b/lib/verify.sh new file mode 100755 index 0000000..3e30898 --- /dev/null +++ b/lib/verify.sh @@ -0,0 +1,295 @@ +#!/usr/bin/env bash +# Shared read-only verify engine for the unified root verify.sh (UNIFICATION.md +# Phase 3, issue #36). Sourced by verify.sh after lib/core.sh — reuses +# CORE_JQ_DEFS, PACKAGES_JSON, PLATFORM, INCLUDE_* and core_detect_platform. +# Check semantics are ported from macOS/verify.sh and linux-desktop/verify.sh; +# where they differ per manager (e.g. command -v fallbacks), each platform's +# legacy behavior is preserved. + +INCLUDE_NONE=false + +verify_parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + --optional) INCLUDE_OPTIONAL=true ;; + --work) INCLUDE_WORK=true ;; + --personal) INCLUDE_PERSONAL=true ;; + --all) INCLUDE_OPTIONAL=true; INCLUDE_WORK=true; INCLUDE_PERSONAL=true; INCLUDE_NONE=true ;; + --distro|--platform) PLATFORM="${2:-}"; shift ;; + --profile) + case "${2:-}" in + server) PLATFORM="server" ;; + desktop) ;; + *) printf 'ERROR: --profile must be desktop or server (got %s).\n' "${2:-}" >&2; exit 1 ;; + esac + shift ;; + *) printf 'Unknown argument: %s\n' "$1" >&2 ;; + esac + shift + done +} + +PASS=0 +FAIL=0 + +check() { + local label="$1" ok="$2" + if [[ "$ok" == true ]]; then + printf ' ✅ %s\n' "$label" + PASS=$((PASS + 1)) + else + printf ' ❌ %s\n' "$label" + FAIL=$((FAIL + 1)) + fi +} + +# Sets INSTALLED (true|false) and LABEL_NOTE (appended to the check label). +probe_pkg() { + local mgr="$1" name="$2" rname="$3" + INSTALLED=false + LABEL_NOTE="" + case "$mgr" in + brew) + if brew list --formula "$rname" &>/dev/null; then + INSTALLED=true + elif command -v "$rname" &>/dev/null; then + INSTALLED=true + LABEL_NOTE=" (via system/other)" + fi ;; + brew-cask) + brew list --cask "$rname" &>/dev/null && INSTALLED=true ;; + app-store) + [[ -d "/Applications/${rname}.app" || -d "$HOME/Applications/${rname}.app" ]] && INSTALLED=true + [[ "$INSTALLED" == false ]] && LABEL_NOTE=" (check App Store manually)" ;; + yay) + { pacman -Qq "$rname" || command -v "$name"; } &>/dev/null && INSTALLED=true ;; + apt) + { dpkg -s "$rname" || command -v "$name"; } &>/dev/null && INSTALLED=true ;; + snap) + { snap list "$rname" || command -v "$name"; } &>/dev/null && INSTALLED=true ;; + pipx) + pipx list --short 2>/dev/null | awk '{print $1}' | grep -qx "$rname" && INSTALLED=true + if [[ "$INSTALLED" == false && "$PLATFORM" != "macos" ]]; then + command -v "$name" &>/dev/null && INSTALLED=true + fi ;; + pnpm) + pnpm list -g --depth=0 2>/dev/null | grep -q " ${rname}@" && INSTALLED=true + [[ "$INSTALLED" == false ]] && command -v "$name" &>/dev/null && INSTALLED=true ;; + custom) + case "$name" in + pyenv) [[ -d "$HOME/.pyenv" ]] && INSTALLED=true ;; + nvm) [[ -s "$HOME/.nvm/nvm.sh" ]] && INSTALLED=true ;; + claude-code) command -v claude &>/dev/null && INSTALLED=true ;; + forgejo-cli) command -v fj &>/dev/null && INSTALLED=true ;; + zen-browser) flatpak info app.zen_browser.zen &>/dev/null && INSTALLED=true ;; + *) + if [[ "$PLATFORM" == "macos" ]]; then + { brew list --formula "$rname" || command -v "$rname"; } &>/dev/null && INSTALLED=true + else + { command -v "$name" || pacman -Qq "$rname"; } &>/dev/null && INSTALLED=true + fi ;; + esac ;; + *) + LABEL_NOTE=" (unknown manager: $mgr)" ;; + esac + return 0 +} + +verify_section() { + local section_label="$1" priority="$2" rows + # shellcheck disable=SC2016 + rows=$(jq -r --arg plat "$PLATFORM" --arg pr "$priority" \ + --arg w "$INCLUDE_WORK" --arg p "$INCLUDE_PERSONAL" \ + "$CORE_JQ_DEFS"'.[] | select( + .package_manager[$plat] != null and .priority == $pr and envok($w; $p) + ) | [.name, .package_manager[$plat], pname($plat), (.optional | tostring)] | @tsv' \ + "$PACKAGES_JSON") + + [[ -z "$rows" ]] && return 0 + + printf '\n── %s ──────────────────────────────────────────\n' "$section_label" + + local name mgr rname opt_flag label + while IFS=$'\t' read -r name mgr rname opt_flag; do + [[ -z "$name" ]] && continue + label="$name ($mgr" + [[ "$rname" != "$name" ]] && label="$label:$rname" + label="$label)" + [[ "$opt_flag" == "true" ]] && label="$label [optional]" + probe_pkg "$mgr" "$name" "$rname" + check "$label$LABEL_NOTE" "$INSTALLED" + done <<< "$rows" +} + +# ── Runtime environment + config checks (ported per platform) ───────────────── + +verify_extras_macos() { + printf '\n── Runtime environments ──────────────────────────────────────\n' + + command -v brew &>/dev/null && check "homebrew (brew)" true || check "homebrew (brew)" false + + command -v pyenv &>/dev/null && check "pyenv" true || check "pyenv" false + + local py_ver + py_ver=$(pyenv version-name 2>/dev/null || echo "") + if [[ -n "$py_ver" && "$py_ver" != "system" ]]; then + check "python via pyenv ($py_ver)" true + else + check "python via pyenv (no pyenv version active)" false + fi + + export NVM_DIR="$HOME/.nvm" + # shellcheck disable=SC1091 + [[ -s "$NVM_DIR/nvm.sh" ]] && source "$NVM_DIR/nvm.sh" 2>/dev/null + command -v nvm &>/dev/null && check "nvm" true || check "nvm" false + + local node_ver nvm_cur + node_ver=$(node --version 2>/dev/null || echo "") + nvm_cur=$(nvm current 2>/dev/null || echo "") + if [[ -n "$node_ver" && "$nvm_cur" != "system" && -n "$nvm_cur" ]]; then + check "node via nvm ($node_ver)" true + else + check "node via nvm (run: nvm install 'lts/*')" false + fi + + verify_npm_pnpm_cooldowns + + [[ -f "$HOME/.zshrc" ]] && check "zshrc present (~/.zshrc)" true || check "zshrc present (~/.zshrc)" false + [[ -f "$HOME/.zsh_plugins.txt" ]] && check "antidote plugin list present (~/.zsh_plugins.txt)" true || check "antidote plugin list present (~/.zsh_plugins.txt)" false + [[ -f "$HOME/.tmux.conf" ]] && check "tmux config present (~/.tmux.conf)" true || check "tmux config present (~/.tmux.conf)" false + + # antidote (brew formula puts antidote.zsh under /opt/homebrew/opt/antidote) + if [[ -f /opt/homebrew/opt/antidote/share/antidote/antidote.zsh ]]; then + check "antidote available" true + else + check "antidote available" false + fi + + local ghostty_cfg="${XDG_CONFIG_HOME:-$HOME/.config}/ghostty/config" + [[ -f "$ghostty_cfg" ]] && check "ghostty config at XDG path" true || check "ghostty config at XDG path" false + + if [[ -n "${PIPX_DEFAULT_PYTHON:-}" && -f "${PIPX_DEFAULT_PYTHON:-}" ]]; then + check "PIPX_DEFAULT_PYTHON → pyenv ($PIPX_DEFAULT_PYTHON)" true + else + check "PIPX_DEFAULT_PYTHON → pyenv (set in .zshrc, source it first)" false + fi +} + +verify_extras_linux() { + printf '\n── Runtime environment & configs ────────────────────────────\n' + + local login_shell zsh_bin + login_shell="$(getent passwd "$USER" | cut -d: -f7)" + zsh_bin="$(command -v zsh 2>/dev/null || true)" + if [[ -n "$zsh_bin" && "$login_shell" == "$zsh_bin" ]]; then + check "login shell is zsh ($login_shell)" true + else + check "login shell is zsh (currently ${login_shell:-unknown})" false + fi + + export PYENV_ROOT="$HOME/.pyenv" + [[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH" + if command -v pyenv &>/dev/null; then + local py_ver + py_ver=$(pyenv version-name 2>/dev/null || echo "") + if [[ -n "$py_ver" && "$py_ver" != "system" ]]; then + check "python via pyenv ($py_ver)" true + else + check "python via pyenv (no version active — run: pyenv global )" false + fi + else + check "pyenv on PATH" false + fi + + export NVM_DIR="$HOME/.nvm" + # shellcheck disable=SC1091 + [[ -s "$NVM_DIR/nvm.sh" ]] && source "$NVM_DIR/nvm.sh" 2>/dev/null + if command -v nvm &>/dev/null; then + local node_ver + node_ver=$(node --version 2>/dev/null || echo "") + if [[ -n "$node_ver" ]]; then + check "node via nvm ($node_ver)" true + else + check "node via nvm (run: nvm install 'lts/*')" false + fi + else + check "nvm loadable" false + fi + + verify_npm_pnpm_cooldowns + + [[ -f "$HOME/.zshrc" ]] && check "zshrc present (~/.zshrc)" true || check "zshrc present (~/.zshrc)" false + [[ -f "$HOME/.zsh_plugins.txt" ]] && check "antidote plugin list present (~/.zsh_plugins.txt)" true || check "antidote plugin list present (~/.zsh_plugins.txt)" false + [[ -f "$HOME/.tmux.conf" ]] && check "tmux config present (~/.tmux.conf)" true || check "tmux config present (~/.tmux.conf)" false + + # antidote (Arch AUR package puts it under /usr/share/zsh-antidote) + if [[ -f /usr/share/zsh-antidote/antidote.zsh ]] || command -v antidote &>/dev/null; then + check "antidote available" true + else + check "antidote available" false + fi + + local ghostty_cfg="${XDG_CONFIG_HOME:-$HOME/.config}/ghostty/config" + [[ -f "$ghostty_cfg" ]] && check "ghostty config present" true || check "ghostty config present" false + + if command -v systemctl &>/dev/null; then + if systemctl is-active --quiet tailscaled 2>/dev/null; then + check "tailscaled service active" true + else + check "tailscaled service active (run: sudo systemctl enable --now tailscaled)" false + fi + fi +} + +# npm + pnpm supply-chain cooldown checks (issue #23) — identical on every platform. +verify_npm_pnpm_cooldowns() { + if grep -q '^min-release-age=' "$HOME/.npmrc" 2>/dev/null; then + local mra + mra=$(grep '^min-release-age=' "$HOME/.npmrc" | tail -1 | cut -d= -f2) + check "npm min-release-age set (${mra}-day cooldown)" true + else + check "npm min-release-age set (run setup.sh)" false + fi + + export PNPM_HOME="$HOME/.local/share/pnpm" + [[ -d "$PNPM_HOME/bin" ]] && export PATH="$PNPM_HOME/bin:$PATH" + if command -v pnpm &>/dev/null; then + check "pnpm ($(pnpm --version 2>/dev/null))" true + local pmra + pmra=$(pnpm config get minimumReleaseAge 2>/dev/null) + if [[ -n "$pmra" && "$pmra" != "undefined" && "$pmra" -gt 0 ]] 2>/dev/null; then + check "pnpm minimumReleaseAge set (${pmra} min)" true + else + check "pnpm minimumReleaseAge set (run setup.sh)" false + fi + else + check "pnpm (run setup.sh — corepack enable)" false + fi +} + +verify_main() { + if [[ "$PLATFORM" == "server" ]]; then + printf 'ERROR: no verify checks for the server profile yet (linux-server has no legacy verify.sh).\n' >&2 + exit 1 + fi + + printf '==> Verifying %s package installs against %s\n' "$PLATFORM" "$PACKAGES_JSON" + + case "$PLATFORM" in + macos) verify_extras_macos ;; + ubuntu|arch) verify_extras_linux ;; + esac + + verify_section "High priority" "high" + verify_section "Medium priority" "medium" + [[ "$INCLUDE_OPTIONAL" == true ]] && verify_section "Low priority (optional)" "low" + [[ "$INCLUDE_NONE" == true ]] && verify_section "Priority none (manual only)" "none" + + printf '\n────────────────────────────────────────────────────────────\n' + printf ' ✅ %s ok ❌ %s missing\n\n' "$PASS" "$FAIL" + if [[ $FAIL -gt 0 ]]; then + printf 'Run setup.sh (with matching flags) to install missing items, or install manually.\n' + else + printf 'All checked items are present.\n' + fi +} diff --git a/linux-desktop/README.md b/linux-desktop/README.md index e4abf15..4c45aee 100644 --- a/linux-desktop/README.md +++ b/linux-desktop/README.md @@ -5,9 +5,11 @@ Tested on CachyOS (Arch-based). Most packages should work on other Arch or Debia ## Setup ```bash -bash linux-desktop/setup.sh [--work] [--personal] [--optional] [--dry-run] +bash setup.sh [--work] [--personal] [--optional] [--dry-run] ``` +(Run from the repo root; `linux-desktop/setup.sh` is a thin shim onto it.) + `setup.sh` auto-detects the distro from `/etc/os-release`: - **Arch / CachyOS** (and Manjaro, EndeavourOS, …) — installs via `yay` (repo + AUR), @@ -18,13 +20,13 @@ Either way, `pyenv` and `nvm` install via their official curl scripts (`~/.pyenv the login shell is switched to `zsh`, and the `zshrc` / antidote / Ghostty configs are copied into place. Use `--dry-run` to preview every command without changing anything. -Package definitions and per-distro names live in -[`linux_desktop_packages.json`](linux_desktop_packages.json). +Package definitions and per-distro names live in the root +[`packages.json`](../packages.json). After running, verify the result (read-only — installs nothing): ```bash -bash linux-desktop/verify.sh [--work] [--personal] [--optional] [--all] +bash verify.sh [--work] [--personal] [--optional] [--all] ``` `verify.sh` mirrors `setup.sh`'s selection logic, so passing the same flags checks diff --git a/linux-desktop/ghostty.config b/linux-desktop/ghostty.config deleted file mode 100644 index de7ade1..0000000 --- a/linux-desktop/ghostty.config +++ /dev/null @@ -1,6 +0,0 @@ -# Ghostty Config - -theme = Dark Pastel -font-family = MesloLGS Nerd Font Mono -shell-integration = zsh -term = xterm-256color diff --git a/linux-desktop/linux_desktop_packages.json b/linux-desktop/linux_desktop_packages.json deleted file mode 100644 index 6c6ade3..0000000 --- a/linux-desktop/linux_desktop_packages.json +++ /dev/null @@ -1,494 +0,0 @@ -[ - { - "name": "zsh", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "priority": "high", - "optional": false, - "description": "Z shell — common shell across all devices; install first so plugins are available" - }, - { - "name": "zsh-antidote", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "priority": "high", - "optional": false, - "description": "Lightweight zsh plugin manager — handles autosuggestions, syntax highlighting, completions, substring search, and abbreviations via a plugins file" - }, - { - "name": "zsh-theme-powerlevel10k", - "package_manager": { "arch": "yay" }, - "priority": "high", - "optional": false, - "description": "Powerlevel10k zsh prompt — rich git status, command duration, exit code, instant prompt; Arch only (Ubuntu: add romkatv/powerlevel10k to zsh_plugins.txt); run `p10k configure` to set up" - }, - { - "name": "zoxide", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "priority": "high", - "optional": false, - "description": "Smarter cd — learns your frequent directories, use z to jump instantly" - }, - { - "name": "pyenv", - "package_manager": { "ubuntu": "curl", "arch": "curl" }, - "install_command": { "ubuntu": "curl https://pyenv.run | bash", "arch": "curl https://pyenv.run | bash" }, - "priority": "high", - "optional": false, - "description": "Python version manager — compiles from source to avoid distro-packaged Python quirks" - }, - { - "name": "nvm", - "package_manager": { "ubuntu": "curl", "arch": "curl" }, - "install_command": { "ubuntu": "curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash", "arch": "curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash" }, - "priority": "high", - "optional": false, - "description": "Node version manager — installed via curl on Ubuntu per nvm recommendation" - }, - { - "name": "pipx", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "arch_name": "python-pipx", - "priority": "high", - "optional": false, - "description": "Installs Python CLI tools in isolated virtual environments" - }, - { - "name": "build-essential", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "arch_name": "base-devel", - "priority": "high", - "optional": false, - "description": "Compiler toolchain — gcc, make, etc.; required for pyenv, native Node modules, and AUR builds" - }, - { - "name": "rust", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "ubuntu_name": "rustup", - "priority": "medium", - "optional": false, - "description": "Rust toolchain — rustc and cargo" - }, - { - "name": "git", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "priority": "high", - "optional": false, - "description": "Version control — usually pre-installed but listed to ensure it's present" - }, - { - "name": "ghostty", - "package_manager": { "ubuntu": "snap", "arch": "yay" }, - "install_command": { "ubuntu": "sudo snap install ghostty --classic" }, - "priority": "medium", - "optional": false, - "description": "GPU-accelerated terminal emulator — requires --classic on snap for full system access" - }, - { - "name": "fastfetch", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "priority": "medium", - "optional": false, - "description": "Fast system info display — shown on Ghostty startup" - }, - { - "name": "gh", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "arch_name": "github-cli", - "priority": "medium", - "optional": false, - "description": "GitHub CLI — PRs, issues, Actions from the terminal" - }, - { - "name": "git-lfs", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "priority": "medium", - "optional": false, - "description": "Git extension for versioning large binary files" - }, - { - "name": "pre-commit", - "package_manager": { "ubuntu": "pipx", "arch": "yay" }, - "priority": "medium", - "optional": false, - "description": "Git pre-commit hook framework — runs this repo's shellcheck, JSON, and zsh syntax checks before each commit" - }, - { - "name": "shellcheck", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "priority": "medium", - "optional": false, - "description": "Static analysis linter for shell scripts — used by the pre-commit config and CI" - }, - { - "name": "gnupg", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "priority": "medium", - "optional": false, - "description": "GPG — required for commit signing" - }, - { - "name": "htop", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "priority": "medium", - "optional": false, - "description": "Interactive process viewer — colorized alternative to top" - }, - { - "name": "nvtop", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "priority": "medium", - "optional": false, - "description": "GPU process monitor — complements htop with GPU visibility" - }, - { - "name": "lact", - "package_manager": { "arch": "yay" }, - "priority": "medium", - "optional": false, - "description": "Linux AMD GPU control panel — fan curves, voltage offset, power cap, clock limits via GUI + background daemon; see linux-desktop/lact_r9700_tuning.md for R9700 config" - }, - { - "name": "tmux", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "priority": "medium", - "optional": false, - "description": "Terminal multiplexer — split panes, persist sessions" - }, - { - "name": "micro", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "priority": "medium", - "optional": false, - "description": "Modern terminal text editor with mouse support and familiar keybindings" - }, - { - "name": "eza", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "priority": "medium", - "optional": false, - "description": "Modern ls replacement — colors, icons, git status, tree view" - }, - { - "name": "fzf", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "priority": "medium", - "optional": false, - "description": "Fuzzy finder — Ctrl+R history search, Ctrl+T file search" - }, - { - "name": "ripgrep", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "priority": "medium", - "optional": false, - "description": "Fast recursive grep replacement — command: rg" - }, - { - "name": "bat", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "ubuntu_name": "bat", - "priority": "medium", - "optional": false, - "description": "cat with syntax highlighting and git integration — Ubuntu installs binary as batcat" - }, - { - "name": "fd", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "ubuntu_name": "fd-find", - "priority": "medium", - "optional": false, - "description": "Fast find alternative — Ubuntu installs binary as fdfind" - }, - { - "name": "jq", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "priority": "medium", - "optional": false, - "description": "JSON processor — required by setup.sh to parse this file" - }, - { - "name": "curl", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "priority": "medium", - "optional": false, - "description": "HTTP client — usually pre-installed; required by various install scripts" - }, - { - "name": "wget", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "priority": "medium", - "optional": false, - "description": "HTTP file downloader — used by setup scripts for key imports" - }, - { - "name": "sshpass", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "priority": "medium", - "optional": false, - "description": "Non-interactive SSH password authentication — used by SSH_and_GPG/add_remote_host.sh" - }, - { - "name": "libnotify-bin", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "arch_name": "libnotify", - "priority": "medium", - "optional": false, - "description": "Desktop notifications CLI (notify-send) — used by zsh-notify plugin" - }, - { - "name": "xdotool", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "priority": "medium", - "optional": false, - "description": "X11 window automation — used by zsh-notify for terminal focus detection" - }, - { - "name": "tailscale", - "package_manager": { "ubuntu": "custom", "arch": "yay" }, - "install_command": { "ubuntu": "curl -fsSL https://tailscale.com/install.sh | sudo sh" }, - "priority": "medium", - "optional": false, - "description": "Zero-config VPN — access all devices on a private network" - }, - { - "name": "docker", - "package_manager": { "ubuntu": "custom", "arch": "yay" }, - "install_command": { "ubuntu": "curl -fsSL https://get.docker.com | sudo sh" }, - "priority": "low", - "optional": true, - "description": "Container platform — official Docker Engine for Linux" - }, - { - "name": "claude-code", - "package_manager": { "ubuntu": "curl", "arch": "curl" }, - "install_command": { "ubuntu": "curl -fsSL https://claude.ai/install.sh | bash", "arch": "curl -fsSL https://claude.ai/install.sh | bash" }, - "priority": "medium", - "optional": false, - "description": "Claude Code — terminal-based AI coding assistant" - }, - { - "name": "claude-desktop", - "package_manager": { "ubuntu": "custom", "arch": "yay" }, - "arch_name": "claude-desktop-bin", - "install_command": { "ubuntu": "curl -fsSL https://pkg.claude-desktop-debian.dev/KEY.gpg | sudo gpg --dearmor -o /usr/share/keyrings/claude-desktop.gpg && echo 'deb [signed-by=/usr/share/keyrings/claude-desktop.gpg arch=amd64,arm64] https://pkg.claude-desktop-debian.dev stable main' | sudo tee /etc/apt/sources.list.d/claude-desktop.list && sudo apt update && sudo apt install -y claude-desktop" }, - "priority": "medium", - "optional": false, - "description": "Anthropic Claude desktop app — Ubuntu uses aaddrick/claude-desktop-debian apt repo" - }, - { - "name": "visual-studio-code", - "package_manager": { "ubuntu": "snap", "arch": "yay" }, - "ubuntu_name": "code", - "arch_name": "visual-studio-code-bin", - "install_command": { "ubuntu": "sudo snap install code --classic" }, - "priority": "medium", - "optional": false, - "description": "Code editor with extensions — requires --classic on snap" - }, - { - "name": "firefox", - "package_manager": { "ubuntu": "snap", "arch": "yay" }, - "priority": "medium", - "optional": false, - "description": "Open source web browser" - }, - { - "name": "chromium", - "package_manager": { "ubuntu": "snap", "arch": "yay" }, - "priority": "medium", - "optional": false, - "environment": ["work"], - "description": "Open source Chromium browser — used for work alongside Firefox" - }, - { - "name": "obs-studio", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "priority": "medium", - "optional": false, - "description": "Open source broadcasting and screen recording" - }, - { - "name": "vlc", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "priority": "medium", - "optional": false, - "description": "Open source media player for nearly every format" - }, - { - "name": "discord", - "package_manager": { "ubuntu": "snap", "arch": "yay" }, - "priority": "medium", - "optional": false, - "environment": ["personal"], - "description": "Voice, video, and text chat platform" - }, - { - "name": "bitwarden", - "package_manager": { "ubuntu": "snap", "arch": "yay" }, - "priority": "medium", - "optional": false, - "description": "Open source password manager" - }, - { - "name": "slack", - "package_manager": { "ubuntu": "snap", "arch": "yay" }, - "arch_name": "slack-desktop", - "priority": "medium", - "optional": false, - "environment": ["work"], - "description": "Team messaging and collaboration" - }, - { - "name": "zoom", - "package_manager": { "ubuntu": "snap", "arch": "yay" }, - "ubuntu_name": "zoom-client", - "priority": "medium", - "optional": false, - "environment": ["work"], - "description": "Video conferencing" - }, - { - "name": "teams-for-linux", - "package_manager": { "ubuntu": "snap", "arch": "yay" }, - "priority": "medium", - "optional": false, - "environment": ["work"], - "description": "Unofficial Microsoft Teams client for Linux" - }, - { - "name": "poetry", - "package_manager": { "ubuntu": "pipx", "arch": "yay" }, - "arch_name": "python-poetry", - "priority": "medium", - "optional": false, - "description": "Python project and dependency manager" - }, - { - "name": "codeburn", - "package_manager": { "ubuntu": "pnpm", "arch": "pnpm" }, - "priority": "medium", - "optional": false, - "description": "Track AI coding token usage by task, tool, model, and project" - }, - { - "name": "hf", - "package_manager": { "ubuntu": "pipx", "arch": "yay" }, - "arch_name": "python-huggingface-hub", - "install_command": { "ubuntu": "pipx install huggingface-hub[cli]" }, - "priority": "medium", - "optional": false, - "environment": ["personal"], - "description": "Hugging Face Hub CLI — download, upload, and manage ML models" - }, - { - "name": "git-xet", - "package_manager": { "ubuntu": "custom", "arch": "yay" }, - "arch_name": "git-xet-bin", - "install_command": { "ubuntu": "curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/huggingface/xet-core/refs/heads/main/git_xet/install.sh | sh" }, - "priority": "medium", - "optional": false, - "environment": ["personal"], - "description": "Git LFS plugin using the Xet protocol for Hugging Face Hub" - }, - { - "name": "net-tools", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "priority": "low", - "optional": true, - "description": "Legacy network utilities — ifconfig, netstat, route" - }, - { - "name": "poppler", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "ubuntu_name": "poppler-utils", - "priority": "low", - "optional": true, - "description": "PDF CLI tools — pdftotext, pdfimages, pdfinfo, pdftoppm" - }, - { - "name": "ffmpeg", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "priority": "low", - "optional": true, - "description": "Audio/video processing toolkit" - }, - { - "name": "exiftool", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "ubuntu_name": "libimage-exiftool-perl", - "arch_name": "perl-image-exiftool", - "priority": "low", - "optional": true, - "description": "Read and write metadata in photos, videos, and other files" - }, - { - "name": "tesseract", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "ubuntu_name": "tesseract-ocr", - "priority": "low", - "optional": true, - "description": "OCR engine — extract text from images and scanned documents" - }, - { - "name": "smartmontools", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "priority": "low", - "optional": true, - "description": "Monitor SSD/HDD health via S.M.A.R.T. data" - }, - { - "name": "nmap", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "priority": "low", - "optional": true, - "description": "Network scanner — discover hosts and open ports on the local network" - }, - { - "name": "notion", - "package_manager": { "ubuntu": "snap", "arch": "yay" }, - "ubuntu_name": "notion-snap-reborn", - "arch_name": "notion-app-electron", - "priority": "low", - "optional": true, - "environment": ["personal"], - "description": "Notes and project management — unofficial snap/AUR packages on Linux" - }, - { - "name": "spotify", - "package_manager": { "ubuntu": "snap", "arch": "yay" }, - "priority": "low", - "optional": true, - "environment": ["personal"], - "description": "Music streaming" - }, - { - "name": "steam", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "priority": "low", - "optional": true, - "environment": ["personal"], - "description": "Game library and storefront" - }, - { - "name": "bolt-launcher", - "package_manager": { "arch": "yay" }, - "priority": "low", - "optional": true, - "environment": ["personal"], - "description": "Open-source RuneScape/OSRS launcher for Linux — replaces Jagex Launcher" - }, - { - "name": "octave", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "priority": "none", - "optional": true, - "environment": ["work"], - "description": "MATLAB-compatible numerical computing environment" - }, - { - "name": "llama.cpp", - "package_manager": { "ubuntu": "custom", "arch": "yay" }, - "install_command": { "ubuntu": "See https://github.com/ggml-org/llama.cpp#build" }, - "priority": "none", - "optional": true, - "description": "LLM inference engine — run local models on CPU/GPU" - } -] diff --git a/linux-desktop/setup.sh b/linux-desktop/setup.sh index 92c42cd..c3c1bda 100755 --- a/linux-desktop/setup.sh +++ b/linux-desktop/setup.sh @@ -1,729 +1,12 @@ #!/usr/bin/env bash -# Linux desktop initial setup script (Ubuntu / Arch) -# Usage: bash linux-desktop/setup.sh [--optional] [--work] [--personal] [--dry-run] [--distro ] -# --optional also install low-priority optional packages -# --work also install work-only packages (Slack, Zoom, Teams, Chromium) -# --personal also install personal-only packages (Discord, VLC, Spotify, Steam) -# --dry-run print all commands without executing anything -# --distro force distro family (ubuntu|arch); default: auto-detect from /etc/os-release -# -# Distro support: -# ubuntu → apt + snap + curl installers -# arch → yay (repo + AUR) + curl installers (CachyOS, Arch, Manjaro, EndeavourOS, …) -# -# Environment filtering: -# No flags → installs packages with no environment tag (shared across all) -# --work → adds packages tagged environment=["work"] -# --personal → adds packages tagged environment=["personal"] -# Both flags → installs everything - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PACKAGES_JSON="$SCRIPT_DIR/linux_desktop_packages.json" - -# Other repo setup scripts surfaced at the end of the run. Each entry is -# "|". To advertise a new -# script, just add a line here — only scripts that exist on disk are shown. -RELATED_SCRIPTS=( - "agentic-ai/Claude/install.sh|Claude Code config — symlink settings, hooks, and CLAUDE.md into ~/.claude" - "SSH_and_GPG/create_ssh_key.sh|Generate an SSH key (and add it to GitHub)" - "SSH_and_GPG/create_gpg_key.sh|Generate a GPG key for signed commits" - "linux-desktop/verify.sh|Verify this install (read-only health check)" -) - -DISTRO="" -INCLUDE_OPTIONAL=false -INCLUDE_WORK=false -INCLUDE_PERSONAL=false -DRY_RUN=false - -while [[ $# -gt 0 ]]; do - case "$1" in - --optional) INCLUDE_OPTIONAL=true ;; - --work) INCLUDE_WORK=true ;; - --personal) INCLUDE_PERSONAL=true ;; - --dry-run) DRY_RUN=true ;; - --distro) DISTRO="${2:-}"; shift ;; - *) echo "Unknown argument: $1" >&2 ;; - esac - shift -done - -# ── Distro detection ─────────────────────────────────────────────────────────── -detect_distro() { - [[ -n "$DISTRO" ]] && return - local id="" id_like="" - if [[ -r /etc/os-release ]]; then - # shellcheck disable=SC1091 - id="$(. /etc/os-release && echo "${ID:-}")" - # shellcheck disable=SC1091 - id_like="$(. /etc/os-release && echo "${ID_LIKE:-}")" - fi - case "$id" in - ubuntu|debian|linuxmint|pop) DISTRO="ubuntu" ;; - arch|cachyos|manjaro|endeavouros|garuda|arcolinux) DISTRO="arch" ;; - *) - case "$id_like" in - *debian*|*ubuntu*) DISTRO="ubuntu" ;; - *arch*) DISTRO="arch" ;; - *) - echo "ERROR: Unsupported distro (ID='$id' ID_LIKE='$id_like')." >&2 - echo " Re-run with --distro ubuntu|arch to override." >&2 - exit 1 ;; - esac ;; - esac -} -detect_distro - -if [[ "$DISTRO" != "ubuntu" && "$DISTRO" != "arch" ]]; then - echo "ERROR: --distro must be 'ubuntu' or 'arch' (got '$DISTRO')." >&2 +# Thin shim (UNIFICATION.md Phase 4): the install logic lives in the root +# setup.sh + lib/core.sh + platforms/{ubuntu,arch}.sh, driven by the root +# packages.json. All flags are forwarded; the distro is auto-detected and +# --distro overrides. +# Usage: bash linux-desktop/setup.sh [--optional] [--work] [--personal] +# [--dry-run] [--distro ] +if [[ "$(uname -s)" != "Linux" ]]; then + printf 'error: linux-desktop setup is Linux-only; use the root setup.sh\n' >&2 exit 1 fi -echo "==> Detected distro family: $DISTRO" - -# ── Helpers ─────────────────────────────────────────────────────────────────── - -run() { - if [[ "$DRY_RUN" == true ]]; then - echo " [dry-run] $*" - else - "$@" - fi -} - -run_eval() { - if [[ "$DRY_RUN" == true ]]; then - echo " [dry-run] eval: $1" - else - eval "$1" - fi -} - -# npm supply-chain cooldown: refuse to install package versions younger than -# NPM_MIN_RELEASE_AGE days. Compromised releases of popular packages (e.g. the -# axios RAT, Mar 2026) are typically caught and yanked within hours, so a short -# cooldown blocks them while barely delaying legit updates. See issue #23. -NPM_MIN_RELEASE_AGE=7 - -configure_npm_cooldown() { - local npmrc="$HOME/.npmrc" key="min-release-age" tmp - if [[ "$DRY_RUN" == true ]]; then - echo " [dry-run] set $key=$NPM_MIN_RELEASE_AGE in $npmrc" - return 0 - fi - touch "$npmrc" - # Drop any existing entry, then append the desired value (idempotent, portable). - tmp="$(mktemp)" - grep -v "^${key}=" "$npmrc" > "$tmp" 2>/dev/null || true - printf '%s=%s\n' "$key" "$NPM_MIN_RELEASE_AGE" >> "$tmp" - mv "$tmp" "$npmrc" - echo " ✓ npm $key=$NPM_MIN_RELEASE_AGE (${NPM_MIN_RELEASE_AGE}-day supply-chain cooldown)" -} - -# Enable pnpm (our daily-driver package manager) via corepack — corepack ships -# with Node, so no extra download is gated by the npm cooldown above — and apply -# the same supply-chain cooldown. pnpm measures minimumReleaseAge in MINUTES; it -# also enforces the value strictly only when set explicitly (the built-in 1-day -# default is non-strict), so we set it on purpose. See issue #23. -configure_pnpm() { - local age_minutes=$((NPM_MIN_RELEASE_AGE * 24 * 60)) - if [[ "$DRY_RUN" == true ]]; then - echo " [dry-run] corepack enable && corepack prepare pnpm@latest --activate" - echo " [dry-run] pnpm config set minimumReleaseAge $age_minutes --location=user" - return 0 - fi - if ! command -v corepack &>/dev/null; then - echo " ⚠ corepack not found (needs Node ≥16) — skipping pnpm setup" - return 0 - fi - corepack enable - corepack prepare pnpm@latest --activate - export PNPM_HOME="$HOME/.local/share/pnpm" - export PATH="$PNPM_HOME/bin:$PATH" - if command -v pnpm &>/dev/null; then - pnpm config set minimumReleaseAge "$age_minutes" --location=user - echo " ✓ pnpm enabled, minimumReleaseAge=$age_minutes min (${NPM_MIN_RELEASE_AGE}-day cooldown)" - else - echo " ⚠ pnpm not on PATH after corepack — skipped cooldown config" - fi -} - -# Echo the other repo setup scripts (those present on disk) for discoverability. -print_related_scripts() { - local repo_root entry rel desc shown=false - repo_root="$(cd "$SCRIPT_DIR/.." && pwd)" - for entry in "${RELATED_SCRIPTS[@]}"; do - rel="${entry%%|*}"; desc="${entry#*|}" - [[ -f "$repo_root/$rel" ]] || continue - if [[ "$shown" == false ]]; then - echo " Other setup scripts in this repo:" - shown=true - fi - echo " • $desc" - echo " bash $rel" - done -} - -# Build a jq filter for environment tags. -# Packages with no environment field always install. -# Packages with environment=["work"] only install if --work is passed, etc. -env_filter() { - local work="$INCLUDE_WORK" personal="$INCLUDE_PERSONAL" - cat <_name -pkg_names() { - local manager="$1" priority="$2" - local ef - ef=$(env_filter) - jq -r --arg m "$manager" --arg p "$priority" --arg d "$DISTRO" \ - "[.[] | select( - .package_manager[\$d] == \$m and - .priority == \$p and - $ef - ) | (.[\$d + \"_name\"] // .name)] | join(\" \")" \ - "$PACKAGES_JSON" -} - -# Query packages that have install_command for this distro -custom_installs() { - local priority="$1" - local ef - ef=$(env_filter) - jq -r --arg p "$priority" --arg d "$DISTRO" \ - ".[] | select( - .package_manager[\$d] == \"custom\" and - .priority == \$p and - .install_command[\$d] != null and - $ef - ) | \"\(.name)|\(.install_command[\$d])\"" \ - "$PACKAGES_JSON" -} - -# Query packages installed via curl (have install_command) -curl_installs() { - local priority="$1" - local ef - ef=$(env_filter) - jq -r --arg p "$priority" --arg d "$DISTRO" \ - ".[] | select( - .package_manager[\$d] == \"curl\" and - .priority == \$p and - .install_command[\$d] != null and - $ef - ) | \"\(.name)|\(.install_command[\$d])\"" \ - "$PACKAGES_JSON" -} - -apt_install() { - local priority="$1" - local names - names=$(pkg_names "apt" "$priority") - # shellcheck disable=SC2086 - [[ -n "$names" ]] && run sudo apt install -y $names -} - -# Install repo + AUR packages on Arch via yay (handles both uniformly). -# If the batch fails (e.g. a single AUR build breaks), retry one package at a -# time so one failure can't abort the whole run (and skip shell/config steps). -yay_install() { - local priority="$1" - local names - names=$(pkg_names "yay" "$priority") - [[ -z "$names" ]] && return 0 - # shellcheck disable=SC2086 - if ! run yay -S --needed --noconfirm $names; then - echo " batch install errored — retrying packages individually..." - local pkg - for pkg in $names; do - run yay -S --needed --noconfirm "$pkg" || echo " ✗ failed: $pkg" - done - fi -} - -snap_install() { - local priority="$1" - local ef - ef=$(env_filter) - # Regular snaps - local names - names=$(pkg_names "snap" "$priority") - # Handle snaps with install_command (e.g. --classic) separately - local custom_snaps - custom_snaps=$(jq -r --arg p "$priority" --arg d "$DISTRO" \ - ".[] | select( - .package_manager[\$d] == \"snap\" and - .priority == \$p and - .install_command[\$d] != null and - $ef - ) | \"\(.name)|\(.install_command[\$d])\"" \ - "$PACKAGES_JSON") - - local regular_snaps - regular_snaps=$(jq -r --arg p "$priority" --arg d "$DISTRO" \ - "[.[] | select( - .package_manager[\$d] == \"snap\" and - .priority == \$p and - .install_command[\$d] == null and - $ef - ) | (.[\$d + \"_name\"] // .name)] | join(\" \")" \ - "$PACKAGES_JSON") - - if [[ -n "$regular_snaps" ]]; then - # shellcheck disable=SC2086 - for snap_name in $regular_snaps; do - run sudo snap install "$snap_name" - done - fi - - if [[ -n "$custom_snaps" ]]; then - while IFS='|' read -r name cmd; do - echo " $name (custom snap)..." - run_eval "$cmd" - done <<< "$custom_snaps" - fi -} - -pipx_install() { - local priority="$1" - local ef - ef=$(env_filter) - jq -r --arg p "$priority" --arg d "$DISTRO" \ - ".[] | select( - .package_manager[\$d] == \"pipx\" and - .priority == \$p and - $ef - ) | if .install_command[\$d] != null then .install_command[\$d] else \"pipx install \" + .name end" \ - "$PACKAGES_JSON" | while read -r cmd; do - run_eval "$cmd" - done -} - -pnpm_install() { - local priority="$1" - local ef names - ef=$(env_filter) - names=$(jq -r --arg p "$priority" --arg d "$DISTRO" \ - "[.[] | select( - .package_manager[\$d] == \"pnpm\" and - .priority == \$p and - $ef - ) | .name] | join(\" \")" \ - "$PACKAGES_JSON") - # shellcheck disable=SC2086 - [[ -n "$names" ]] && run pnpm add -g $names -} - -# Bootstrap the AUR helper on Arch (yay lives in the CachyOS/Arch repos, so pacman -# installs it directly — the one place pacman is required instead of yay). -ensure_yay() { - if ! command -v yay &>/dev/null; then - echo "==> Bootstrapping yay (via pacman)..." - run sudo pacman -S --needed --noconfirm base-devel git yay - else - echo "==> yay already installed ($(yay --version 2>/dev/null | head -1))" - fi -} - -# ── Package-manager bootstrap ──────────────────────────────────────────────── -if [[ "$DISTRO" == "ubuntu" ]]; then - echo "==> Updating package list..." - run sudo apt update - - # Bootstrap jq - if ! command -v jq &>/dev/null; then - echo "==> Bootstrapping jq..." - run sudo apt install -y jq - fi - - # External apt repos (idempotent) - NEED_UPDATE=false - - # fastfetch — not in standard Ubuntu repos - if ! apt-cache show fastfetch &>/dev/null 2>&1; then - echo "==> Adding fastfetch PPA..." - run sudo add-apt-repository -y ppa:zhangsongcui3371/fastfetch - NEED_UPDATE=true - fi - - # eza — not in standard Ubuntu repos - if ! apt-cache show eza &>/dev/null 2>&1; then - echo "==> Adding eza apt repo..." - if [[ "$DRY_RUN" != true ]]; then - sudo apt install -y gpg - wget -qO- https://raw.githubusercontent.com/eza-community/eza/main/deb.asc \ - | sudo gpg --dearmor -o /etc/apt/keyrings/gierens.gpg - echo "deb [signed-by=/etc/apt/keyrings/gierens.gpg] http://deb.gierens.de stable main" \ - | sudo tee /etc/apt/sources.list.d/gierens.list > /dev/null - sudo chmod 644 /etc/apt/keyrings/gierens.gpg /etc/apt/sources.list.d/gierens.list - else - echo " [dry-run] add eza community apt repo" - fi - NEED_UPDATE=true - fi - - # gh — GitHub CLI apt repo - if ! command -v gh &>/dev/null; then - echo "==> Adding GitHub CLI apt repo..." - if [[ "$DRY_RUN" != true ]]; then - wget -qO- https://cli.github.com/packages/githubcli-archive-keyring.gpg \ - | sudo gpg --dearmor -o /usr/share/keyrings/githubcli-archive-keyring.gpg - sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg - echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ - | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null - else - echo " [dry-run] add GitHub CLI apt repo" - fi - NEED_UPDATE=true - fi - - [[ "$NEED_UPDATE" == true ]] && run sudo apt update -else - # Arch: yay covers repo + AUR, so no external repos needed. - ensure_yay - if ! command -v jq &>/dev/null; then - echo "==> Bootstrapping jq..." - run sudo pacman -S --needed --noconfirm jq - fi -fi - -# ── High-priority packages ─────────────────────────────────────────────────── -echo "" -if [[ "$DISTRO" == "ubuntu" ]]; then - echo "==> Installing high-priority apt packages..." - apt_install "high" -else - echo "==> Installing high-priority packages (yay)..." - yay_install "high" -fi - -# ── pyenv ──────────────────────────────────────────────────────────────────── -PYTHON_VERSION="3.12.13" - -echo "" -if [[ "$DRY_RUN" == true ]]; then - echo "==> pyenv..." - echo " [dry-run] eval: curl https://pyenv.run | bash" - echo " [dry-run] pyenv install $PYTHON_VERSION && pyenv global $PYTHON_VERSION" -else - if [[ ! -d "$HOME/.pyenv" ]]; then - echo "==> Installing pyenv..." - curl_installs "high" | while IFS='|' read -r name cmd; do - [[ "$name" == "pyenv" ]] && eval "$cmd" - done - else - echo "==> pyenv already installed" - fi - - export PYENV_ROOT="$HOME/.pyenv" - export PATH="$PYENV_ROOT/bin:$PATH" - if command -v pyenv &>/dev/null; then - # Force bash output — pyenv otherwise detects the shell from $SHELL and may - # emit fish/zsh syntax that this bash script's eval rejects (aborts under set -e). - eval "$(pyenv init - bash)" - if ! pyenv versions 2>/dev/null | grep -q "$PYTHON_VERSION"; then - echo "==> Installing pyenv build dependencies..." - if [[ "$DISTRO" == "ubuntu" ]]; then - sudo apt install -y libssl-dev libffi-dev libncurses-dev libreadline-dev \ - libbz2-dev libsqlite3-dev liblzma-dev zlib1g-dev tk-dev - else - # Only install deps not already satisfied. CachyOS ships zlib-ng-compat - # (which provides zlib), so explicitly requesting the zlib package would - # conflict; pacman -T treats it as already satisfied and skips it. - _pyenv_deps=(base-devel openssl zlib xz tk) - _pyenv_need=() - for _dep in "${_pyenv_deps[@]}"; do - pacman -T "$_dep" >/dev/null 2>&1 || _pyenv_need+=("$_dep") - done - if [[ ${#_pyenv_need[@]} -gt 0 ]]; then - sudo pacman -S --needed --noconfirm "${_pyenv_need[@]}" - else - echo " build deps already satisfied" - fi - fi - echo "==> Installing Python $PYTHON_VERSION via pyenv..." - pyenv install "$PYTHON_VERSION" - pyenv global "$PYTHON_VERSION" - else - echo "==> Python $PYTHON_VERSION already installed" - fi - fi -fi - -# ── nvm + Node ─────────────────────────────────────────────────────────────── -echo "" -if [[ "$DRY_RUN" == true ]]; then - echo "==> nvm..." - echo " [dry-run] install nvm via curl" - echo " [dry-run] nvm install lts/* && nvm alias default lts/*" - configure_npm_cooldown - configure_pnpm -else - if [ ! -d "$HOME/.nvm" ]; then - echo "==> Installing nvm..." - curl_installs "high" | while IFS='|' read -r name cmd; do - [[ "$name" == "nvm" ]] && eval "$cmd" - done - else - echo "==> nvm already installed" - fi - - export NVM_DIR="$HOME/.nvm" - [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" - - if command -v nvm &>/dev/null; then - nvm install 'lts/*' - nvm alias default 'lts/*' - nvm use default - fi - - # Apply the supply-chain cooldown before any package install runs below. - configure_npm_cooldown - configure_pnpm -fi - -# ── Medium-priority packages ───────────────────────────────────────────────── -echo "" -if [[ "$DISTRO" == "ubuntu" ]]; then - echo "==> Installing medium-priority apt packages..." - apt_install "medium" - - echo "" - echo "==> Installing snap packages..." - snap_install "medium" - - # bat / fd-find symlinks (Ubuntu installs them as batcat / fdfind) - echo "" - echo "==> Setting up bat and fd symlinks..." - for pair in "batcat bat" "fdfind fd"; do - src="${pair%% *}"; dst="${pair##* }" - if command -v "$src" &>/dev/null && ! command -v "$dst" &>/dev/null; then - run sudo ln -sf "$(command -v "$src")" /usr/local/bin/"$dst" - echo " linked $dst → $src" - else - echo " ✓ $dst" - fi - done -else - echo "==> Installing medium-priority packages (yay)..." - yay_install "medium" - # Arch ships bat and fd under their real names — no symlinks needed. -fi - -# ── Tailscale ──────────────────────────────────────────────────────────────── -echo "" -if [[ "$DISTRO" == "ubuntu" ]]; then - if [[ "$DRY_RUN" == true ]]; then - command -v tailscale &>/dev/null \ - && echo "==> Tailscale already installed" \ - || echo " [dry-run] eval: curl -fsSL https://tailscale.com/install.sh | sudo sh" - elif ! command -v tailscale &>/dev/null; then - echo "==> Installing Tailscale..." - custom_installs "medium" | while IFS='|' read -r name cmd; do - [[ "$name" == "tailscale" ]] && eval "$cmd" - done - else - echo "==> Tailscale already installed ($(tailscale version | head -1))" - fi -else - # Arch: tailscale was installed in the yay medium batch; just enable the daemon. - echo "==> Enabling tailscaled service..." - run sudo systemctl enable --now tailscaled -fi - -# ── claude-code ────────────────────────────────────────────────────────────── -echo "" -if [[ "$DRY_RUN" == true ]]; then - command -v claude &>/dev/null \ - && echo "==> claude-code already installed" \ - || echo " [dry-run] eval: curl -fsSL https://claude.ai/install.sh | bash" -elif ! command -v claude &>/dev/null; then - echo "==> Installing claude-code..." - curl_installs "medium" | while IFS='|' read -r name cmd; do - [[ "$name" == "claude-code" ]] && eval "$cmd" - done -else - echo "==> claude-code already installed" -fi - -# ── pipx packages ──────────────────────────────────────────────────────────── -# (No-op on Arch: poetry/hf are installed via yay there; nothing maps to pipx.) -echo "" -echo "==> Installing pipx packages..." -if command -v pipx &>/dev/null; then - pipx ensurepath - pipx_install "medium" -else - echo " pipx not found — skipping" -fi - -# ── npm packages ───────────────────────────────────────────────────────────── -echo "" -echo "==> Installing pnpm packages..." -export NVM_DIR="$HOME/.nvm" -[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" -export PNPM_HOME="$HOME/.local/share/pnpm" -export PATH="$PNPM_HOME/bin:$PATH" -if command -v pnpm &>/dev/null; then - pnpm_install "medium" -else - echo " pnpm not found — skipping" -fi - -# ── zsh as default shell ───────────────────────────────────────────────────── -# Use the real login shell from /etc/passwd — $SHELL can be an inherited -# interactive shell (e.g. zsh launched over an SSH session whose login shell is fish). -ZSH_BIN="$(command -v zsh 2>/dev/null || true)" -if [[ -n "$ZSH_BIN" ]]; then - CURRENT_SHELL="$(getent passwd "$USER" | cut -d: -f7)" - if [[ "$CURRENT_SHELL" != "$ZSH_BIN" ]]; then - echo "" - echo "==> Setting zsh as default shell (was: ${CURRENT_SHELL:-unknown})..." - run sudo usermod -s "$ZSH_BIN" "$USER" - else - echo "" - echo "==> zsh already the default login shell" - fi -fi - -# ── zshrc ──────────────────────────────────────────────────────────────────── -echo "" -if [[ ! -f "$HOME/.zshrc" ]]; then - run cp "$SCRIPT_DIR/zshrc.example" "$HOME/.zshrc" - echo "==> ~/.zshrc installed from zshrc.example" -elif ! diff -q "$SCRIPT_DIR/zshrc.example" "$HOME/.zshrc" &>/dev/null; then - BACKUP="$HOME/.zshrc.bak.$(date +%Y%m%d_%H%M%S)" - run cp "$HOME/.zshrc" "$BACKUP" - run cp "$SCRIPT_DIR/zshrc.example" "$HOME/.zshrc" - echo "==> ~/.zshrc updated (backup saved to $BACKUP)" -else - echo "==> ~/.zshrc already up to date" -fi - -# ── tmux config ─────────────────────────────────────────────────────────────── -echo "" -if [[ ! -f "$HOME/.tmux.conf" ]]; then - run cp "$SCRIPT_DIR/tmux.conf" "$HOME/.tmux.conf" - echo "==> ~/.tmux.conf installed from tmux.conf" -elif ! diff -q "$SCRIPT_DIR/tmux.conf" "$HOME/.tmux.conf" &>/dev/null; then - TMUX_BACKUP="$HOME/.tmux.conf.bak.$(date +%Y%m%d_%H%M%S)" - run cp "$HOME/.tmux.conf" "$TMUX_BACKUP" - run cp "$SCRIPT_DIR/tmux.conf" "$HOME/.tmux.conf" - echo "==> ~/.tmux.conf updated (backup saved to $TMUX_BACKUP)" -else - echo "==> ~/.tmux.conf already up to date" -fi - -# ── antidote plugins file ──────────────────────────────────────────────────── -echo "" -if [[ ! -f "$HOME/.zsh_plugins.txt" ]]; then - run cp "$SCRIPT_DIR/zsh_plugins.txt" "$HOME/.zsh_plugins.txt" - echo "==> ~/.zsh_plugins.txt installed" -elif ! diff -q "$SCRIPT_DIR/zsh_plugins.txt" "$HOME/.zsh_plugins.txt" &>/dev/null; then - run cp "$SCRIPT_DIR/zsh_plugins.txt" "$HOME/.zsh_plugins.txt" - echo "==> ~/.zsh_plugins.txt updated" -else - echo "==> ~/.zsh_plugins.txt already up to date" -fi - -# ── Powerlevel10k config ───────────────────────────────────────────────────── -echo "" -if [[ ! -f "$HOME/.p10k.zsh" ]]; then - run cp "$SCRIPT_DIR/p10k.zsh.example" "$HOME/.p10k.zsh" - echo "==> ~/.p10k.zsh installed from p10k.zsh.example" -elif ! diff -q "$SCRIPT_DIR/p10k.zsh.example" "$HOME/.p10k.zsh" &>/dev/null; then - P10K_BACKUP="$HOME/.p10k.zsh.bak.$(date +%Y%m%d_%H%M%S)" - run cp "$HOME/.p10k.zsh" "$P10K_BACKUP" - run cp "$SCRIPT_DIR/p10k.zsh.example" "$HOME/.p10k.zsh" - echo "==> ~/.p10k.zsh updated (backup saved to $P10K_BACKUP)" -else - echo "==> ~/.p10k.zsh already up to date" -fi - -# ── Ghostty config ─────────────────────────────────────────────────────────── -GHOSTTY_CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/ghostty" -GHOSTTY_CONFIG="$GHOSTTY_CONFIG_DIR/config" - -echo "" -if [[ "$DRY_RUN" == true ]]; then - echo " [dry-run] mkdir -p $GHOSTTY_CONFIG_DIR" - echo " [dry-run] cp ghostty.config → $GHOSTTY_CONFIG" -else - mkdir -p "$GHOSTTY_CONFIG_DIR" - if [[ -f "$GHOSTTY_CONFIG" ]] && ! diff -q "$SCRIPT_DIR/ghostty.config" "$GHOSTTY_CONFIG" &>/dev/null; then - cp "$GHOSTTY_CONFIG" "${GHOSTTY_CONFIG}.bak" - cp "$SCRIPT_DIR/ghostty.config" "$GHOSTTY_CONFIG" - echo "==> Ghostty config updated (backup saved)" - elif [[ ! -f "$GHOSTTY_CONFIG" ]]; then - cp "$SCRIPT_DIR/ghostty.config" "$GHOSTTY_CONFIG" - echo "==> Ghostty config installed to $GHOSTTY_CONFIG" - else - echo "==> Ghostty config already up to date" - fi -fi - -# ── Optional low-priority packages (--optional flag) ───────────────────────── -if [[ "$INCLUDE_OPTIONAL" == true ]]; then - echo "" - echo "==> Installing optional (low) packages..." - if [[ "$DISTRO" == "ubuntu" ]]; then - apt_install "low" - snap_install "low" - else - yay_install "low" - fi - pipx_install "low" - pnpm_install "low" - - # Docker (optional) - if [[ "$DISTRO" == "ubuntu" ]]; then - if ! command -v docker &>/dev/null; then - echo "==> Installing Docker..." - run_eval "curl -fsSL https://get.docker.com | sudo sh" - run sudo usermod -aG docker "$USER" - echo " Log out and back in for the docker group to take effect." - else - echo "==> Docker already installed ($(docker --version | head -1))" - fi - else - # Arch: docker was installed in the yay low batch; enable the daemon + group. - if command -v docker &>/dev/null || [[ "$DRY_RUN" == true ]]; then - echo "==> Enabling Docker..." - run sudo systemctl enable --now docker.service - run sudo usermod -aG docker "$USER" - echo " Log out and back in for the docker group to take effect." - fi - fi -fi - -# ── Done ───────────────────────────────────────────────────────────────────── -echo "" -if [[ "$DRY_RUN" == true ]]; then - echo "Dry run complete — nothing was installed." -else - echo "================================================================" - echo " Done. A few manual steps remain:" - echo "================================================================" - echo "" - echo " 1. Log out and back in — activates zsh (and docker group if installed)" - echo "" - echo " 2. Open a new Ghostty terminal — antidote will clone plugins" - echo " on first launch (takes ~10 seconds)" - echo "" - echo " 3. Authenticate Tailscale:" - echo " sudo tailscale up" - echo "" - echo " 4. Optional — run these from the repo root as needed:" - echo "" - print_related_scripts - echo "" - echo "================================================================" -fi +exec bash "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." && pwd)/setup.sh" "$@" diff --git a/linux-desktop/verify.sh b/linux-desktop/verify.sh index 0dd6eb0..0a32392 100755 --- a/linux-desktop/verify.sh +++ b/linux-desktop/verify.sh @@ -1,275 +1,11 @@ #!/usr/bin/env bash -# Linux desktop install verification (Ubuntu / Arch) -# Usage: bash linux-desktop/verify.sh [--work] [--personal] [--optional] [--all] [--distro ] -# --work also check work-only packages -# --personal also check personal-only packages -# --optional also check low-priority optional packages -# --all check everything (implies --work --personal --optional + priority "none") -# --distro force distro family (ubuntu|arch); default: auto-detect -# -# Mirrors setup.sh's selection logic, so the packages checked here match what -# `setup.sh` with the same flags would install. Read-only — installs nothing. - -set -uo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PACKAGES_JSON="$SCRIPT_DIR/linux_desktop_packages.json" -DISTRO="" -INCLUDE_OPTIONAL=false -INCLUDE_WORK=false -INCLUDE_PERSONAL=false -INCLUDE_NONE=false - -while [[ $# -gt 0 ]]; do - case "$1" in - --optional) INCLUDE_OPTIONAL=true ;; - --work) INCLUDE_WORK=true ;; - --personal) INCLUDE_PERSONAL=true ;; - --all) INCLUDE_OPTIONAL=true; INCLUDE_WORK=true; INCLUDE_PERSONAL=true; INCLUDE_NONE=true ;; - --distro) DISTRO="${2:-}"; shift ;; - *) echo "Unknown argument: $1" >&2 ;; - esac - shift -done - -# ── Distro detection (same logic as setup.sh) ──────────────────────────────── -detect_distro() { - [[ -n "$DISTRO" ]] && return - local id="" id_like="" - if [[ -r /etc/os-release ]]; then - # shellcheck disable=SC1091 - id="$(. /etc/os-release && echo "${ID:-}")" - # shellcheck disable=SC1091 - id_like="$(. /etc/os-release && echo "${ID_LIKE:-}")" - fi - case "$id" in - ubuntu|debian|linuxmint|pop) DISTRO="ubuntu" ;; - arch|cachyos|manjaro|endeavouros|garuda|arcolinux) DISTRO="arch" ;; - *) - case "$id_like" in - *debian*|*ubuntu*) DISTRO="ubuntu" ;; - *arch*) DISTRO="arch" ;; - *) - echo "ERROR: Unsupported distro (ID='$id' ID_LIKE='$id_like')." >&2 - echo " Re-run with --distro ubuntu|arch to override." >&2 - exit 1 ;; - esac ;; - esac -} -detect_distro - -PASS=0 -FAIL=0 - -check() { - local label="$1" ok="$2" - if [[ "$ok" == true ]]; then - echo " ✅ $label" - PASS=$(( PASS + 1 )) - else - echo " ❌ $label" - FAIL=$(( FAIL + 1 )) - fi -} - -# Build the same environment filter setup.sh uses. -env_filter() { - local work="$INCLUDE_WORK" personal="$INCLUDE_PERSONAL" - cat </dev/null && return 0 - command -v "$name" &>/dev/null && return 0 - return 1 ;; - apt) - dpkg -s "$rname" &>/dev/null && return 0 - command -v "$name" &>/dev/null && return 0 - return 1 ;; - snap) - snap list "$rname" &>/dev/null && return 0 - command -v "$name" &>/dev/null && return 0 - return 1 ;; - pipx) - pipx list --short 2>/dev/null | awk '{print $1}' | grep -qx "$rname" && return 0 - command -v "$name" &>/dev/null && return 0 - return 1 ;; - npm) - npm list -g --depth=0 2>/dev/null | grep -q " ${name}@" && return 0 - command -v "$name" &>/dev/null && return 0 - return 1 ;; - curl|custom) - case "$name" in - pyenv) [[ -d "$HOME/.pyenv" ]] && return 0 ;; - nvm) [[ -s "$HOME/.nvm/nvm.sh" ]] && return 0 ;; - claude-code) command -v claude &>/dev/null && return 0 ;; - *) - command -v "$name" &>/dev/null && return 0 - pacman -Qq "$rname" &>/dev/null && return 0 ;; - esac - return 1 ;; - *) - return 1 ;; - esac -} - -# ── Section printer ─────────────────────────────────────────────────────────── -verify_section() { - local section_label="$1" priority="$2" - local ef rows - ef=$(env_filter) - rows=$(jq -r --arg p "$priority" --arg d "$DISTRO" \ - "[.[] | select( - .package_manager[\$d] != null and - .priority == \$p and - $ef - ) | [.name, .package_manager[\$d], (.[\$d + \"_name\"] // .name), (.optional | tostring)] | @tsv] - | .[]" \ - "$PACKAGES_JSON") - - [[ -z "$rows" ]] && return - - echo "" - echo "── $section_label ──────────────────────────────────────────" - - local name mgr rname opt_flag label - while IFS=$'\t' read -r name mgr rname opt_flag; do - [[ -z "$name" ]] && continue - label="$name ($mgr" - [[ "$rname" != "$name" ]] && label="$label:$rname" - label="$label)" - [[ "$opt_flag" == "true" ]] && label="$label [optional]" - if is_installed "$mgr" "$name" "$rname"; then - check "$label" true - else - check "$label" false - fi - done <<< "$rows" -} - -# ── Runtime environment + config checks ────────────────────────────────────── -verify_extras() { - echo "" - echo "── Runtime environment & configs ────────────────────────────" - - # Login shell - local login_shell zsh_bin - login_shell="$(getent passwd "$USER" | cut -d: -f7)" - zsh_bin="$(command -v zsh 2>/dev/null || true)" - if [[ -n "$zsh_bin" && "$login_shell" == "$zsh_bin" ]]; then - check "login shell is zsh ($login_shell)" true - else - check "login shell is zsh (currently ${login_shell:-unknown})" false - fi - - # pyenv + active Python - export PYENV_ROOT="$HOME/.pyenv" - [[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH" - if command -v pyenv &>/dev/null; then - local py_ver - py_ver=$(pyenv version-name 2>/dev/null || echo "") - if [[ -n "$py_ver" && "$py_ver" != "system" ]]; then - check "python via pyenv ($py_ver)" true - else - check "python via pyenv (no version active — run: pyenv global )" false - fi - else - check "pyenv on PATH" false - fi - - # nvm + Node - export NVM_DIR="$HOME/.nvm" - # shellcheck disable=SC1091 - [[ -s "$NVM_DIR/nvm.sh" ]] && source "$NVM_DIR/nvm.sh" 2>/dev/null - if command -v nvm &>/dev/null; then - local node_ver - node_ver=$(node --version 2>/dev/null || echo "") - if [[ -n "$node_ver" ]]; then - check "node via nvm ($node_ver)" true - else - check "node via nvm (run: nvm install 'lts/*')" false - fi - else - check "nvm loadable" false - fi - - # npm supply-chain cooldown (issue #23) - if grep -q '^min-release-age=' "$HOME/.npmrc" 2>/dev/null; then - local mra - mra=$(grep '^min-release-age=' "$HOME/.npmrc" | tail -1 | cut -d= -f2) - check "npm min-release-age set (${mra}-day cooldown)" true - else - check "npm min-release-age set (run setup.sh)" false - fi - - # pnpm (daily-driver package manager) + its supply-chain cooldown (issue #23) - export PNPM_HOME="$HOME/.local/share/pnpm" - [[ -d "$PNPM_HOME/bin" ]] && export PATH="$PNPM_HOME/bin:$PATH" - if command -v pnpm &>/dev/null; then - check "pnpm ($(pnpm --version 2>/dev/null))" true - local pmra - pmra=$(pnpm config get minimumReleaseAge 2>/dev/null) - if [[ -n "$pmra" && "$pmra" != "undefined" && "$pmra" -gt 0 ]] 2>/dev/null; then - check "pnpm minimumReleaseAge set (${pmra} min)" true - else - check "pnpm minimumReleaseAge set (run setup.sh)" false - fi - else - check "pnpm (run setup.sh — corepack enable)" false - fi - - # Shell config files - [[ -f "$HOME/.zshrc" ]] && check "zshrc present (~/.zshrc)" true || check "zshrc present (~/.zshrc)" false - [[ -f "$HOME/.zsh_plugins.txt" ]] && check "antidote plugin list present (~/.zsh_plugins.txt)" true || check "antidote plugin list present (~/.zsh_plugins.txt)" false - [[ -f "$HOME/.tmux.conf" ]] && check "tmux config present (~/.tmux.conf)" true || check "tmux config present (~/.tmux.conf)" false - - # antidote (Arch AUR package puts it under /usr/share/zsh-antidote) - if [[ -f /usr/share/zsh-antidote/antidote.zsh ]] || command -v antidote &>/dev/null; then - check "antidote available" true - else - check "antidote available" false - fi - - # Ghostty config - local ghostty_cfg="${XDG_CONFIG_HOME:-$HOME/.config}/ghostty/config" - [[ -f "$ghostty_cfg" ]] && check "ghostty config present" true || check "ghostty config present" false - - # Tailscale daemon - if command -v systemctl &>/dev/null; then - if systemctl is-active --quiet tailscaled 2>/dev/null; then - check "tailscaled service active" true - else - check "tailscaled service active (run: sudo systemctl enable --now tailscaled)" false - fi - fi -} - -# ── Run ─────────────────────────────────────────────────────────────────────── -echo "==> Verifying $DISTRO package installs against $PACKAGES_JSON" - -verify_extras -verify_section "High priority" "high" -verify_section "Medium priority" "medium" -[[ "$INCLUDE_OPTIONAL" == true ]] && verify_section "Low priority (optional)" "low" -[[ "$INCLUDE_NONE" == true ]] && verify_section "Priority none (manual only)" "none" - -# ── Summary ───────────────────────────────────────────────────────────────── -echo "" -echo "────────────────────────────────────────────────────────────" -echo " ✅ $PASS ok ❌ $FAIL missing" -echo "" -if [[ $FAIL -gt 0 ]]; then - echo "Run setup.sh (with matching flags) to install missing items, or install manually." -else - echo "All checked items are present." +# Thin shim (UNIFICATION.md Phase 4): the check logic lives in the root +# verify.sh + lib/verify.sh, driven by the root packages.json. All flags are +# forwarded; the distro is auto-detected and --distro overrides. +# Usage: bash linux-desktop/verify.sh [--optional] [--work] [--personal] +# [--all] [--distro ] +if [[ "$(uname -s)" != "Linux" ]]; then + printf 'error: linux-desktop verify is Linux-only; use the root verify.sh\n' >&2 + exit 1 fi +exec bash "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." && pwd)/verify.sh" "$@" diff --git a/linux-server/README.md b/linux-server/README.md index 1341ca3..fb8c347 100644 --- a/linux-server/README.md +++ b/linux-server/README.md @@ -5,11 +5,13 @@ Tested on Ubuntu Server LTS. Most packages work on other Debian-based distros. ## Quick start ```sh -bash linux-server/setup.sh # core packages + zsh setup -bash linux-server/setup.sh --optional # also install samba, smartmontools, nmap, ffmpeg, etc. -bash linux-server/setup.sh --dry-run # preview without installing +bash setup.sh --profile server # core packages + zsh setup +bash setup.sh --profile server --optional # also install samba, smartmontools, nmap, ffmpeg, etc. +bash setup.sh --profile server --dry-run # preview without installing ``` +(Run from the repo root; `linux-server/setup.sh` is a thin shim onto it.) + After running, log out and back in to start using zsh. After setup.sh completes, authenticate Tailscale: @@ -22,12 +24,12 @@ sudo tailscale up ## Files -- [`setup.sh`](setup.sh) — automated install script +- [`setup.sh`](setup.sh) — thin shim onto the root [`setup.sh`](../setup.sh) (server platform) - [`post-install.md`](post-install.md) — step-by-step checklist to follow after setup.sh - [`apt_packages.md`](apt_packages.md) — full package list with descriptions and links -- [`zshrc.example`](zshrc.example) — reference zsh config; copied to `~/.zshrc` by `setup.sh` if none exists -- [`tmux.conf`](tmux.conf) — tmux config with mouse support, vi copy mode, and a status bar; copied to `~/.tmux.conf` by `setup.sh` -- [`linux_server_packages.json`](linux_server_packages.json) — machine-readable package manifest used by `setup.sh` +- [`zshrc.example`](zshrc.example) — server zsh config, deployed to `~/.zshrc` by `setup.sh`; overrides the shared [`dotfiles/zshrc.example`](../dotfiles/zshrc.example) base because the server is headless (no Ghostty/fastfetch/notification hooks) +- [`../dotfiles/tmux.conf`](../dotfiles/tmux.conf) — tmux config with mouse support, vi copy mode, and a status bar (shared across all platforms); copied to `~/.tmux.conf` by `setup.sh` +- [`../packages.json`](../packages.json) — machine-readable package manifest (shared across all platforms) ## Next steps diff --git a/linux-server/apt_packages.md b/linux-server/apt_packages.md index c94d4e9..7ea227e 100644 --- a/linux-server/apt_packages.md +++ b/linux-server/apt_packages.md @@ -7,63 +7,64 @@ Tested on Ubuntu Server LTS. Most packages work on other Debian-based distros. 1. zsh | [apt](https://packages.ubuntu.com/search?keywords=zsh) | [Homepage](https://www.zsh.org/) 1. Z shell — consistent shell across all devices; set as default by `setup.sh` -2. zsh-autosuggestions | [apt](https://packages.ubuntu.com/search?keywords=zsh-autosuggestions) | [GitHub](https://github.com/zsh-users/zsh-autosuggestions) - 1. Fish-style history-based suggestions for zsh — press → to accept -3. zsh-syntax-highlighting | [apt](https://packages.ubuntu.com/search?keywords=zsh-syntax-highlighting) | [GitHub](https://github.com/zsh-users/zsh-syntax-highlighting) - 1. Real-time syntax highlighting in zsh — valid commands appear green, invalid ones red, before pressing Enter +2. zsh-antidote | [apt](https://packages.ubuntu.com/search?keywords=zsh-antidote) | [GitHub](https://github.com/mattmc3/antidote) + 1. Zsh plugin manager — manages zsh-autosuggestions and zsh-syntax-highlighting per `~/.zsh_plugins.txt` + 2. Run `antidote update` to pull latest plugin versions ## CLI Tools -4. fastfetch | [PPA](https://launchpad.net/~zhangsongcui3371/+archive/ubuntu/fastfetch) | [GitHub](https://github.com/fastfetch-cli/fastfetch) +3. fastfetch | [PPA](https://launchpad.net/~zhangsongcui3371/+archive/ubuntu/fastfetch) | [GitHub](https://github.com/fastfetch-cli/fastfetch) 1. Fast, customizable system info display — shown on every interactive shell startup in the example zshrc 2. Not in standard Ubuntu repos; `setup.sh` adds the official PPA automatically -5. fzf | [apt](https://packages.ubuntu.com/search?keywords=fzf) | [GitHub](https://github.com/junegunn/fzf) +4. fzf | [apt](https://packages.ubuntu.com/search?keywords=fzf) | [GitHub](https://github.com/junegunn/fzf) 1. Fuzzy finder — Ctrl+R for history search, Ctrl+T for file search; key bindings sourced in zshrc -6. ripgrep | [apt](https://packages.ubuntu.com/search?keywords=ripgrep) | [GitHub](https://github.com/BurntSushi/ripgrep) +5. ripgrep | [apt](https://packages.ubuntu.com/search?keywords=ripgrep) | [GitHub](https://github.com/BurntSushi/ripgrep) 1. Fast recursive grep replacement — command: `rg` -7. bat | [apt](https://packages.ubuntu.com/search?keywords=bat) | [GitHub](https://github.com/sharkdp/bat) +6. bat | [apt](https://packages.ubuntu.com/search?keywords=bat) | [GitHub](https://github.com/sharkdp/bat) 1. `cat` with syntax highlighting and git integration 2. Ubuntu installs the binary as `batcat` due to a naming conflict; zshrc aliases `bat=batcat` and `setup.sh` creates a symlink -8. fd-find | [apt](https://packages.ubuntu.com/search?keywords=fd-find) | [GitHub](https://github.com/sharkdp/fd) +7. fd-find | [apt](https://packages.ubuntu.com/search?keywords=fd-find) | [GitHub](https://github.com/sharkdp/fd) 1. Fast and user-friendly `find` alternative — command: `fd` 2. Ubuntu installs the binary as `fdfind`; zshrc aliases `fd=fdfind` and `setup.sh` creates a symlink -9. eza | [apt](https://github.com/eza-community/eza/blob/main/INSTALL.md) | [GitHub](https://github.com/eza-community/eza) +8. eza | [apt](https://github.com/eza-community/eza/blob/main/INSTALL.md) | [GitHub](https://github.com/eza-community/eza) 1. Modern `ls` replacement with icons, colors, and git status per file 2. Not in standard Ubuntu repos; `setup.sh` adds the eza community apt repo automatically 3. Used for `ll` and `la` aliases in zshrc: `ll='eza -lAh --git'` ## Development -10. gh | [apt](https://github.com/cli/cli/blob/trunk/docs/install_linux.md) | [GitHub](https://github.com/cli/cli) - 1. GitHub CLI — create PRs, manage issues, clone repos, and trigger Actions from the terminal - 2. `setup.sh` adds the official GitHub CLI apt repo automatically -11. git | [apt](https://packages.ubuntu.com/search?keywords=git) | [Homepage](https://git-scm.com/) +9. gh | [apt](https://github.com/cli/cli/blob/trunk/docs/install_linux.md) | [GitHub](https://github.com/cli/cli) + 1. GitHub CLI — create PRs, manage issues, clone repos, and trigger Actions from the terminal + 2. `setup.sh` adds the official GitHub CLI apt repo automatically +10. git | [apt](https://packages.ubuntu.com/search?keywords=git) | [Homepage](https://git-scm.com/) 1. Version control — usually pre-installed on Ubuntu Server -12. git-lfs | [apt](https://packages.ubuntu.com/search?keywords=git-lfs) | [GitHub](https://github.com/git-lfs/git-lfs) +11. git-lfs | [apt](https://packages.ubuntu.com/search?keywords=git-lfs) | [GitHub](https://github.com/git-lfs/git-lfs) 1. Git extension for versioning large binary files (datasets, model weights, media) -13. gnupg | [apt](https://packages.ubuntu.com/search?keywords=gnupg) | [Homepage](https://gnupg.org/) +12. gnupg | [apt](https://packages.ubuntu.com/search?keywords=gnupg) | [Homepage](https://gnupg.org/) 1. GPG — required for commit signing; used by `SSH_and_GPG/create_gpg_key.sh` ## System Monitoring -14. htop | [apt](https://packages.ubuntu.com/search?keywords=htop) | [GitHub](https://github.com/htop-dev/htop) +13. htop | [apt](https://packages.ubuntu.com/search?keywords=htop) | [GitHub](https://github.com/htop-dev/htop) 1. Interactive process viewer — colorized, scrollable alternative to `top` with per-core CPU, memory, and swap meters -15. nvtop | [apt](https://packages.ubuntu.com/search?keywords=nvtop) | [GitHub](https://github.com/Syllo/nvtop) +14. nvtop | [snap](https://snapcraft.io/nvtop) | [GitHub](https://github.com/Syllo/nvtop) 1. GPU process monitor similar to htop — supports NVIDIA, AMD, Intel, and integrated GPUs + 2. Installed via snap by `setup.sh` (snapd is preinstalled on Ubuntu Server) ## Server Utilities -16. tmux | [apt](https://packages.ubuntu.com/search?keywords=tmux) | [GitHub](https://github.com/tmux/tmux) +15. tmux | [apt](https://packages.ubuntu.com/search?keywords=tmux) | [GitHub](https://github.com/tmux/tmux) 1. Terminal multiplexer — split panes, persist sessions across SSH disconnects, run background processes -17. micro | [apt](https://packages.ubuntu.com/search?keywords=micro) | [GitHub](https://github.com/zyedidia/micro) +16. micro | [snap](https://snapcraft.io/micro) | [GitHub](https://github.com/zyedidia/micro) 1. Modern terminal text editor with mouse support, syntax highlighting, and familiar keybindings (Ctrl+S, Ctrl+C, etc.) -18. ncdu | [apt](https://packages.ubuntu.com/search?keywords=ncdu) | [Homepage](https://dev.yorhel.nl/ncdu) + 2. Installed via snap (`--classic`) by `setup.sh` — noble's apt version is stuck at 2.0.13 +17. ncdu | [apt](https://packages.ubuntu.com/search?keywords=ncdu) | [Homepage](https://dev.yorhel.nl/ncdu) 1. Interactive disk usage analyzer — essential for managing storage on a file server -19. cockpit | [apt](https://packages.ubuntu.com/search?keywords=cockpit) | [Homepage](https://cockpit-project.org/) +18. cockpit | [apt](https://packages.ubuntu.com/search?keywords=cockpit) | [Homepage](https://cockpit-project.org/) 1. Web-based server admin UI — system metrics, journal logs, network config, storage, and service management 2. Enabled automatically via systemd socket activation on install; access at `https://:9090` 3. Login with your Linux username and password -20. sshpass | [apt](https://packages.ubuntu.com/search?keywords=sshpass) | [Homepage](https://sourceforge.net/projects/sshpass/) +19. sshpass | [apt](https://packages.ubuntu.com/search?keywords=sshpass) | [Homepage](https://sourceforge.net/projects/sshpass/) 1. Non-interactive SSH password authentication — used by `SSH_and_GPG/add_remote_host.sh` ## Optional diff --git a/linux-server/linux_server_packages.json b/linux-server/linux_server_packages.json deleted file mode 100644 index f7e33c3..0000000 --- a/linux-server/linux_server_packages.json +++ /dev/null @@ -1,223 +0,0 @@ -[ - { - "name": "zsh", - "package_manager": "apt", - "priority": "high", - "optional": false, - "description": "Z shell — common shell across all devices; installed first so plugins below are available" - }, - { - "name": "zsh-autosuggestions", - "package_manager": "apt", - "priority": "high", - "optional": false, - "description": "Fish-style history-based suggestions for zsh" - }, - { - "name": "zsh-syntax-highlighting", - "package_manager": "apt", - "priority": "high", - "optional": false, - "description": "Real-time syntax highlighting in zsh" - }, - { - "name": "fastfetch", - "package_manager": "apt", - "priority": "medium", - "optional": false, - "description": "Fast system info display — shown on login shell startup; requires fastfetch PPA (setup.sh adds it)" - }, - { - "name": "fzf", - "package_manager": "apt", - "priority": "medium", - "optional": false, - "description": "Fuzzy finder — Ctrl+R history search, Ctrl+T file search; key bindings sourced in zshrc" - }, - { - "name": "ripgrep", - "package_manager": "apt", - "priority": "medium", - "optional": false, - "description": "Fast recursive grep replacement — command: rg" - }, - { - "name": "bat", - "package_manager": "apt", - "priority": "medium", - "optional": false, - "description": "cat with syntax highlighting and git integration — Ubuntu installs binary as batcat; zshrc aliases bat=batcat" - }, - { - "name": "fd-find", - "package_manager": "apt", - "priority": "medium", - "optional": false, - "description": "Fast and user-friendly find alternative — Ubuntu installs binary as fdfind; zshrc aliases fd=fdfind" - }, - { - "name": "eza", - "package_manager": "apt", - "priority": "medium", - "optional": false, - "description": "Modern ls replacement with icons and git status — not in standard Ubuntu repos; setup.sh adds the eza community apt repo" - }, - { - "name": "gh", - "package_manager": "apt", - "priority": "medium", - "optional": false, - "description": "GitHub CLI — PRs, issues, Actions from the terminal; setup.sh adds the GitHub CLI apt repo" - }, - { - "name": "git", - "package_manager": "apt", - "priority": "medium", - "optional": false, - "description": "Version control — usually pre-installed on Ubuntu Server" - }, - { - "name": "git-lfs", - "package_manager": "apt", - "priority": "medium", - "optional": false, - "description": "Git extension for versioning large binary files" - }, - { - "name": "gnupg", - "package_manager": "apt", - "priority": "medium", - "optional": false, - "description": "GPG — required for commit signing" - }, - { - "name": "sshpass", - "package_manager": "apt", - "priority": "medium", - "optional": false, - "description": "Non-interactive SSH password authentication — used by SSH_and_GPG/add_remote_host.sh" - }, - { - "name": "htop", - "package_manager": "apt", - "priority": "medium", - "optional": false, - "description": "Interactive process viewer — colorized alternative to top" - }, - { - "name": "nvtop", - "package_manager": "apt", - "priority": "medium", - "optional": false, - "description": "GPU process monitor — complements htop with GPU visibility" - }, - { - "name": "tmux", - "package_manager": "apt", - "priority": "medium", - "optional": false, - "description": "Terminal multiplexer — split panes, persist sessions across SSH disconnects" - }, - { - "name": "micro", - "package_manager": "apt", - "priority": "medium", - "optional": false, - "description": "Modern terminal text editor with mouse support and familiar keybindings" - }, - { - "name": "ncdu", - "package_manager": "apt", - "priority": "medium", - "optional": false, - "description": "Interactive disk usage analyzer — essential for managing storage on a file server" - }, - { - "name": "cockpit", - "package_manager": "apt", - "priority": "medium", - "optional": false, - "description": "Web-based server admin UI — system metrics, journal logs, network config, storage, and service management; access at https://:9090" - }, - { - "name": "curl", - "package_manager": "apt", - "priority": "medium", - "optional": false, - "description": "HTTP client — usually pre-installed; required by various install scripts" - }, - { - "name": "wget", - "package_manager": "apt", - "priority": "medium", - "optional": false, - "description": "HTTP file downloader — used by setup.sh for apt key imports" - }, - { - "name": "jq", - "package_manager": "apt", - "priority": "medium", - "optional": false, - "description": "JSON processor — required by setup.sh to parse this file" - }, - { - "name": "samba", - "package_manager": "apt", - "priority": "low", - "optional": true, - "description": "SMB/CIFS file sharing — exposes network shares to macOS and Windows; requires manual smb.conf configuration after install" - }, - { - "name": "smartmontools", - "package_manager": "apt", - "priority": "low", - "optional": true, - "description": "Monitor SSD/HDD health via S.M.A.R.T. data — use: smartctl -a /dev/sdX" - }, - { - "name": "nmap", - "package_manager": "apt", - "priority": "low", - "optional": true, - "description": "Network scanner — discover hosts and open ports on the local network" - }, - { - "name": "net-tools", - "package_manager": "apt", - "priority": "low", - "optional": true, - "description": "Legacy network utilities — ifconfig, netstat, route" - }, - { - "name": "ffmpeg", - "package_manager": "apt", - "priority": "low", - "optional": true, - "description": "Audio/video processing toolkit" - }, - { - "name": "libimage-exiftool-perl", - "package_manager": "apt", - "priority": "low", - "optional": true, - "description": "Read and write metadata in photos, videos, and other files — command: exiftool" - }, - { - "name": "docker-ce", - "package_manager": "custom", - "priority": "medium", - "optional": false, - "handled_by_setup": true, - "install_command": "curl -fsSL https://get.docker.com | sudo sh", - "description": "Container platform — official Docker Engine for Linux; convenience script installs CE, CLI, compose plugin, and containerd" - }, - { - "name": "tailscale", - "package_manager": "custom", - "priority": "medium", - "optional": false, - "handled_by_setup": true, - "install_command": "curl -fsSL https://tailscale.com/install.sh | sudo sh", - "description": "Zero-config VPN — access the server from anywhere without port forwarding; installed by setup.sh, run 'sudo tailscale up' to authenticate" - } -] diff --git a/linux-server/setup.sh b/linux-server/setup.sh index 854859c..ba32531 100755 --- a/linux-server/setup.sh +++ b/linux-server/setup.sh @@ -1,346 +1,8 @@ #!/usr/bin/env bash -# Linux server initial setup script +# Thin shim (UNIFICATION.md Phase 4): the install logic lives in the root +# setup.sh + lib/core.sh + platforms/server.sh, driven by the root +# packages.json. All flags are forwarded (see ../setup.sh); the trailing +# --platform pin wins over any forwarded platform flag, so this entrypoint +# always runs the server flow. # Usage: bash linux-server/setup.sh [--optional] [--dry-run] -# --optional also install low-priority optional packages -# --dry-run print all commands without executing anything - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PACKAGES_JSON="$SCRIPT_DIR/linux_server_packages.json" -INCLUDE_OPTIONAL=false -DRY_RUN=false - -for arg in "$@"; do - case "$arg" in - --optional) INCLUDE_OPTIONAL=true ;; - --dry-run) DRY_RUN=true ;; - esac -done - -# ── Helpers ─────────────────────────────────────────────────────────────────── - -run() { - if [[ "$DRY_RUN" == true ]]; then - echo " [dry-run] $*" - else - "$@" - fi -} - - -apt_install() { - local priority="$1" - local names - names=$(jq -r --arg p "$priority" \ - '[.[] | select(.package_manager == "apt" and .priority == $p) | .name] | join(" ")' \ - "$PACKAGES_JSON") - [[ -n "$names" ]] && run sudo apt install -y $names -} - -print_custom_reminders() { - local priority="$1" - local items - items=$(jq -r --arg p "$priority" \ - '.[] | select(.package_manager == "custom" and .priority == $p and (.handled_by_setup != true)) | - " - \(.name)\n \(.description)\n Install: \(.install_command)"' \ - "$PACKAGES_JSON") - [[ -n "$items" ]] && echo "$items" || true -} - - -# ── apt update ─────────────────────────────────────────────────────────────── -echo "==> Updating package list..." -run sudo apt update - -# ── Bootstrap: jq ──────────────────────────────────────────────────────────── -# jq is needed to parse the packages JSON — install it first if missing. -if ! command -v jq &>/dev/null; then - echo "==> Bootstrapping jq..." - run sudo apt install -y jq -fi - -# ── External apt repos (idempotent) ────────────────────────────────────────── -NEED_UPDATE=false - -# fastfetch — not in standard Ubuntu repos -if ! apt-cache show fastfetch &>/dev/null 2>&1; then - echo "==> Adding fastfetch PPA..." - run sudo add-apt-repository -y ppa:zhangsongcui3371/fastfetch - NEED_UPDATE=true -fi - -# eza — not in standard Ubuntu repos -if ! apt-cache show eza &>/dev/null 2>&1; then - echo "==> Adding eza apt repo..." - if [[ "$DRY_RUN" != true ]]; then - sudo apt install -y gpg - wget -qO- https://raw.githubusercontent.com/eza-community/eza/main/deb.asc \ - | sudo gpg --dearmor -o /etc/apt/keyrings/gierens.gpg - echo "deb [signed-by=/etc/apt/keyrings/gierens.gpg] http://deb.gierens.de stable main" \ - | sudo tee /etc/apt/sources.list.d/gierens.list > /dev/null - sudo chmod 644 /etc/apt/keyrings/gierens.gpg /etc/apt/sources.list.d/gierens.list - else - echo " [dry-run] add eza community apt repo" - fi - NEED_UPDATE=true -fi - -# gh — GitHub CLI apt repo -if ! command -v gh &>/dev/null; then - echo "==> Adding GitHub CLI apt repo..." - if [[ "$DRY_RUN" != true ]]; then - wget -qO- https://cli.github.com/packages/githubcli-archive-keyring.gpg \ - | sudo gpg --dearmor -o /usr/share/keyrings/githubcli-archive-keyring.gpg - sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg - echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ - | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null - else - echo " [dry-run] add GitHub CLI apt repo" - fi - NEED_UPDATE=true -fi - -[[ "$NEED_UPDATE" == true ]] && run sudo apt update - -# ── apt packages ────────────────────────────────────────────────────────────── -echo "" -echo "==> Installing high-priority packages..." -apt_install "high" - -echo "" -echo "==> Installing medium-priority packages..." -apt_install "medium" - -if [[ "$INCLUDE_OPTIONAL" == true ]]; then - echo "" - echo "==> Installing optional (low) packages..." - apt_install "low" -fi - -# ── bat / fd-find symlinks ──────────────────────────────────────────────────── -# Ubuntu installs bat as batcat and fd-find as fdfind due to naming conflicts. -# Symlinks let scripts and tools that expect bat/fd work without aliases. -echo "" -echo "==> Setting up bat and fd symlinks..." -for pair in "batcat bat" "fdfind fd"; do - src="${pair%% *}"; dst="${pair##* }" - if command -v "$src" &>/dev/null && ! command -v "$dst" &>/dev/null; then - run sudo ln -sf "$(command -v "$src")" /usr/local/bin/"$dst" - echo " linked $dst → $src" - else - echo " ✓ $dst" - fi -done - -# ── zsh as default shell ────────────────────────────────────────────────────── -ZSH_BIN="$(command -v zsh 2>/dev/null || true)" -if [[ -n "$ZSH_BIN" && "$SHELL" != "$ZSH_BIN" ]]; then - echo "" - echo "==> Setting zsh as default shell..." - run sudo usermod -s "$ZSH_BIN" "$USER" -fi - -# ── zshrc ───────────────────────────────────────────────────────────────────── -echo "" -if [[ ! -f "$HOME/.zshrc" ]]; then - run cp "$SCRIPT_DIR/zshrc.example" "$HOME/.zshrc" - echo "==> ~/.zshrc installed from zshrc.example" -elif ! diff -q "$SCRIPT_DIR/zshrc.example" "$HOME/.zshrc" &>/dev/null; then - BACKUP="$HOME/.zshrc.bak.$(date +%Y%m%d_%H%M%S)" - run cp "$HOME/.zshrc" "$BACKUP" - run cp "$SCRIPT_DIR/zshrc.example" "$HOME/.zshrc" - echo "==> ~/.zshrc updated (backup saved to $BACKUP)" -else - echo "==> ~/.zshrc already up to date" -fi - -# ── tmux config ─────────────────────────────────────────────────────────────── -echo "" -if [[ ! -f "$HOME/.tmux.conf" ]]; then - run cp "$SCRIPT_DIR/tmux.conf" "$HOME/.tmux.conf" - echo "==> ~/.tmux.conf installed from tmux.conf" -elif ! diff -q "$SCRIPT_DIR/tmux.conf" "$HOME/.tmux.conf" &>/dev/null; then - TMUX_BACKUP="$HOME/.tmux.conf.bak.$(date +%Y%m%d_%H%M%S)" - run cp "$HOME/.tmux.conf" "$TMUX_BACKUP" - run cp "$SCRIPT_DIR/tmux.conf" "$HOME/.tmux.conf" - echo "==> ~/.tmux.conf updated (backup saved to $TMUX_BACKUP)" -else - echo "==> ~/.tmux.conf already up to date" -fi - -# ── Tailscale ───────────────────────────────────────────────────────────────── -echo "" -if ! command -v tailscale &>/dev/null; then - echo "==> Installing Tailscale..." - run curl -fsSL https://tailscale.com/install.sh | sudo sh -else - echo "==> Tailscale already installed ($(tailscale version | head -1))" -fi -echo " Run 'sudo tailscale up' to authenticate and connect to your Tailnet." - -# ── Docker ──────────────────────────────────────────────────────────────────── -echo "" -if ! command -v docker &>/dev/null; then - echo "==> Installing Docker..." - run curl -fsSL https://get.docker.com | sudo sh - run sudo usermod -aG docker "$USER" - echo " Docker installed. Log out and back in for the docker group to take effect." -else - echo "==> Docker already installed ($(docker --version | head -1))" -fi - -# ── Cockpit ─────────────────────────────────────────────────────────────────── -echo "" -if systemctl is-active --quiet cockpit.socket 2>/dev/null; then - echo "==> Cockpit already running" -else - echo "==> Enabling cockpit..." - run sudo systemctl enable --now cockpit.socket -fi - -# ── Tailscale web service ───────────────────────────────────────────────────── -echo "" -SERVICE_DST="$HOME/.config/systemd/user/tailscale-web.service" -if [[ ! -f "$SERVICE_DST" ]] || ! diff -q "$SCRIPT_DIR/tailscale-web.service" "$SERVICE_DST" &>/dev/null; then - echo "==> Installing Tailscale web service..." - run mkdir -p "$HOME/.config/systemd/user" - run cp "$SCRIPT_DIR/tailscale-web.service" "$SERVICE_DST" - run systemctl --user daemon-reload - run systemctl --user enable --now tailscale-web -else - echo "==> Tailscale web service already installed" -fi -run loginctl enable-linger "$USER" - -# ── claude-code ─────────────────────────────────────────────────────────────── -echo "" -if ! command -v claude &>/dev/null; then - echo "==> Installing claude-code..." - run curl -fsSL https://claude.ai/install.sh | bash -else - echo "==> claude-code already installed" -fi - -# ── AdGuard: free port 53 ───────────────────────────────────────────────────── -echo "" -RESOLVED_CONF="/etc/systemd/resolved.conf" -if ! grep -q "^DNSStubListener=no" "$RESOLVED_CONF" 2>/dev/null; then - echo "==> Freeing port 53 for AdGuard..." - run sudo sed -i 's/#DNSStubListener=yes/DNSStubListener=no/' "$RESOLVED_CONF" - run sudo systemctl restart systemd-resolved -else - echo "==> Port 53 already free for AdGuard" -fi - -# ── Docker .env files ───────────────────────────────────────────────────────── -echo "" -echo "==> Setting up Docker service .env files..." -for svc in homepage speedtest-tracker filebrowser tailscale-proxy; do - if [[ -f "$SCRIPT_DIR/$svc/.env.example" && ! -f "$SCRIPT_DIR/$svc/.env" ]]; then - run cp "$SCRIPT_DIR/$svc/.env.example" "$SCRIPT_DIR/$svc/.env" - echo " created $svc/.env" - else - echo " ✓ $svc/.env" - fi -done - -# Fill derivable values into homepage/.env (replace placeholders or append if missing) -HOMEPAGE_ENV="$SCRIPT_DIR/homepage/.env" -if [[ -f "$HOMEPAGE_ENV" ]]; then - _fill_env() { - local key="$1" val="$2" - if [[ "$DRY_RUN" == true ]]; then - echo " [dry-run] $key → $val" - return - fi - if grep -q "^$key=" "$HOMEPAGE_ENV"; then - sed -i "s|^$key=.*|$key=$val|" "$HOMEPAGE_ENV" - else - echo "$key=$val" >> "$HOMEPAGE_ENV" - fi - echo " $key → $val" - } - - _fill_env HOSTNAME "$(hostname)" - _fill_env SERVER_IP "$(hostname -I | awk '{print $1}')" - - if command -v tailscale &>/dev/null; then - TS_HOSTNAME=$(tailscale status --json 2>/dev/null | jq -r '.Self.DNSName // empty' | sed 's/\.$//') - if [[ -n "$TS_HOSTNAME" ]]; then - _fill_env TAILSCALE_HOSTNAME "$TS_HOSTNAME" - else - echo " TAILSCALE_HOSTNAME → (skipped — run 'tailscale up' then re-run setup.sh)" - fi - fi -fi - -# Auto-generate APP_KEY for speedtest-tracker -if [[ -f "$SCRIPT_DIR/speedtest-tracker/.env" ]]; then - if ! grep -qE "^APP_KEY=base64:.+" "$SCRIPT_DIR/speedtest-tracker/.env" 2>/dev/null; then - if [[ "$DRY_RUN" == true ]]; then - echo " [dry-run] generate APP_KEY in speedtest-tracker/.env" - else - APP_KEY="base64:$(openssl rand -base64 32)" - sed -i "s|^APP_KEY=.*|APP_KEY=$APP_KEY|" "$SCRIPT_DIR/speedtest-tracker/.env" - echo " auto-generated APP_KEY for speedtest-tracker" - fi - fi -fi - -# ── Docker services ─────────────────────────────────────────────────────────── -echo "" -echo "==> Starting Docker services..." -for svc in homepage portainer glances speedtest-tracker filebrowser watchtower \ - uptime-kuma nginx-proxy-manager ntfy syncthing adguard tailscale-proxy; do - svc_dir="$SCRIPT_DIR/$svc" - if [[ -d "$svc_dir" && -f "$svc_dir/docker-compose.yml" ]]; then - echo " $svc..." - if ! run sudo docker compose -f "$svc_dir/docker-compose.yml" up -d; then - echo " Warning: $svc failed to start" - fi - fi -done - -# ── Manual install reminders ────────────────────────────────────────────────── -CUSTOM_REMINDERS="$(print_custom_reminders "medium")" -[[ "$INCLUDE_OPTIONAL" == true ]] && CUSTOM_REMINDERS+="$(print_custom_reminders "low")" -if [[ -n "$CUSTOM_REMINDERS" ]]; then - echo "" - echo "Install these manually (require their own repo setup):" - echo "$CUSTOM_REMINDERS" -fi - -# ── Done ───────────────────────────────────────────────────────────────────── -echo "" -if [[ "$DRY_RUN" == true ]]; then - echo "Dry run complete — nothing was installed." -else - echo "================================================================" - echo " Done. A few manual steps remain:" - echo "================================================================" - echo "" - echo " 1. Log out and back in — activates zsh and 'docker' without sudo" - echo "" - echo " 2. Authenticate Tailscale:" - echo " sudo tailscale up" - echo " sudo tailscale set --operator=\$USER" - echo "" - echo " 3. SSH / GPG keys:" - echo " bash SSH_and_GPG/create_ssh_key.sh" - echo " bash SSH_and_GPG/create_gpg_key.sh" - echo "" - echo " 4. Authenticate Tailscale, then re-run setup.sh to auto-fill TAILSCALE_HOSTNAME:" - echo " sudo tailscale up" - echo " bash linux-server/setup.sh" - echo " Then fill in any remaining values in linux-server/homepage/.env and restart:" - echo " cd linux-server/homepage && docker compose restart" - echo "" - echo "================================================================" - echo "" - if command -v bat &>/dev/null; then - bat "$SCRIPT_DIR/post-install.md" - else - cat "$SCRIPT_DIR/post-install.md" - fi -fi +exec bash "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." && pwd)/setup.sh" "$@" --platform server diff --git a/linux-server/tmux.conf b/linux-server/tmux.conf deleted file mode 100644 index f3ce4d7..0000000 --- a/linux-server/tmux.conf +++ /dev/null @@ -1,38 +0,0 @@ -# ── General ─────────────────────────────────────────────────────────────────── -# Prefix left at the default C-b. -set -g default-terminal "tmux-256color" -set -ag terminal-overrides ",xterm-256color:RGB" -set -g mouse on -set -g history-limit 10000 -set -s escape-time 10 -set -g base-index 1 -setw -g pane-base-index 1 -set -g renumber-windows on - -# ── Key bindings ────────────────────────────────────────────────────────────── -bind | split-window -h -c "#{pane_current_path}" -bind - split-window -v -c "#{pane_current_path}" -unbind '"' -unbind % - -bind r source-file ~/.tmux.conf \; display "Config reloaded" - -bind h select-pane -L -bind j select-pane -D -bind k select-pane -U -bind l select-pane -R - -# ── Copy mode (vi) ──────────────────────────────────────────────────────────── -setw -g mode-keys vi -bind-key -T copy-mode-vi v send-keys -X begin-selection -bind-key -T copy-mode-vi y send-keys -X copy-selection-and-cancel - -# ── Status bar ──────────────────────────────────────────────────────────────── -set -g status-position top -set -g status-style "bg=#1e2030,fg=#c8d3f5" -set -g status-left-length 30 -set -g status-right-length 60 -set -g status-left "#[bold] #H #[nobold]" -set -g status-right "#[fg=#82aaff]%Y-%m-%d #[fg=#c8d3f5]%H:%M" -set -g window-status-format " #I:#W " -set -g window-status-current-format "#[bg=#82aaff,fg=#1e2030] #I:#W " diff --git a/linux-server/zsh_plugins.txt b/linux-server/zsh_plugins.txt new file mode 100644 index 0000000..1317a58 --- /dev/null +++ b/linux-server/zsh_plugins.txt @@ -0,0 +1,14 @@ +# ═══════════════════════════════════════════════════════════════ +# ~/.zsh_plugins.txt — antidote plugin list (server override) +# ═══════════════════════════════════════════════════════════════ +# antidote clones these from GitHub and generates a static +# source file. Run `antidote update` to pull latest versions. +# Headless subset of dotfiles/zsh_plugins.txt — no zsh-notify +# (desktop notifications), no fpath completions (compinit runs +# before antidote load in the server zshrc). + +# Fish-style autosuggestions +zsh-users/zsh-autosuggestions + +# Syntax highlighting (load near end — must see all widgets) +zsh-users/zsh-syntax-highlighting diff --git a/linux-server/zshrc.example b/linux-server/zshrc.example index 6f1bb72..8519258 100644 --- a/linux-server/zshrc.example +++ b/linux-server/zshrc.example @@ -1,8 +1,9 @@ # ═══════════════════════════════════════════════════════════════ # ~/.zshrc — Linux server zsh config (Ubuntu Server) # ═══════════════════════════════════════════════════════════════ -# Requires: zsh, zsh-autosuggestions, zsh-syntax-highlighting, -# fastfetch, fzf, bat (batcat), fd-find (fdfind), eza +# Requires: zsh, zsh-antidote, fastfetch, fzf, +# bat (batcat), fd-find (fdfind), eza +# Plugins managed by antidote — see ~/.zsh_plugins.txt # Install: bash linux-server/setup.sh @@ -69,6 +70,7 @@ alias grep='grep --color=auto' alias df='df -h' alias du='du -sh' alias reload='source ~/.zshrc' +command -v fastfetch &>/dev/null && alias cf='clear && fastfetch' # Ubuntu binary name shims alias bat='batcat' @@ -106,9 +108,10 @@ export LC_ALL='en_US.UTF-8' # ═══════════════════════════════════════════════════════════════ -# PLUGINS (apt install zsh-autosuggestions zsh-syntax-highlighting) +# ANTIDOTE (apt install zsh-antidote) # ═══════════════════════════════════════════════════════════════ -[[ -f /usr/share/zsh-autosuggestions/zsh-autosuggestions.zsh ]] && \ - source /usr/share/zsh-autosuggestions/zsh-autosuggestions.zsh -[[ -f /usr/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh ]] && \ - source /usr/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh +# antidote sources all plugins listed in ~/.zsh_plugins.txt +if [[ -f /usr/share/zsh-antidote/antidote.zsh && -f "$HOME/.zsh_plugins.txt" ]]; then + source /usr/share/zsh-antidote/antidote.zsh + antidote load +fi diff --git a/macOS/README.md b/macOS/README.md index 7906bc0..be9b8f4 100644 --- a/macOS/README.md +++ b/macOS/README.md @@ -4,7 +4,7 @@ 1. Homebrew | [Download](https://brew.sh/) 1. The missing package manager for macOS — see [brew_packages.md](brew_packages.md) for the full list of useful formulae - 2. Packages: `gh`, `hf`, `git-lfs`, `git-xet`, `fastfetch`, `pipx`, `pyenv`, `sshpass`, `gnupg`, `zsh-autosuggestions`, `zsh-syntax-highlighting`, `htop`, `nvtop`, `mactop`, `micro`, `tmux`, `llama.cpp`, and more + 2. Packages: `gh`, `hf`, `git-lfs`, `git-xet`, `fastfetch`, `pipx`, `pyenv`, `sshpass`, `gnupg`, `antidote`, `htop`, `nvtop`, `mactop`, `micro`, `tmux`, `llama.cpp`, and more (zsh plugins are managed by antidote — see [dotfiles/zsh_plugins.txt](../dotfiles/zsh_plugins.txt)) 2. pipx | [brew](https://formulae.brew.sh/formula/pipx) 1. Installs Python CLI tools in isolated virtual environments — see [pipx_packages.md](pipx_packages.md) for the full list 2. Packages: `poetry`, and more @@ -90,7 +90,7 @@ ## Work -Install with `zsh setup.sh --work`. +Install with `bash setup.sh --work` from the repo root (`macOS/setup.sh` is a thin shim onto it). 1. Slack | [brew](https://formulae.brew.sh/cask/slack) 1. Team messaging and collaboration diff --git a/macOS/brew_packages.md b/macOS/brew_packages.md index bd30727..cf9244f 100644 --- a/macOS/brew_packages.md +++ b/macOS/brew_packages.md @@ -14,7 +14,7 @@ Install via `brew install ` unless noted otherwise. 1. Fast, customizable system info display with full Apple Silicon chip and sensor support; shows on Ghostty startup in the example zshrc 6. pipx | [brew](https://formulae.brew.sh/formula/pipx) | [GitHub](https://github.com/pypa/pipx) 1. Installs Python CLI tools in isolated virtual environments — avoids polluting the global Python install - 2. **macOS Tahoe:** brew's Python bottles link against the system `libexpat`, which is missing symbols on Tahoe — causing `ensurepip` to fail. Use pyenv + expat instead (handled automatically by [setup.sh](setup.sh)): + 2. **macOS Tahoe:** brew's Python bottles link against the system `libexpat`, which is missing symbols on Tahoe — causing `ensurepip` to fail. Use pyenv + expat instead (handled automatically by [platforms/macos.sh](../platforms/macos.sh) via the root setup.sh): ```sh brew install pyenv expat pyenv install 3.12.13 && pyenv global 3.12.13 @@ -27,44 +27,42 @@ Install via `brew install ` unless noted otherwise. 1. PDF rendering library that ships useful CLI tools: `pdftotext`, `pdfimages`, `pdfinfo`, `pdftoppm` 9. sshpass | [brew](https://formulae.brew.sh/formula/sshpass) 1. Non-interactive SSH password authentication — used by `add_remote_host.sh` to automate host setup -10. zsh-autosuggestions | [brew](https://formulae.brew.sh/formula/zsh-autosuggestions) | [GitHub](https://github.com/zsh-users/zsh-autosuggestions) - 1. Fish-style history-based suggestions for zsh — press → to accept -11. zsh-syntax-highlighting | [brew](https://formulae.brew.sh/formula/zsh-syntax-highlighting) | [GitHub](https://github.com/zsh-users/zsh-syntax-highlighting) - 1. Real-time syntax highlighting in zsh — valid commands appear green, invalid ones red, before pressing Enter -12. mongodb-community | [brew tap](https://www.mongodb.com/docs/manual/tutorial/install-mongodb-on-os-x/) | [Download](https://www.mongodb.com/try/download/community) +10. antidote | [brew](https://formulae.brew.sh/formula/antidote) | [GitHub](https://github.com/mattmc3/antidote) + 1. Zsh plugin manager — loads autosuggestions, syntax highlighting, substring search, abbreviations, and zsh-notify from [dotfiles/zsh_plugins.txt](../dotfiles/zsh_plugins.txt) (replaces the standalone zsh-autosuggestions / zsh-syntax-highlighting formulae) +11. mongodb-community | [brew tap](https://www.mongodb.com/docs/manual/tutorial/install-mongodb-on-os-x/) | [Download](https://www.mongodb.com/try/download/community) 1. MongoDB Community Server; installing via the `mongodb/brew` tap also provides `mongosh` and `mongodb-database-tools` 2. Install: `brew tap mongodb/brew && brew install mongodb-community` -13. htop | [brew](https://formulae.brew.sh/formula/htop) | [GitHub](https://github.com/htop-dev/htop) +12. htop | [brew](https://formulae.brew.sh/formula/htop) | [GitHub](https://github.com/htop-dev/htop) 1. Interactive process viewer — colorized, scrollable alternative to `top` with per-core CPU, memory, and swap meters -14. nvtop | [brew](https://formulae.brew.sh/formula/nvtop) | [GitHub](https://github.com/Syllo/nvtop) +13. nvtop | [brew](https://formulae.brew.sh/formula/nvtop) | [GitHub](https://github.com/Syllo/nvtop) 1. GPU process monitor similar to htop — supports NVIDIA, AMD, Intel, and Apple Silicon GPUs -15. asitop | [brew](https://formulae.brew.sh/formula/asitop) | [GitHub](https://github.com/tlkh/asitop) +14. asitop | [brew](https://formulae.brew.sh/formula/asitop) | [GitHub](https://github.com/tlkh/asitop) 1. Performance monitoring TUI for Apple Silicon — shows CPU, GPU, ANE, memory bandwidth, and power usage in real time; requires `sudo` -16. llama.cpp | [brew](https://formulae.brew.sh/formula/llama.cpp) | [GitHub](https://github.com/ggml-org/llama.cpp) +15. llama.cpp | [brew](https://formulae.brew.sh/formula/llama.cpp) | [GitHub](https://github.com/ggml-org/llama.cpp) 1. LLM inference engine in C/C++ — run local models (GGUF format) on CPU and Apple Silicon GPU via Metal -17. mactop | [brew](https://formulae.brew.sh/formula/mactop) | [GitHub](https://github.com/metaspartan/mactop) +16. mactop | [brew](https://formulae.brew.sh/formula/mactop) | [GitHub](https://github.com/metaspartan/mactop) 1. Apple Silicon system monitor — shows CPU/GPU/ANE/memory/power usage in a real-time TUI; requires `sudo` -18. micro | [brew](https://formulae.brew.sh/formula/micro) | [GitHub](https://github.com/zyedidia/micro) +17. micro | [brew](https://formulae.brew.sh/formula/micro) | [GitHub](https://github.com/zyedidia/micro) 1. Modern terminal text editor with mouse support, syntax highlighting, and familiar keybindings (Ctrl+S, Ctrl+C, etc.) -19. eza | [brew](https://formulae.brew.sh/formula/eza) | [GitHub](https://github.com/eza-community/eza) +18. eza | [brew](https://formulae.brew.sh/formula/eza) | [GitHub](https://github.com/eza-community/eza) 1. Modern `ls` replacement — color output, icons, git-aware status column, and tree view; used as `ll`/`la` aliases in the example zshrc -20. tmux | [brew](https://formulae.brew.sh/formula/tmux) | [GitHub](https://github.com/tmux/tmux) +19. tmux | [brew](https://formulae.brew.sh/formula/tmux) | [GitHub](https://github.com/tmux/tmux) 1. Terminal multiplexer — split panes, persist sessions across disconnects, and run background processes -21. ffmpeg | [brew](https://formulae.brew.sh/formula/ffmpeg) | [Homepage](https://ffmpeg.org/) +20. ffmpeg | [brew](https://formulae.brew.sh/formula/ffmpeg) | [Homepage](https://ffmpeg.org/) 1. Audio/video processing toolkit — convert, trim, encode, stream, and extract frames from media files -22. exiftool | [brew](https://formulae.brew.sh/formula/exiftool) | [Homepage](https://exiftool.org/) +21. exiftool | [brew](https://formulae.brew.sh/formula/exiftool) | [Homepage](https://exiftool.org/) 1. Read and write metadata (EXIF, IPTC, XMP) in photos, videos, and other files -23. tesseract | [brew](https://formulae.brew.sh/formula/tesseract) | [GitHub](https://github.com/tesseract-ocr/tesseract) +22. tesseract | [brew](https://formulae.brew.sh/formula/tesseract) | [GitHub](https://github.com/tesseract-ocr/tesseract) 1. OCR engine — extract text from images and scanned documents via CLI or library -24. smartmontools | [brew](https://formulae.brew.sh/formula/smartmontools) | [Homepage](https://www.smartmontools.org/) +23. smartmontools | [brew](https://formulae.brew.sh/formula/smartmontools) | [Homepage](https://www.smartmontools.org/) 1. Monitor SSD/HDD health via S.M.A.R.T. data; `smartctl -a /dev/disk0` shows drive status -25. octave | [brew](https://formulae.brew.sh/formula/octave) | [Homepage](https://octave.org/) +24. octave | [brew](https://formulae.brew.sh/formula/octave) | [Homepage](https://octave.org/) 1. MATLAB-compatible numerical computing environment — matrix operations, plotting, and scripting -26. qemu | [brew](https://formulae.brew.sh/formula/qemu) | [Homepage](https://www.qemu.org/) +25. qemu | [brew](https://formulae.brew.sh/formula/qemu) | [Homepage](https://www.qemu.org/) 1. Machine emulator and virtualizer — run x86, ARM, and other architectures on Apple Silicon -27. gogcli | [brew](https://formulae.brew.sh/formula/gogcli) | [Homepage](https://gogcli.sh/) +26. gogcli | [brew](https://formulae.brew.sh/formula/gogcli) | [Homepage](https://gogcli.sh/) 1. Google Workspace CLI — Gmail, Calendar, Drive, Docs, Sheets, Slides, and more in the terminal; built for scripts, CI, and coding agents -28. powerlevel10k | [brew](https://formulae.brew.sh/formula/powerlevel10k) | [GitHub](https://github.com/romkatv/powerlevel10k) +27. powerlevel10k | [brew](https://formulae.brew.sh/formula/powerlevel10k) | [GitHub](https://github.com/romkatv/powerlevel10k) 1. Feature-rich zsh prompt theme — git status, command duration, exit code, instant prompt; run `p10k configure` once after install to set up `~/.p10k.zsh` ## Casks diff --git a/macOS/ghostty.config b/macOS/ghostty.config deleted file mode 100644 index de7ade1..0000000 --- a/macOS/ghostty.config +++ /dev/null @@ -1,6 +0,0 @@ -# Ghostty Config - -theme = Dark Pastel -font-family = MesloLGS Nerd Font Mono -shell-integration = zsh -term = xterm-256color diff --git a/macOS/ghostty_config.md b/macOS/ghostty_config.md index f6ace54..85572c5 100644 --- a/macOS/ghostty_config.md +++ b/macOS/ghostty_config.md @@ -2,7 +2,7 @@ Source: [ghostty.org/docs/config](https://ghostty.org/docs/config) -The raw config file is [ghostty.config](ghostty.config) — `setup.sh` copies it to the XDG path automatically. +The raw config file is [dotfiles/ghostty.config](../dotfiles/ghostty.config) (shared across macOS and Linux) — `setup.sh` copies it to the XDG path automatically. ## File Locations @@ -25,5 +25,7 @@ Both locations are supported on macOS. Since the XDG path works cross-platform a ``` theme = Dark Pastel +font-family = MesloLGS Nerd Font Mono shell-integration = zsh +term = xterm-256color ``` diff --git a/macOS/macOS_packages.json b/macOS/macOS_packages.json deleted file mode 100644 index 99a9a4d..0000000 --- a/macOS/macOS_packages.json +++ /dev/null @@ -1,420 +0,0 @@ -[ - { - "name": "jq", - "package_manager": "brew", - "priority": "low", - "optional": true, - "description": "JSON processor — system-provided on macOS; brew version only needed if the system one is missing or outdated" - }, - { - "name": "expat", - "package_manager": "brew", - "priority": "high", - "optional": false, - "description": "XML parsing library — required for pyenv to compile Python correctly on macOS Tahoe" - }, - { - "name": "pyenv", - "package_manager": "brew", - "priority": "high", - "optional": false, - "description": "Python version manager — compiles from source to avoid broken Homebrew bottles on macOS Tahoe" - }, - { - "name": "nvm", - "package_manager": "curl", - "priority": "high", - "optional": false, - "install_command": "curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash", - "description": "Node version manager — installed via curl per nvm recommendation; brew install causes PATH issues" - }, - { - "name": "pipx", - "package_manager": "brew", - "priority": "high", - "optional": false, - "description": "Installs Python CLI tools in isolated virtual environments" - }, - { - "name": "fastfetch", - "package_manager": "brew", - "priority": "medium", - "optional": false, - "description": "Fast system info display — shown on Ghostty startup" - }, - { - "name": "gh", - "package_manager": "brew", - "priority": "medium", - "optional": false, - "description": "GitHub CLI — PRs, issues, Actions from the terminal" - }, - { - "name": "git-lfs", - "package_manager": "brew", - "priority": "medium", - "optional": false, - "description": "Git extension for versioning large binary files" - }, - { - "name": "git-xet", - "package_manager": "brew", - "priority": "medium", - "optional": false, - "description": "Git LFS plugin using the Xet protocol for Hugging Face Hub" - }, - { - "name": "gnupg", - "package_manager": "brew", - "priority": "medium", - "optional": false, - "description": "GPG — required for commit signing" - }, - { - "name": "hf", - "package_manager": "brew", - "priority": "medium", - "optional": false, - "description": "Hugging Face Hub CLI — download, upload, and manage ML models" - }, - { - "name": "htop", - "package_manager": "brew", - "priority": "medium", - "optional": false, - "description": "Interactive process viewer — colorized alternative to top" - }, - { - "name": "llama.cpp", - "package_manager": "brew", - "priority": "medium", - "optional": false, - "description": "LLM inference engine — run local models on CPU and Apple Silicon GPU" - }, - { - "name": "mactop", - "package_manager": "brew", - "priority": "medium", - "optional": false, - "description": "Apple Silicon system monitor — CPU, GPU, ANE, memory, power in real-time TUI" - }, - { - "name": "micro", - "package_manager": "brew", - "priority": "medium", - "optional": false, - "description": "Modern terminal text editor with mouse support and familiar keybindings" - }, - { - "name": "nvtop", - "package_manager": "brew", - "priority": "medium", - "optional": false, - "description": "GPU process monitor — complements htop with GPU visibility" - }, - { - "name": "sshpass", - "package_manager": "brew", - "priority": "medium", - "optional": false, - "description": "Non-interactive SSH password authentication — used by add_remote_host.sh" - }, - { - "name": "tmux", - "package_manager": "brew", - "priority": "medium", - "optional": false, - "description": "Terminal multiplexer — split panes, persist sessions across disconnects" - }, - { - "name": "eza", - "package_manager": "brew", - "priority": "medium", - "optional": false, - "description": "Modern ls replacement — colors, icons, git status, tree view" - }, - { - "name": "zsh-autosuggestions", - "package_manager": "brew", - "priority": "medium", - "optional": false, - "description": "Fish-style history-based suggestions for zsh" - }, - { - "name": "zsh-syntax-highlighting", - "package_manager": "brew", - "priority": "medium", - "optional": false, - "description": "Real-time syntax highlighting in zsh" - }, - { - "name": "ghostty", - "package_manager": "brew-cask", - "priority": "medium", - "optional": false, - "description": "GPU-accelerated terminal emulator with native macOS feel" - }, - { - "name": "claude", - "package_manager": "brew-cask", - "priority": "medium", - "optional": false, - "description": "Anthropic Claude desktop app" - }, - { - "name": "claude-code", - "package_manager": "brew-cask", - "priority": "medium", - "optional": false, - "description": "Claude Code — terminal-based AI coding assistant" - }, - { - "name": "firefox", - "package_manager": "brew-cask", - "priority": "medium", - "optional": false, - "description": "Open source web browser" - }, - { - "name": "mac-mouse-fix", - "package_manager": "brew-cask", - "priority": "medium", - "optional": false, - "description": "Decouples mouse scroll speed from trackpad settings" - }, - { - "name": "rectangle", - "package_manager": "brew-cask", - "priority": "medium", - "optional": false, - "description": "Open source window management" - }, - { - "name": "visual-studio-code", - "package_manager": "brew-cask", - "priority": "medium", - "optional": false, - "description": "Code editor with extensions" - }, - { - "name": "caffeine", - "package_manager": "brew-cask", - "priority": "medium", - "optional": false, - "description": "Simple menubar toggle to keep Mac awake" - }, - { - "name": "Spark Mail – AI Email Inbox", - "package_manager": "app-store", - "priority": "medium", - "optional": false, - "description": "Smart email client with AI features" - }, - { - "name": "notion", - "package_manager": "brew-cask", - "priority": "medium", - "optional": false, - "description": "Notes and project management" - }, - { - "name": "obs", - "package_manager": "brew-cask", - "priority": "medium", - "optional": false, - "description": "Open source broadcasting and screen recording" - }, - { - "name": "vlc", - "package_manager": "brew-cask", - "priority": "medium", - "optional": false, - "description": "Open source media player for nearly every format" - }, - { - "name": "google-drive", - "package_manager": "brew-cask", - "priority": "medium", - "optional": false, - "description": "Google Drive desktop client" - }, - { - "name": "amazon-photos", - "package_manager": "brew-cask", - "priority": "medium", - "optional": false, - "description": "Unlimited full-resolution photo backup for Amazon Prime members" - }, - { - "name": "discord", - "package_manager": "brew-cask", - "priority": "medium", - "optional": false, - "description": "Voice, video, and text chat platform" - }, - { - "name": "poetry", - "package_manager": "pipx", - "priority": "medium", - "optional": false, - "description": "Python project and dependency manager" - }, - { - "name": "slack", - "package_manager": "brew-cask", - "priority": "medium", - "optional": false, - "work": true, - "description": "Team messaging and collaboration" - }, - { - "name": "zoom", - "package_manager": "brew-cask", - "priority": "medium", - "optional": false, - "work": true, - "description": "Video conferencing" - }, - { - "name": "microsoft-outlook", - "package_manager": "brew-cask", - "priority": "medium", - "optional": false, - "work": true, - "description": "Email client for Microsoft 365" - }, - { - "name": "codeburn", - "package_manager": "pnpm", - "priority": "medium", - "optional": false, - "description": "Track AI coding token usage by task, tool, model, and project" - }, - { - "name": "Bitwarden", - "package_manager": "app-store", - "priority": "medium", - "optional": false, - "description": "Open source password manager — App Store version required for browser integration" - }, - { - "name": "Tailscale", - "package_manager": "app-store", - "priority": "medium", - "optional": false, - "description": "Zero-config VPN — App Store version required for network extension entitlements" - }, - { - "name": "Photomator", - "package_manager": "app-store", - "priority": "medium", - "optional": false, - "description": "Photo editor — free version with pay-once upgrade" - }, - { - "name": "poppler", - "package_manager": "brew", - "priority": "low", - "optional": true, - "description": "PDF CLI tools — pdftotext, pdfimages, pdfinfo, pdftoppm" - }, - { - "name": "ffmpeg", - "package_manager": "brew", - "priority": "low", - "optional": true, - "description": "Audio/video processing toolkit" - }, - { - "name": "exiftool", - "package_manager": "brew", - "priority": "low", - "optional": true, - "description": "Read and write metadata in photos, videos, and other files" - }, - { - "name": "tesseract", - "package_manager": "brew", - "priority": "low", - "optional": true, - "description": "OCR engine — extract text from images and scanned documents" - }, - { - "name": "smartmontools", - "package_manager": "brew", - "priority": "low", - "optional": true, - "description": "Monitor SSD/HDD health via S.M.A.R.T. data" - }, - { - "name": "gogcli", - "package_manager": "brew", - "priority": "low", - "optional": true, - "description": "Google Workspace CLI — Gmail, Calendar, Drive, Docs, and more in the terminal" - }, - { - "name": "docker", - "package_manager": "brew-cask", - "priority": "low", - "optional": true, - "description": "Container platform — Docker Desktop for Mac" - }, - { - "name": "fluidtop", - "package_manager": "pipx", - "priority": "low", - "optional": true, - "description": "Apple Silicon performance monitor with AI workload focus — alternative to mactop" - }, - { - "name": "exifread", - "package_manager": "pipx", - "priority": "low", - "optional": true, - "description": "Python CLI for reading EXIF metadata from JPEG and TIFF photos" - }, - { - "name": "Goodnotes", - "package_manager": "app-store", - "priority": "low", - "optional": true, - "description": "Handwriting and note-taking app with Apple Pencil support" - }, - { - "name": "octave", - "package_manager": "brew", - "priority": "none", - "optional": true, - "description": "MATLAB-compatible numerical computing environment" - }, - { - "name": "qemu", - "package_manager": "brew", - "priority": "none", - "optional": true, - "description": "Machine emulator — run x86, ARM, and other architectures on Apple Silicon" - }, - { - "name": "mongodb-community", - "package_manager": "brew", - "priority": "none", - "optional": true, - "install_command": "brew tap mongodb/brew && brew install mongodb-community", - "description": "MongoDB Community Server — includes mongosh and mongodb-database-tools" - }, - { - "name": "Pixelmator Pro", - "package_manager": "app-store", - "priority": "none", - "optional": true, - "description": "Photo editor — pay once" - }, - { - "name": "Amphetamine", - "package_manager": "app-store", - "priority": "none", - "optional": true, - "description": "Keep Mac awake with scheduling and app triggers — more powerful alternative to Caffeine" - } -] diff --git a/macOS/setup.sh b/macOS/setup.sh index 6a718bd..3d51f40 100755 --- a/macOS/setup.sh +++ b/macOS/setup.sh @@ -1,384 +1,8 @@ #!/usr/bin/env bash -# macOS initial setup script -# Usage: bash macOS/setup.sh [--optional] [--work] [--dry-run] -# --optional also install low-priority optional packages -# --work also install work packages (Slack, Zoom, Outlook) -# --dry-run print all commands without executing anything - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PACKAGES_JSON="$SCRIPT_DIR/macOS_packages.json" - -# Other repo setup scripts to advertise at the end of a run. Each entry is -# "|". To advertise a new -# script, just add a line here — only scripts that exist on disk are shown. -RELATED_SCRIPTS=( - "agentic-ai/Claude/install.sh|Claude Code config — symlink settings, hooks, and CLAUDE.md into ~/.claude" - "SSH_and_GPG/create_ssh_key.sh|Generate an SSH key (and add it to GitHub)" - "SSH_and_GPG/create_gpg_key.sh|Generate a GPG key for signed commits" - "macOS/verify.sh|Verify this install (read-only health check)" -) - -INCLUDE_OPTIONAL=false -INCLUDE_WORK=false -DRY_RUN=false - -for arg in "$@"; do - case "$arg" in - --optional) INCLUDE_OPTIONAL=true ;; - --work) INCLUDE_WORK=true ;; - --dry-run) DRY_RUN=true ;; - esac -done - -ZSHRC="$HOME/.zshrc" - -# ── Run helpers ─────────────────────────────────────────────────────────────── - -run() { - if [[ "$DRY_RUN" == true ]]; then - printf ' [dry-run] %s\n' "$*" - else - "$@" - fi -} - -run_eval() { - if [[ "$DRY_RUN" == true ]]; then - printf ' [dry-run] eval: %s\n' "$1" - else - eval "$1" - fi -} - -# npm supply-chain cooldown: refuse to install package versions younger than -# NPM_MIN_RELEASE_AGE days. Compromised releases of popular packages (e.g. the -# axios RAT, Mar 2026) are typically caught and yanked within hours, so a short -# cooldown blocks them while barely delaying legit updates. See issue #23. -NPM_MIN_RELEASE_AGE=7 - -configure_npm_cooldown() { - local npmrc="$HOME/.npmrc" key="min-release-age" tmp - if [[ "$DRY_RUN" == true ]]; then - printf ' [dry-run] set %s=%s in %s\n' "$key" "$NPM_MIN_RELEASE_AGE" "$npmrc" - return 0 - fi - touch "$npmrc" - # Drop any existing entry, then append the desired value (idempotent, portable). - tmp="$(mktemp)" - grep -v "^${key}=" "$npmrc" > "$tmp" 2>/dev/null || true - printf '%s=%s\n' "$key" "$NPM_MIN_RELEASE_AGE" >> "$tmp" - mv "$tmp" "$npmrc" - printf ' ✓ npm %s=%s (%s-day supply-chain cooldown)\n' "$key" "$NPM_MIN_RELEASE_AGE" "$NPM_MIN_RELEASE_AGE" -} - -# Enable pnpm (our daily-driver package manager) via corepack — corepack ships -# with Node — and apply the same supply-chain cooldown. pnpm measures -# minimumReleaseAge in MINUTES and enforces it strictly only when set explicitly -# (the built-in 1-day default is non-strict), so we set it on purpose. Issue #23. -configure_pnpm() { - local age_minutes=$((NPM_MIN_RELEASE_AGE * 24 * 60)) - if [[ "$DRY_RUN" == true ]]; then - printf ' [dry-run] corepack enable && corepack prepare pnpm@latest --activate\n' - printf ' [dry-run] pnpm config set minimumReleaseAge %s --location=user\n' "$age_minutes" - return 0 - fi - if ! command -v corepack &>/dev/null; then - printf ' ⚠ corepack not found (needs Node ≥16) — skipping pnpm setup\n' - return 0 - fi - corepack enable - corepack prepare pnpm@latest --activate - export PNPM_HOME="$HOME/.local/share/pnpm" - export PATH="$PNPM_HOME/bin:$PATH" - if command -v pnpm &>/dev/null; then - pnpm config set minimumReleaseAge "$age_minutes" --location=user - printf ' ✓ pnpm enabled, minimumReleaseAge=%s min (%s-day cooldown)\n' "$age_minutes" "$NPM_MIN_RELEASE_AGE" - else - printf ' ⚠ pnpm not on PATH after corepack — skipped cooldown config\n' - fi -} - -add_to_zshrc() { - local line="$1" - if [[ "$DRY_RUN" == true ]]; then - printf ' [dry-run] add to .zshrc: %s\n' "$line" - else - grep -qF "$line" "$ZSHRC" 2>/dev/null || printf '%s\n' "$line" >> "$ZSHRC" - fi -} - -# ── Package install helpers ─────────────────────────────────────────────────── - -brew_install() { - local priority="$1" optional_filter="$2" - local names - names=$(jq -r --arg p "$priority" --argjson opt "$optional_filter" \ - '[.[] | select( - .package_manager == "brew" and - .priority == $p and - (.work != true) and - (.install_command == null or .install_command == "") and - (if $opt then true else .optional == false end) - ) | .name] | join(" ")' "$PACKAGES_JSON") - # shellcheck disable=SC2086 - [[ -n "$names" ]] && run brew install $names -} - -brew_cask_install() { - local priority="$1" optional_filter="$2" - local names - names=$(jq -r --arg p "$priority" --argjson opt "$optional_filter" \ - '[.[] | select( - .package_manager == "brew-cask" and - .priority == $p and - (.work != true) and - (if $opt then true else .optional == false end) - ) | .name] | join(" ")' "$PACKAGES_JSON") - # shellcheck disable=SC2086 - [[ -n "$names" ]] && run brew install --cask $names -} - -pipx_install() { - local priority="$1" optional_filter="$2" - jq -r --arg p "$priority" --argjson opt "$optional_filter" \ - '.[] | select( - .package_manager == "pipx" and - .priority == $p and - (.work != true) and - (if $opt then true else .optional == false end) - ) | .name' "$PACKAGES_JSON" | while read -r name; do - run pipx install "$name" - done -} - -pnpm_install() { - local priority="$1" optional_filter="$2" - local names - names=$(jq -r --arg p "$priority" --argjson opt "$optional_filter" \ - '[.[] | select( - .package_manager == "pnpm" and - .priority == $p and - (.work != true) and - (if $opt then true else .optional == false end) - ) | .name] | join(" ")' "$PACKAGES_JSON") - # shellcheck disable=SC2086 - [[ -n "$names" ]] && run pnpm add -g $names -} - -brew_custom_install() { - local priority="$1" optional_filter="$2" - jq -r --arg p "$priority" --argjson opt "$optional_filter" \ - '.[] | select( - .package_manager == "brew" and - .priority == $p and - (.work != true) and - (.install_command != null and .install_command != "") and - (if $opt then true else .optional == false end) - ) | .install_command' "$PACKAGES_JSON" | while read -r cmd; do - run_eval "$cmd" - done -} - -print_app_store_reminders() { - local priority="$1" optional_filter="$2" - local apps - apps=$(jq -r --arg p "$priority" --argjson opt "$optional_filter" \ - '.[] | select( - .package_manager == "app-store" and - .priority == $p and - (.work != true) and - (if $opt then true else .optional == false end) - ) | " - \(.name): \(.description)"' "$PACKAGES_JSON") - [[ -n "$apps" ]] && printf '%s\n' "$apps" -} - -# Echo the other repo setup scripts (those present on disk) for discoverability. -print_related_scripts() { - local repo_root entry rel desc shown=false - repo_root="$(cd "$SCRIPT_DIR/.." && pwd)" - for entry in "${RELATED_SCRIPTS[@]}"; do - rel="${entry%%|*}"; desc="${entry#*|}" - [[ -f "$repo_root/$rel" ]] || continue - if [[ "$shown" == false ]]; then - printf ' Other setup scripts in this repo:\n' - shown=true - fi - printf ' • %s\n' "$desc" - printf ' bash %s\n' "$rel" - done -} - -# ── Homebrew ────────────────────────────────────────────────────────────────── -if ! command -v brew &>/dev/null; then - printf '==> Installing Homebrew...\n' - run /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" - run_eval 'eval "$(/opt/homebrew/bin/brew shellenv)"' -fi - -# ── Bootstrap: jq (required to parse macOS_packages.json) ──────────────────── -# jq is system-provided on macOS; only install via brew if missing. -command -v jq &>/dev/null || run brew install jq - -# ── High-priority brew formulae (pyenv, expat, pipx) ───────────────────────── -printf '==> Installing high-priority brew formulae...\n' -brew_install "high" false - -# ── Python (pyenv) ──────────────────────────────────────────────────────────── -# pyenv compiles Python from source using brew's expat, bypassing broken -# Homebrew Python bottles on macOS Tahoe (missing libexpat symbols). -PYTHON_VERSION="3.12.13" -PYENV_PYTHON="$HOME/.pyenv/versions/$PYTHON_VERSION/bin/python3.12" - -if [[ "$DRY_RUN" == true ]]; then - printf ' [dry-run] pyenv install %s\n' "$PYTHON_VERSION" - printf ' [dry-run] pyenv global %s\n' "$PYTHON_VERSION" -elif ! pyenv versions 2>/dev/null | grep -q "$PYTHON_VERSION"; then - printf '==> Installing Python %s via pyenv...\n' "$PYTHON_VERSION" - LDFLAGS="-L/opt/homebrew/opt/expat/lib" \ - CPPFLAGS="-I/opt/homebrew/opt/expat/include" \ - PKG_CONFIG_PATH="/opt/homebrew/opt/expat/lib/pkgconfig" \ - pyenv install "$PYTHON_VERSION" - pyenv global "$PYTHON_VERSION" -fi - -add_to_zshrc 'export PYENV_ROOT="$HOME/.pyenv"' -add_to_zshrc 'export PATH="$PYENV_ROOT/bin:$PATH"' -add_to_zshrc 'eval "$(pyenv init -)"' -add_to_zshrc "export PIPX_DEFAULT_PYTHON=\"$PYENV_PYTHON\"" - -if [[ "$DRY_RUN" == false ]]; then - export PYENV_ROOT="$HOME/.pyenv" - export PATH="$PYENV_ROOT/bin:$PATH" - eval "$(pyenv init -)" - export PIPX_DEFAULT_PYTHON="$PYENV_PYTHON" -fi - -# ── Node (nvm) ──────────────────────────────────────────────────────────────── -# nvm installed via curl — brew install nvm causes PATH issues. -# nvm is kept last in .zshrc so it wins over brew's node. -if [[ "$DRY_RUN" == true ]]; then - printf ' [dry-run] install nvm via curl\n' - printf ' [dry-run] nvm install lts/* && nvm alias default lts/*\n' -elif [[ ! -d "$HOME/.nvm" ]]; then - printf '==> Installing nvm...\n' - INSTALL_CMD=$(jq -r '.[] | select(.name == "nvm") | .install_command' "$PACKAGES_JSON") - eval "$INSTALL_CMD" -fi - -if [[ "$DRY_RUN" == false ]]; then - export NVM_DIR="$HOME/.nvm" - [[ -s "$NVM_DIR/nvm.sh" ]] && \. "$NVM_DIR/nvm.sh" - nvm install 'lts/*' - nvm alias default 'lts/*' - nvm use default -fi - -# Apply the supply-chain cooldown before any package install runs below. -configure_npm_cooldown -configure_pnpm - -add_to_zshrc 'export NVM_DIR="$HOME/.nvm"' -add_to_zshrc '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' -add_to_zshrc 'nvm use --delete-prefix default --silent 2>/dev/null' -# pnpm global bin dir (corepack provides the pnpm shim; PNPM_HOME holds global CLIs) -add_to_zshrc 'export PNPM_HOME="$HOME/.local/share/pnpm"' -add_to_zshrc '[ -d "$PNPM_HOME/bin" ] && export PATH="$PNPM_HOME/bin:$PATH"' - -# ── Medium-priority brew casks ──────────────────────────────────────────────── -printf '==> Installing brew casks...\n' -brew_cask_install "medium" false - -# ── Ghostty config ──────────────────────────────────────────────────────────── -# Written to XDG path (~/.config/ghostty/) — works on both macOS and Linux. -# On macOS the platform-specific path loads after XDG and overrides it, -# so we rename it if it exists to keep XDG as the single source of truth. -GHOSTTY_CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/ghostty" -GHOSTTY_CONFIG="$GHOSTTY_CONFIG_DIR/config.ghostty" -MACOS_GHOSTTY_CONFIG="$HOME/Library/Application Support/com.mitchellh.ghostty/config.ghostty" - -if [[ "$DRY_RUN" == true ]]; then - printf ' [dry-run] mkdir -p %s\n' "$GHOSTTY_CONFIG_DIR" - [[ -f "$GHOSTTY_CONFIG" ]] && printf ' [dry-run] backup %s → %s.bak\n' "$GHOSTTY_CONFIG" "$GHOSTTY_CONFIG" - printf ' [dry-run] cp ghostty.config → %s\n' "$GHOSTTY_CONFIG" - [[ -f "$MACOS_GHOSTTY_CONFIG" ]] && printf ' [dry-run] rename macOS-specific config to .bak\n' -else - mkdir -p "$GHOSTTY_CONFIG_DIR" - [[ -f "$GHOSTTY_CONFIG" ]] && cp "$GHOSTTY_CONFIG" "${GHOSTTY_CONFIG}.bak" - cp "$SCRIPT_DIR/ghostty.config" "$GHOSTTY_CONFIG" - printf '==> Ghostty config written to %s\n' "$GHOSTTY_CONFIG" - if [[ -f "$MACOS_GHOSTTY_CONFIG" ]]; then - mv "$MACOS_GHOSTTY_CONFIG" "${MACOS_GHOSTTY_CONFIG}.bak" - printf '==> Renamed macOS-specific Ghostty config to .bak (XDG takes precedence)\n' - fi -fi - -# ── Git: GPG signing ───────────────────────────────────────────────────────── -run git config --global gpg.program /opt/homebrew/bin/gpg -add_to_zshrc 'export GPG_TTY=$(tty)' - -# ── Medium-priority pipx packages ───────────────────────────────────────────── -printf '==> Installing pipx packages...\n' -if [[ "$DRY_RUN" == false ]]; then - rm -rf ~/.local/pipx/shared - pipx ensurepath -fi -pipx_install "medium" false - -# ── Medium-priority pnpm packages ───────────────────────────────────────────── -printf '==> Installing pnpm packages...\n' -export PNPM_HOME="$HOME/.local/share/pnpm" -export PATH="$PNPM_HOME/bin:$PATH" -if [[ "$DRY_RUN" == true ]] || command -v pnpm &>/dev/null; then - pnpm_install "medium" false - - # Post-install: codeburn menubar (macOS native Swift app) - run codeburn menubar -else - printf ' pnpm not found — skipping (run corepack enable)\n' -fi - -# ── Medium-priority brew formulae ───────────────────────────────────────────── -printf '==> Installing brew formulae...\n' -brew_install "medium" false - -# ── Optional low-priority packages (--optional flag) ────────────────────────── -# priority "none" packages are never auto-installed regardless of flags -if [[ "$INCLUDE_OPTIONAL" == true ]]; then - printf '==> Installing optional (low) packages...\n' - brew_install "low" true - brew_custom_install "low" true - brew_cask_install "low" true - pipx_install "low" true - command -v pnpm &>/dev/null && pnpm_install "low" true -fi - -# ── Work packages (--work flag) ─────────────────────────────────────────────── -if [[ "$INCLUDE_WORK" == true ]]; then - printf '==> Installing work packages...\n' - names=$(jq -r '[.[] | select(.work == true and .package_manager == "brew-cask") | .name] | join(" ")' "$PACKAGES_JSON") - # shellcheck disable=SC2086 - [[ -n "$names" ]] && run brew install --cask $names - names=$(jq -r '[.[] | select(.work == true and .package_manager == "brew") | .name] | join(" ")' "$PACKAGES_JSON") - # shellcheck disable=SC2086 - [[ -n "$names" ]] && run brew install $names - work_app_store=$(jq -r '.[] | select(.work == true and .package_manager == "app-store") | " - \(.name): \(.description)"' "$PACKAGES_JSON") - if [[ -n "$work_app_store" ]]; then - printf 'Install these work apps from the App Store:\n' - printf '%s\n' "$work_app_store" - fi -fi - -# ── App Store reminders ─────────────────────────────────────────────────────── -printf '\n' -printf 'Install these manually from the App Store:\n' -print_app_store_reminders "medium" false -[[ "$INCLUDE_OPTIONAL" == true ]] && print_app_store_reminders "low" true - -printf '\n' -printf 'Optional — run these from the repo root as needed:\n' -print_related_scripts - -printf '\n' -[[ "$DRY_RUN" == true ]] && printf 'Dry run complete — nothing was installed.\n' || printf 'Done! Restart your terminal or open a new tab.\n' +# Thin shim (UNIFICATION.md Phase 4): the install logic lives in the root +# setup.sh + lib/core.sh + platforms/macos.sh, driven by the root packages.json. +# All flags are forwarded (see ../setup.sh for the full list); the trailing +# --platform pin wins over any forwarded platform flag, so this entrypoint +# always runs the macOS flow. +# Usage: bash macOS/setup.sh [--optional] [--work] [--personal] [--dry-run] +exec bash "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." && pwd)/setup.sh" "$@" --platform macos diff --git a/macOS/verify.sh b/macOS/verify.sh index c81db60..1283a55 100755 --- a/macOS/verify.sh +++ b/macOS/verify.sh @@ -1,208 +1,9 @@ -#!/bin/zsh -# macOS install verification script -# Usage: zsh macOS/verify.sh [--optional] [--all] -# --optional also check low-priority optional packages -# --all check every package including priority "none" - -set -e - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PACKAGES_JSON="$SCRIPT_DIR/macOS_packages.json" -INCLUDE_OPTIONAL=false -INCLUDE_NONE=false - -for arg in "$@"; do - case "$arg" in - --optional) INCLUDE_OPTIONAL=true ;; - --all) INCLUDE_NONE=true; INCLUDE_OPTIONAL=true ;; - esac -done - -PASS=0 -FAIL=0 - -# ── Check helpers ───────────────────────────────────────────────────────────── - -check() { - local label="$1" ok="$2" - if [[ "$ok" == true ]]; then - echo " ✅ $label" - PASS=$(( PASS + 1 )) - else - echo " ❌ $label" - FAIL=$(( FAIL + 1 )) - fi -} - -brew_installed() { brew list --formula "$1" &>/dev/null } -cask_installed() { brew list --cask "$1" &>/dev/null } -npm_installed() { npm list -g --depth=0 2>/dev/null | grep -q " $1@" } -pipx_installed() { pipx list --short 2>/dev/null | awk '{print $1}' | grep -qx "$1" } -app_installed() { [[ -d "/Applications/${1}.app" || -d "$HOME/Applications/${1}.app" ]] } - -# ── Section printer ─────────────────────────────────────────────────────────── - -verify_section() { - local section_label="$1" priority_filter="$2" opt_filter="$3" - - # Output tab-separated: name package_manager optional - local rows - rows=$(jq -r --arg p "$priority_filter" --argjson opt "$opt_filter" \ - '.[] | select( - .priority == $p and - (if $opt then true else .optional == false end) - ) | [.name, .package_manager, (.optional | tostring)] | @tsv' \ - "$PACKAGES_JSON") - - [[ -z "$rows" ]] && return - - echo "" - echo "── $section_label ──────────────────────────────────────────────────" - - local name pm opt_flag label_str installed - while IFS=$'\t' read -r name pm opt_flag; do - label_str="$name ($pm)" - [[ "$opt_flag" == "true" ]] && label_str="$label_str [optional]" - - case "$pm" in - brew) - if brew_installed "$name"; then - installed=true - elif command -v "$name" &>/dev/null; then - installed=true - label_str="$label_str (via system/other)" - else - installed=false - fi - ;; - brew-cask) - cask_installed "$name" && installed=true || installed=false ;; - npm) - npm_installed "$name" && installed=true || installed=false ;; - pipx) - pipx_installed "$name" && installed=true || installed=false ;; - curl) - # nvm — check for ~/.nvm directory - if [[ "$name" == "nvm" ]]; then - [[ -d "$HOME/.nvm" ]] && installed=true || installed=false - else - installed=false - label_str="$label_str (manual check required)" - fi - ;; - app-store) - app_installed "$name" && installed=true || installed=false - [[ "$installed" == "false" ]] && label_str="$label_str (check App Store manually)" - ;; - *) - installed=false - label_str="$label_str (unknown manager: $pm)" - ;; - esac - - check "$label_str" "$installed" - done <<< "$rows" -} - -# ── Extra checks (runtime environments not in JSON) ─────────────────────────── - -verify_extras() { - echo "" - echo "── Runtime environments ──────────────────────────────────────" - - # Homebrew - command -v brew &>/dev/null && check "homebrew (brew)" true || check "homebrew (brew)" false - - # pyenv - command -v pyenv &>/dev/null && check "pyenv" true || check "pyenv" false - - # Python via pyenv - local py_ver - py_ver=$(pyenv version-name 2>/dev/null || echo "") - if [[ -n "$py_ver" && "$py_ver" != "system" ]]; then - check "python via pyenv ($py_ver)" true - else - check "python via pyenv (no pyenv version active)" false - fi - - # nvm - export NVM_DIR="$HOME/.nvm" - [[ -s "$NVM_DIR/nvm.sh" ]] && source "$NVM_DIR/nvm.sh" 2>/dev/null - command -v nvm &>/dev/null && check "nvm" true || check "nvm" false - - # Node (nvm-managed) - local node_ver nvm_cur - node_ver=$(node --version 2>/dev/null || echo "") - nvm_cur=$(nvm current 2>/dev/null || echo "") - if [[ -n "$node_ver" && "$nvm_cur" != "system" && -n "$nvm_cur" ]]; then - check "node via nvm ($node_ver)" true - else - check "node via nvm (run: nvm install 'lts/*')" false - fi - - # npm supply-chain cooldown (issue #23) - if grep -q '^min-release-age=' "$HOME/.npmrc" 2>/dev/null; then - local mra - mra=$(grep '^min-release-age=' "$HOME/.npmrc" | tail -1 | cut -d= -f2) - check "npm min-release-age set (${mra}-day cooldown)" true - else - check "npm min-release-age set (run setup.sh)" false - fi - - # pnpm (daily-driver package manager) + its supply-chain cooldown (issue #23) - export PNPM_HOME="$HOME/.local/share/pnpm" - [[ -d "$PNPM_HOME/bin" ]] && export PATH="$PNPM_HOME/bin:$PATH" - if command -v pnpm &>/dev/null; then - check "pnpm ($(pnpm --version 2>/dev/null))" true - local pmra - pmra=$(pnpm config get minimumReleaseAge 2>/dev/null) - if [[ -n "$pmra" && "$pmra" != "undefined" && "$pmra" -gt 0 ]] 2>/dev/null; then - check "pnpm minimumReleaseAge set (${pmra} min)" true - else - check "pnpm minimumReleaseAge set (run setup.sh)" false - fi - else - check "pnpm (run setup.sh — corepack enable)" false - fi - - # Ghostty XDG config - local ghostty_cfg="${XDG_CONFIG_HOME:-$HOME/.config}/ghostty/config.ghostty" - [[ -f "$ghostty_cfg" ]] && check "ghostty config at XDG path" true || check "ghostty config at XDG path" false - - # PIPX_DEFAULT_PYTHON pointing to pyenv python - if [[ -n "$PIPX_DEFAULT_PYTHON" && -f "$PIPX_DEFAULT_PYTHON" ]]; then - check "PIPX_DEFAULT_PYTHON → pyenv ($PIPX_DEFAULT_PYTHON)" true - else - check "PIPX_DEFAULT_PYTHON → pyenv (set in .zshrc, source it first)" false - fi -} - -# ── Run ─────────────────────────────────────────────────────────────────────── - -echo "==> Verifying macOS package installs against $PACKAGES_JSON" - -verify_extras - -verify_section "High priority" "high" false - -verify_section "Medium priority" "medium" false - -if [[ "$INCLUDE_OPTIONAL" == true ]]; then - verify_section "Low priority (optional)" "low" true -fi - -if [[ "$INCLUDE_NONE" == true ]]; then - verify_section "Priority none (manual only)" "none" true -fi - -# ── Summary ─────────────────────────────────────────────────────────────────── - -echo "" -echo "────────────────────────────────────────────────────────────" -echo " ✅ $PASS installed ❌ $FAIL missing" -echo "" -if [[ $FAIL -gt 0 ]]; then - echo "Run setup.sh to install missing packages, or install them manually." -else - echo "All checked packages are installed." -fi +#!/usr/bin/env bash +# Thin shim (UNIFICATION.md Phase 4): the check logic lives in the root +# verify.sh + lib/verify.sh, driven by the root packages.json. All flags are +# forwarded (see ../verify.sh); the trailing --platform pin wins over any +# forwarded platform flag, so this entrypoint always checks the macOS set. +# Note: work-tagged packages now require --work (or --all), matching setup.sh; +# the legacy script checked them unconditionally. +# Usage: bash macOS/verify.sh [--optional] [--work] [--personal] [--all] +exec bash "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." && pwd)/verify.sh" "$@" --platform macos diff --git a/macOS/zshrc-upgrade.md b/macOS/zshrc-upgrade.md deleted file mode 100644 index e9a5e8c..0000000 --- a/macOS/zshrc-upgrade.md +++ /dev/null @@ -1,227 +0,0 @@ -# macOS zshrc upgrade plan - -Bring the macOS zshrc.example up to parity with the linux-desktop config, -which now has CachyOS Fish-style enhancements via antidote. - -## Current state - -The macOS zshrc manually sources two plugins from Homebrew: - -```zsh -source $(brew --prefix)/share/zsh-autosuggestions/zsh-autosuggestions.zsh -source $(brew --prefix)/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh -``` - -No icons, no abbreviations, no substring search, no command duration, no -notify, no zoxide, no fzf keybindings, no bat man pages. - -## Target state - -Match linux-desktop/zshrc.example feature set while keeping macOS-specific -bits (Homebrew PATH, brew completions, `brewup` alias). - -## Changes required - -### 1. Switch to antidote - -**Install:** `brew install antidote` - -**Replace** the manual `source $(brew --prefix)/share/...` lines with: - -```zsh -if [[ -f $(brew --prefix)/opt/antidote/share/antidote/antidote.zsh ]]; then - source $(brew --prefix)/opt/antidote/share/antidote/antidote.zsh - antidote load -fi -``` - -**Create** `macOS/zsh_plugins.txt` (identical to linux-desktop): - -``` -zsh-users/zsh-completions kind:fpath path:src -zsh-users/zsh-autosuggestions -zsh-users/zsh-syntax-highlighting -zsh-users/zsh-history-substring-search -olets/zsh-abbr -marzocchi/zsh-notify -``` - -This replaces two Homebrew-managed plugins with six antidote-managed ones. -The Homebrew `zsh-autosuggestions` and `zsh-syntax-highlighting` formulae -can be uninstalled afterward. - -### 2. eza aliases with icons - -**Prerequisite:** A Nerd Font must be set in Ghostty (e.g., JetBrains Mono -Nerd Font). Check ghostty.config. - -**Replace** current eza block: - -```zsh -alias ls='ls -G' -if command -v eza &>/dev/null; then - alias ll='eza -lah --git' - alias la='eza -a' -else - ... -fi -``` - -**With:** - -```zsh -alias ls='ls -G' -if command -v eza &>/dev/null; then - alias ls='eza -al --color=always --icons --group-directories-first' - alias ll='eza -lah --git --icons --group-directories-first' - alias la='eza -a --icons --group-directories-first' - alias lt='eza --tree --icons --level=2 --group-directories-first' -else - ... -fi -``` - -**Packages needed:** `brew install eza` (may already be installed). - -### 3. RPROMPT with command duration - -**Add** the `_prompt_precmd` / `_prompt_preexec` hooks and RPROMPT from -linux-desktop/zshrc.example. Replace the simple `precmd() { vcs_info }` -with the full precmd that also tracks command duration. - -```zsh -_prompt_precmd() { - local exit_code=$? - vcs_info - _last_exit=$exit_code - _cmd_end=$EPOCHSECONDS - if (( _cmd_start > 0 && _cmd_end - _cmd_start >= 2 )); then - local elapsed=$(( _cmd_end - _cmd_start )) - local mins=$(( elapsed / 60 )) - local secs=$(( elapsed % 60 )) - if (( mins > 0 )); then - _cmd_duration="${mins}m${secs}s" - else - _cmd_duration="${secs}s" - fi - else - _cmd_duration="" - fi - _cmd_start=0 -} - -_prompt_preexec() { _cmd_start=$EPOCHSECONDS } - -precmd_functions+=(_prompt_precmd) -preexec_functions+=(_prompt_preexec) - -RPROMPT='${_cmd_duration:+%F{242}${_cmd_duration}%f}' -``` - -### 4. Colorized man pages via bat - -```zsh -if command -v bat &>/dev/null; then - export MANPAGER="sh -c 'col -bx | bat -l man -p'" - export MANROFFOPT="-c" -fi -``` - -**Packages needed:** `brew install bat` (may already be installed). - -### 5. zsh-notify - -On macOS, zsh-notify uses `terminal-notifier` instead of `notify-send`. - -**Packages needed:** `brew install terminal-notifier` - -**Config:** - -```zsh -zstyle ':notify:*' command-complete-timeout 30 -zstyle ':notify:*' error-title "Command failed" -zstyle ':notify:*' success-title "Command finished" -``` - -No `xdotool` needed — macOS focus detection uses native AppleScript. - -### 6. Fish-style abbreviations - -```zsh -if type abbr &>/dev/null; then - abbr set -q gs='git status' 2>/dev/null - abbr set -q ga='git add' 2>/dev/null - abbr set -q gc='git commit' 2>/dev/null - abbr set -q gp='git push' 2>/dev/null - abbr set -q gl='git log --oneline --graph --decorate' 2>/dev/null - abbr set -q ..='cd ..' 2>/dev/null - abbr set -q ...='cd ../..' 2>/dev/null -fi -``` - -This replaces the existing static git aliases (`alias gs=...`) — abbr -expands inline so your history shows the real command. - -### 7. History substring search - -```zsh -bindkey '^[[A' history-substring-search-up -bindkey '^[[B' history-substring-search-down -bindkey '^[OA' history-substring-search-up -bindkey '^[OB' history-substring-search-down -``` - -### 8. Completion enhancements - -```zsh -zstyle ':completion:*' group-name '' -zstyle ':completion:*:descriptions' format '%F{yellow}── %d ──%f' -``` - -### 9. Syntax highlighting config - -```zsh -ZSH_HIGHLIGHT_HIGHLIGHTERS=(main brackets pattern) -ZSH_HIGHLIGHT_STYLES[path]='fg=cyan,underline' -ZSH_HIGHLIGHT_STYLES[path_prefix]='fg=cyan,underline' -``` - -### 10. Zoxide - -```zsh -command -v zoxide &>/dev/null && eval "$(zoxide init zsh)" -``` - -**Packages needed:** `brew install zoxide` (may already be installed). - -### 11. FZF keybindings (Homebrew paths) - -```zsh -[[ -f $(brew --prefix)/opt/fzf/shell/key-bindings.zsh ]] && \ - source $(brew --prefix)/opt/fzf/shell/key-bindings.zsh -[[ -f $(brew --prefix)/opt/fzf/shell/completion.zsh ]] && \ - source $(brew --prefix)/opt/fzf/shell/completion.zsh -``` - -**Packages needed:** `brew install fzf` (may already be installed). - -## Packages to install (Homebrew) - -```bash -brew install antidote eza bat zoxide fzf terminal-notifier -``` - -## Packages to uninstall (replaced by antidote) - -```bash -brew uninstall zsh-autosuggestions zsh-syntax-highlighting -``` - -## Migration steps - -1. Install new Homebrew packages -2. Create `~/.zsh_plugins.txt` from `macOS/zsh_plugins.txt` -3. Replace `~/.zshrc` with updated `macOS/zshrc.example` -4. Open new Ghostty terminal (antidote clones plugins on first load) -5. Uninstall replaced Homebrew formulae -6. Verify: icons, abbreviations, notifications, man pages, duration diff --git a/macOS/zshrc.example b/macOS/zshrc.example deleted file mode 100644 index 415c2c1..0000000 --- a/macOS/zshrc.example +++ /dev/null @@ -1,131 +0,0 @@ -# ------------------------------------------------------------------------------------------------- - -# ═══════════════════════════════════════════════════════════════ -# ~/.zshrc — macOS zsh config (Ghostty + Apple Silicon) -# ═══════════════════════════════════════════════════════════════ - -# Requires fastfetch via brew - -# ── Instant prompt (Powerlevel10k — keep at very top) ────────── -[[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]] && \ - source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" - - -# ═══════════════════════════════════════════════════════════════ -# PATH -# ═══════════════════════════════════════════════════════════════ -export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:$PATH" -export PATH="$HOME/.local/bin:$PATH" - - -# ═══════════════════════════════════════════════════════════════ -# HISTORY -# ═══════════════════════════════════════════════════════════════ -HISTFILE="$HOME/.zsh_history" -HISTSIZE=10000 -SAVEHIST=10000 -setopt SHARE_HISTORY -setopt HIST_IGNORE_DUPS -setopt HIST_IGNORE_SPACE -setopt HIST_REDUCE_BLANKS - - -# ═══════════════════════════════════════════════════════════════ -# COMPLETION -# ═══════════════════════════════════════════════════════════════ -autoload -Uz compinit && compinit -zstyle ':completion:*' menu select -zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}' -zstyle ':completion:*' list-colors "${(s.:.)LS_COLORS}" - - -# ═══════════════════════════════════════════════════════════════ -# OPTIONS -# ═══════════════════════════════════════════════════════════════ -setopt AUTO_CD -setopt CORRECT -setopt GLOB_DOTS -setopt NO_BEEP - - -# ═══════════════════════════════════════════════════════════════ -# PROMPT (Powerlevel10k if available, vcs_info fallback) -# ═══════════════════════════════════════════════════════════════ -# macOS: brew install powerlevel10k | run: p10k configure -if [[ -f /opt/homebrew/share/powerlevel10k/powerlevel10k.zsh-theme ]]; then - source /opt/homebrew/share/powerlevel10k/powerlevel10k.zsh-theme -else - autoload -Uz vcs_info - precmd() { vcs_info } - zstyle ':vcs_info:*' enable git - zstyle ':vcs_info:*' check-for-changes true - zstyle ':vcs_info:git:*' stagedstr '%F{green}+%f' - zstyle ':vcs_info:git:*' unstagedstr '%F{red}*%f' - zstyle ':vcs_info:git:*' formats ' (%F{magenta}%b%f%u%c)' - zstyle ':vcs_info:git:*' actionformats ' (%F{magenta}%b%f|%F{yellow}%a%f%u%c)' - setopt PROMPT_SUBST - PROMPT='%F{blue}%n%f%F{white}@%f%F{cyan}%m%f %F{yellow}%~%f${vcs_info_msg_0_} %# ' -fi - - -# ═══════════════════════════════════════════════════════════════ -# ALIASES -# ═══════════════════════════════════════════════════════════════ -alias ls='ls -G' -if command -v eza &>/dev/null; then - alias ll='eza -lah --git' - alias la='eza -a' -else - alias ll='ls -lAh' - alias la='ls -A' -fi -alias ..='cd ..' -alias ...='cd ../..' -alias grep='grep --color=auto' -alias df='df -h' -alias du='du -sh' -alias reload='source ~/.zshrc' - -# Homebrew -alias brewup='brew update && brew upgrade && brew cleanup' - -# Git -alias gs='git status' -alias ga='git add' -alias gc='git commit' -alias gp='git push' -alias gl='git log --oneline --graph --decorate' - - -# ═══════════════════════════════════════════════════════════════ -# ENVIRONMENT -# ═══════════════════════════════════════════════════════════════ -export EDITOR='micro' -export LANG='en_US.UTF-8' -export LC_ALL='en_US.UTF-8' -export GPG_TTY=$(tty) - - -# ═══════════════════════════════════════════════════════════════ -# HOMEBREW COMPLETIONS -# ═══════════════════════════════════════════════════════════════ -if command -v brew &>/dev/null; then - FPATH="$(brew --prefix)/share/zsh/site-functions:$FPATH" -fi - - -# ═══════════════════════════════════════════════════════════════ -# SYSINFO DISPLAY (Ghostty startup) -# ═══════════════════════════════════════════════════════════════ -[[ -o interactive && "$TERM_PROGRAM" == "ghostty" ]] && fastfetch - -# ------------------------------------------------------------------------------------------------- - -# Created by `pipx` on 2026-02-01 10:15:48 -export PATH="$PATH:/Users/ulises/.local/bin" - -# zsh auto suggestions & syntax highlighting (installed via Homebrew) -source $(brew --prefix)/share/zsh-autosuggestions/zsh-autosuggestions.zsh -source $(brew --prefix)/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh - -[[ -f ~/.p10k.zsh ]] && source ~/.p10k.zsh diff --git a/packages.json b/packages.json new file mode 100644 index 0000000..9694df5 --- /dev/null +++ b/packages.json @@ -0,0 +1,815 @@ +[ + { + "name": "zsh", + "package_manager": { "ubuntu": "apt", "arch": "yay", "server": "apt" }, + "priority": "high", + "optional": false, + "description": "Z shell — common shell across all devices; install first so plugins are available" + }, + { + "name": "zsh-antidote", + "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, + "macos_name": "antidote", + "priority": "high", + "optional": false, + "description": "Lightweight zsh plugin manager — handles autosuggestions, syntax highlighting, completions, substring search, and abbreviations via a plugins file" + }, + { + "name": "zoxide", + "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay" }, + "priority": "high", + "optional": false, + "description": "Smarter cd — learns your frequent directories, use z to jump instantly" + }, + { + "name": "expat", + "package_manager": { "macos": "brew" }, + "priority": "high", + "optional": false, + "description": "XML parsing library — required for pyenv to compile Python correctly on macOS Tahoe" + }, + { + "name": "pyenv", + "package_manager": { "macos": "brew", "ubuntu": "custom", "arch": "custom" }, + "priority": "high", + "optional": false, + "handled_by_setup": true, + "install_command": "curl https://pyenv.run | bash", + "description": "Python version manager — compiles from source to avoid distro-packaged Python quirks and broken Homebrew bottles on macOS Tahoe" + }, + { + "name": "nvm", + "package_manager": { "macos": "custom", "ubuntu": "custom", "arch": "custom" }, + "priority": "high", + "optional": false, + "handled_by_setup": true, + "install_command": "curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash", + "description": "Node version manager — installed via curl per nvm recommendation; brew install causes PATH issues" + }, + { + "name": "pipx", + "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay" }, + "arch_name": "python-pipx", + "priority": "high", + "optional": false, + "description": "Installs Python CLI tools in isolated virtual environments" + }, + { + "name": "uv", + "package_manager": { "macos": "brew", "ubuntu": "pipx", "arch": "yay" }, + "priority": "medium", + "optional": false, + "description": "Fast Python package and project manager — pip/venv/pyenv successor" + }, + { + "name": "build-essential", + "package_manager": { "ubuntu": "apt", "arch": "yay" }, + "arch_name": "base-devel", + "priority": "high", + "optional": false, + "description": "Compiler toolchain — gcc, make, etc.; required for pyenv, native Node modules, and AUR builds" + }, + { + "name": "git", + "package_manager": { "ubuntu": "apt", "arch": "yay" }, + "priority": "high", + "optional": false, + "description": "Version control — usually pre-installed but listed to ensure it's present" + }, + { + "name": "git", + "package_manager": { "server": "apt" }, + "priority": "medium", + "optional": false, + "description": "Version control — usually pre-installed on Ubuntu Server" + }, + { + "name": "rust", + "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay" }, + "ubuntu_name": "rustup", + "arch_name": "rustup", + "priority": "medium", + "optional": false, + "description": "Rust toolchain — rustup on Linux (distro rustc goes stale; setup runs 'rustup default stable'), plain rust formula on macOS" + }, + { + "name": "go", + "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay" }, + "ubuntu_name": "golang-go", + "priority": "medium", + "optional": false, + "description": "Go toolchain" + }, + { + "name": "ghostty", + "package_manager": { "macos": "brew-cask", "ubuntu": "snap", "arch": "yay" }, + "priority": "medium", + "optional": false, + "install_command": "sudo snap install ghostty --classic", + "description": "GPU-accelerated terminal emulator — requires --classic on snap for full system access" + }, + { + "name": "fastfetch", + "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, + "priority": "medium", + "optional": false, + "description": "Fast system info display — shown on terminal startup; Ubuntu needs the fastfetch PPA (setup adds it)" + }, + { + "name": "gh", + "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, + "arch_name": "github-cli", + "priority": "medium", + "optional": false, + "description": "GitHub CLI — PRs, issues, Actions from the terminal; apt platforms use the GitHub CLI apt repo (setup adds it)" + }, + { + "name": "forgejo-cli", + "package_manager": { "macos": "brew", "ubuntu": "custom", "arch": "custom" }, + "priority": "medium", + "optional": false, + "install_command": "cargo install forgejo-cli", + "description": "Forgejo CLI (fj) — PRs, issues, releases against the self-hosted Forgejo instance; no Linux package, cargo is the upstream install path" + }, + { + "name": "git-lfs", + "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, + "priority": "medium", + "optional": false, + "description": "Git extension for versioning large binary files" + }, + { + "name": "git-xet", + "package_manager": { "macos": "brew" }, + "priority": "medium", + "optional": false, + "description": "Git LFS plugin using the Xet protocol for Hugging Face Hub" + }, + { + "name": "git-xet", + "package_manager": { "ubuntu": "custom", "arch": "custom" }, + "priority": "medium", + "optional": false, + "environment": ["personal"], + "install_command": "curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/huggingface/xet-core/refs/heads/main/git_xet/install.sh | sh", + "description": "Git LFS plugin using the Xet protocol for Hugging Face Hub" + }, + { + "name": "pre-commit", + "package_manager": { "ubuntu": "pipx", "arch": "yay" }, + "priority": "medium", + "optional": false, + "description": "Git pre-commit hook framework — runs this repo's shellcheck, JSON, and zsh syntax checks before each commit" + }, + { + "name": "shellcheck", + "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay" }, + "priority": "medium", + "optional": false, + "description": "Static analysis linter for shell scripts — used by the pre-commit config and CI" + }, + { + "name": "codespell", + "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay" }, + "priority": "medium", + "optional": false, + "description": "Spell checker for source code — catches common misspellings in identifiers, comments, and docs" + }, + { + "name": "swiftlint", + "package_manager": { "macos": "brew" }, + "priority": "medium", + "optional": false, + "description": "Swift linter — Swift development is macOS-only" + }, + { + "name": "swiftformat", + "package_manager": { "macos": "brew" }, + "priority": "medium", + "optional": false, + "description": "Swift code formatter — pairs with swiftlint" + }, + { + "name": "xcodegen", + "package_manager": { "macos": "brew" }, + "priority": "medium", + "optional": false, + "description": "Generates Xcode project files from a YAML/JSON spec — Xcode tooling is macOS-only" + }, + { + "name": "gnupg", + "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, + "priority": "medium", + "optional": false, + "description": "GPG — required for commit signing" + }, + { + "name": "hf", + "package_manager": { "macos": "brew" }, + "priority": "medium", + "optional": false, + "description": "Hugging Face Hub CLI — download, upload, and manage ML models" + }, + { + "name": "hf", + "package_manager": { "ubuntu": "pipx", "arch": "pipx" }, + "priority": "medium", + "optional": false, + "environment": ["personal"], + "install_command": "pipx install huggingface-hub[cli]", + "description": "Hugging Face Hub CLI — download, upload, and manage ML models" + }, + { + "name": "htop", + "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, + "priority": "medium", + "optional": false, + "description": "Interactive process viewer — colorized alternative to top" + }, + { + "name": "llama.cpp", + "package_manager": { "macos": "brew" }, + "priority": "medium", + "optional": false, + "description": "LLM inference engine — run local models on CPU and Apple Silicon GPU" + }, + { + "name": "llama.cpp", + "package_manager": { "ubuntu": "custom", "arch": "yay" }, + "priority": "none", + "optional": true, + "install_command": "See https://github.com/ggml-org/llama.cpp#build", + "description": "LLM inference engine — run local models on CPU/GPU" + }, + { + "name": "mlx-lm", + "package_manager": { "macos": "brew" }, + "priority": "medium", + "optional": false, + "description": "Run and fine-tune LLMs with Apple MLX on Apple silicon" + }, + { + "name": "mactop", + "package_manager": { "macos": "brew" }, + "priority": "medium", + "optional": false, + "description": "Apple Silicon system monitor — CPU, GPU, ANE, memory, power in real-time TUI" + }, + { + "name": "micro", + "package_manager": { "macos": "brew", "ubuntu": "snap", "arch": "yay", "server": "snap" }, + "priority": "medium", + "optional": false, + "install_command": "sudo snap install micro --classic", + "description": "Modern terminal text editor — snap on noble platforms (apt is stuck at 2.0.13; upstream guarantees latest via snap)" + }, + { + "name": "nvtop", + "package_manager": { "macos": "brew", "ubuntu": "snap", "arch": "yay", "server": "snap" }, + "priority": "medium", + "optional": false, + "description": "GPU process monitor — complements htop with GPU visibility" + }, + { + "name": "lact", + "package_manager": { "arch": "yay" }, + "priority": "medium", + "optional": false, + "description": "Linux AMD GPU control panel — fan curves, voltage offset, power cap, clock limits via GUI + background daemon; see linux-desktop/lact_r9700_tuning.md for R9700 config" + }, + { + "name": "sshpass", + "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, + "priority": "medium", + "optional": false, + "description": "Non-interactive SSH password authentication — used by SSH_and_GPG/add_remote_host.sh" + }, + { + "name": "tmux", + "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, + "priority": "medium", + "optional": false, + "description": "Terminal multiplexer — split panes, persist sessions across disconnects" + }, + { + "name": "eza", + "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, + "priority": "medium", + "optional": false, + "description": "Modern ls replacement — colors, icons, git status, tree view; Ubuntu needs the eza community apt repo (setup adds it)" + }, + { + "name": "terminal-notifier", + "package_manager": { "macos": "brew" }, + "priority": "medium", + "optional": false, + "description": "macOS notification CLI — backend for zsh-notify (Linux uses notify-send/xdotool instead)" + }, + { + "name": "fzf", + "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, + "priority": "medium", + "optional": false, + "description": "Fuzzy finder — Ctrl+R history search, Ctrl+T file search" + }, + { + "name": "ripgrep", + "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, + "priority": "medium", + "optional": false, + "description": "Fast recursive grep replacement — command: rg" + }, + { + "name": "bat", + "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, + "priority": "medium", + "optional": false, + "description": "cat with syntax highlighting and git integration — Ubuntu installs binary as batcat (setup symlinks bat)" + }, + { + "name": "fd", + "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, + "ubuntu_name": "fd-find", + "server_name": "fd-find", + "priority": "medium", + "optional": false, + "description": "Fast find alternative — Ubuntu installs binary as fdfind (setup symlinks fd)" + }, + { + "name": "jq", + "package_manager": { "ubuntu": "apt", "arch": "yay", "server": "apt" }, + "priority": "medium", + "optional": false, + "description": "JSON processor — required by setup.sh to parse this file" + }, + { + "name": "jq", + "package_manager": { "macos": "brew" }, + "priority": "low", + "optional": true, + "description": "JSON processor — system-provided on macOS; brew version only needed if the system one is missing or outdated" + }, + { + "name": "curl", + "package_manager": { "ubuntu": "apt", "arch": "yay", "server": "apt" }, + "priority": "medium", + "optional": false, + "description": "HTTP client — usually pre-installed; required by various install scripts" + }, + { + "name": "wget", + "package_manager": { "ubuntu": "apt", "arch": "yay", "server": "apt" }, + "priority": "medium", + "optional": false, + "description": "HTTP file downloader — used by setup scripts for key imports" + }, + { + "name": "libnotify-bin", + "package_manager": { "ubuntu": "apt", "arch": "yay" }, + "arch_name": "libnotify", + "priority": "medium", + "optional": false, + "description": "Desktop notifications CLI (notify-send) — used by zsh-notify plugin" + }, + { + "name": "xdotool", + "package_manager": { "ubuntu": "apt", "arch": "yay" }, + "priority": "medium", + "optional": false, + "description": "X11 window automation — used by zsh-notify for terminal focus detection" + }, + { + "name": "ncdu", + "package_manager": { "server": "apt" }, + "priority": "medium", + "optional": false, + "description": "Interactive disk usage analyzer — essential for managing storage on a file server" + }, + { + "name": "cockpit", + "package_manager": { "server": "apt" }, + "priority": "medium", + "optional": false, + "description": "Web-based server admin UI — system metrics, journal logs, network config, storage, and service management; access at https://:9090" + }, + { + "name": "tailscale", + "package_manager": { "macos": "app-store", "ubuntu": "custom", "arch": "yay", "server": "custom" }, + "macos_name": "Tailscale", + "priority": "medium", + "optional": false, + "install_command": "curl -fsSL https://tailscale.com/install.sh | sudo sh", + "handled_by_setup": true, + "description": "Zero-config VPN — access all devices on a private network; App Store version required on macOS for network extension entitlements" + }, + { + "name": "docker", + "package_manager": { "macos": "brew-cask", "ubuntu": "custom", "arch": "yay" }, + "macos_name": "docker-desktop", + "priority": "low", + "optional": true, + "handled_by_setup": true, + "install_command": "curl -fsSL https://get.docker.com | sudo sh", + "description": "Container platform — Docker Desktop on macOS, official Docker Engine on Linux" + }, + { + "name": "docker-ce", + "package_manager": { "server": "custom" }, + "priority": "medium", + "optional": false, + "handled_by_setup": true, + "install_command": "curl -fsSL https://get.docker.com | sudo sh", + "description": "Container platform — official Docker Engine for Linux; convenience script installs CE, CLI, compose plugin, and containerd" + }, + { + "name": "claude-code", + "package_manager": { "macos": "custom", "ubuntu": "custom", "arch": "custom", "server": "custom" }, + "priority": "medium", + "optional": false, + "handled_by_setup": true, + "install_command": "curl -fsSL https://claude.ai/install.sh | bash", + "description": "Claude Code — terminal-based AI coding assistant" + }, + { + "name": "opencode", + "package_manager": { "macos": "brew", "ubuntu": "custom", "arch": "yay" }, + "macos_name": "anomalyco/tap/opencode", + "arch_name": "opencode-bin", + "priority": "medium", + "optional": false, + "install_command": { "ubuntu": "curl -fsSL https://opencode.ai/install | bash" }, + "description": "Terminal-based AI coding agent — brew installs from the anomalyco tap" + }, + { + "name": "claude-desktop", + "package_manager": { "macos": "brew-cask", "ubuntu": "custom", "arch": "yay" }, + "macos_name": "claude", + "arch_name": "claude-desktop-bin", + "priority": "medium", + "optional": false, + "install_command": "curl -fsSL https://pkg.claude-desktop-debian.dev/KEY.gpg | sudo gpg --dearmor -o /usr/share/keyrings/claude-desktop.gpg && echo 'deb [signed-by=/usr/share/keyrings/claude-desktop.gpg arch=amd64,arm64] https://pkg.claude-desktop-debian.dev stable main' | sudo tee /etc/apt/sources.list.d/claude-desktop.list && sudo apt update && sudo apt install -y claude-desktop", + "description": "Anthropic Claude desktop app — Ubuntu uses aaddrick/claude-desktop-debian apt repo" + }, + { + "name": "visual-studio-code", + "package_manager": { "macos": "brew-cask", "ubuntu": "snap", "arch": "yay" }, + "ubuntu_name": "code", + "arch_name": "visual-studio-code-bin", + "priority": "medium", + "optional": false, + "install_command": "sudo snap install code --classic", + "description": "Code editor with extensions — requires --classic on snap" + }, + { + "name": "firefox", + "package_manager": { "macos": "brew-cask", "ubuntu": "snap", "arch": "yay" }, + "priority": "medium", + "optional": false, + "description": "Open source web browser" + }, + { + "name": "zen-browser", + "package_manager": { "macos": "brew-cask", "ubuntu": "custom", "arch": "yay" }, + "macos_name": "zen", + "arch_name": "zen-browser-bin", + "priority": "medium", + "optional": false, + "install_command": { "ubuntu": "flatpak install flathub app.zen_browser.zen" }, + "description": "Firefox-based browser focused on privacy and a calmer UI — no Ubuntu snap/apt package, flatpak is the upstream install path" + }, + { + "name": "chromium", + "package_manager": { "ubuntu": "snap", "arch": "yay" }, + "priority": "medium", + "optional": false, + "environment": ["work"], + "description": "Open source Chromium browser — used for work alongside Firefox" + }, + { + "name": "obs-studio", + "package_manager": { "macos": "brew-cask", "ubuntu": "apt", "arch": "yay" }, + "macos_name": "obs", + "priority": "medium", + "optional": false, + "description": "Open source broadcasting and screen recording" + }, + { + "name": "vlc", + "package_manager": { "macos": "brew-cask", "ubuntu": "apt", "arch": "yay" }, + "priority": "medium", + "optional": false, + "description": "Open source media player for nearly every format" + }, + { + "name": "discord", + "package_manager": { "macos": "brew-cask" }, + "priority": "medium", + "optional": false, + "description": "Voice, video, and text chat platform" + }, + { + "name": "discord", + "package_manager": { "ubuntu": "snap", "arch": "yay" }, + "priority": "medium", + "optional": false, + "environment": ["personal"], + "description": "Voice, video, and text chat platform" + }, + { + "name": "bitwarden", + "package_manager": { "macos": "app-store", "ubuntu": "snap", "arch": "yay" }, + "macos_name": "Bitwarden", + "priority": "medium", + "optional": false, + "description": "Open source password manager — App Store version required on macOS for browser integration" + }, + { + "name": "slack", + "package_manager": { "macos": "brew-cask", "ubuntu": "snap", "arch": "yay" }, + "arch_name": "slack-desktop", + "priority": "medium", + "optional": false, + "environment": ["work"], + "description": "Team messaging and collaboration" + }, + { + "name": "zoom", + "package_manager": { "macos": "brew-cask", "ubuntu": "snap", "arch": "yay" }, + "ubuntu_name": "zoom-client", + "priority": "medium", + "optional": false, + "environment": ["work"], + "description": "Video conferencing" + }, + { + "name": "microsoft-outlook", + "package_manager": { "macos": "brew-cask" }, + "priority": "medium", + "optional": false, + "environment": ["work"], + "description": "Email client for Microsoft 365" + }, + { + "name": "teams-for-linux", + "package_manager": { "ubuntu": "snap", "arch": "yay" }, + "priority": "medium", + "optional": false, + "environment": ["work"], + "description": "Unofficial Microsoft Teams client for Linux" + }, + { + "name": "poetry", + "package_manager": { "macos": "pipx", "ubuntu": "pipx", "arch": "yay" }, + "arch_name": "python-poetry", + "priority": "medium", + "optional": false, + "description": "Python project and dependency manager" + }, + { + "name": "codeburn", + "package_manager": { "macos": "pnpm", "ubuntu": "pnpm", "arch": "pnpm" }, + "priority": "medium", + "optional": false, + "description": "Track AI coding token usage by task, tool, model, and project" + }, + { + "name": "mac-mouse-fix", + "package_manager": { "macos": "brew-cask" }, + "priority": "medium", + "optional": false, + "description": "Decouples mouse scroll speed from trackpad settings" + }, + { + "name": "rectangle", + "package_manager": { "macos": "brew-cask" }, + "priority": "medium", + "optional": false, + "description": "Open source window management" + }, + { + "name": "caffeine", + "package_manager": { "macos": "brew-cask" }, + "priority": "medium", + "optional": false, + "description": "Simple menubar toggle to keep Mac awake" + }, + { + "name": "google-drive", + "package_manager": { "macos": "brew-cask" }, + "priority": "medium", + "optional": false, + "description": "Google Drive desktop client" + }, + { + "name": "amazon-photos", + "package_manager": { "macos": "brew-cask" }, + "priority": "medium", + "optional": false, + "description": "Unlimited full-resolution photo backup for Amazon Prime members" + }, + { + "name": "notion", + "package_manager": { "macos": "brew-cask" }, + "priority": "medium", + "optional": false, + "description": "Notes and project management" + }, + { + "name": "notion", + "package_manager": { "ubuntu": "snap", "arch": "yay" }, + "ubuntu_name": "notion-snap-reborn", + "arch_name": "notion-app-electron", + "priority": "low", + "optional": true, + "environment": ["personal"], + "description": "Notes and project management — unofficial snap/AUR packages on Linux" + }, + { + "name": "obsidian", + "package_manager": { "macos": "brew-cask", "ubuntu": "snap", "arch": "yay" }, + "priority": "medium", + "optional": false, + "install_command": "sudo snap install obsidian --classic", + "description": "Markdown knowledge base — official snap requires --classic for vault access" + }, + { + "name": "anki", + "package_manager": { "macos": "brew-cask", "ubuntu": "custom", "arch": "yay" }, + "priority": "medium", + "optional": false, + "install_command": { "ubuntu": "Download the installer from https://apps.ankiweb.net (apt's anki is years stale)" }, + "description": "Spaced-repetition flashcards" + }, + { + "name": "Spark Mail – AI Email Inbox", + "macos_name": "Spark Desktop", + "package_manager": { "macos": "app-store" }, + "priority": "medium", + "optional": false, + "description": "Smart email client with AI features" + }, + { + "name": "Photomator", + "package_manager": { "macos": "app-store" }, + "priority": "medium", + "optional": false, + "description": "Photo editor — free version with pay-once upgrade" + }, + { + "name": "net-tools", + "package_manager": { "ubuntu": "apt", "arch": "yay", "server": "apt" }, + "priority": "low", + "optional": true, + "description": "Legacy network utilities — ifconfig, netstat, route" + }, + { + "name": "poppler", + "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay" }, + "ubuntu_name": "poppler-utils", + "priority": "low", + "optional": true, + "description": "PDF CLI tools — pdftotext, pdfimages, pdfinfo, pdftoppm" + }, + { + "name": "ffmpeg", + "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, + "priority": "low", + "optional": true, + "description": "Audio/video processing toolkit" + }, + { + "name": "exiftool", + "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, + "ubuntu_name": "libimage-exiftool-perl", + "arch_name": "perl-image-exiftool", + "server_name": "libimage-exiftool-perl", + "priority": "low", + "optional": true, + "description": "Read and write metadata in photos, videos, and other files" + }, + { + "name": "tesseract", + "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay" }, + "ubuntu_name": "tesseract-ocr", + "priority": "low", + "optional": true, + "description": "OCR engine — extract text from images and scanned documents" + }, + { + "name": "smartmontools", + "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, + "priority": "low", + "optional": true, + "description": "Monitor SSD/HDD health via S.M.A.R.T. data — use: smartctl -a /dev/sdX" + }, + { + "name": "nmap", + "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, + "priority": "low", + "optional": true, + "description": "Network scanner — discover hosts and open ports on the local network" + }, + { + "name": "samba", + "package_manager": { "server": "apt" }, + "priority": "low", + "optional": true, + "description": "SMB/CIFS file sharing — exposes network shares to macOS and Windows; requires manual smb.conf configuration after install" + }, + { + "name": "gogcli", + "package_manager": { "macos": "brew" }, + "priority": "low", + "optional": true, + "description": "Google Workspace CLI — Gmail, Calendar, Drive, Docs, and more in the terminal" + }, + { + "name": "fluidtop", + "package_manager": { "macos": "pipx" }, + "priority": "low", + "optional": true, + "description": "Apple Silicon performance monitor with AI workload focus — alternative to mactop" + }, + { + "name": "exifread", + "package_manager": { "macos": "pipx" }, + "priority": "low", + "optional": true, + "description": "Python CLI for reading EXIF metadata from JPEG and TIFF photos" + }, + { + "name": "Goodnotes", + "package_manager": { "macos": "app-store" }, + "priority": "low", + "optional": true, + "description": "Handwriting and note-taking app with Apple Pencil support" + }, + { + "name": "spotify", + "package_manager": { "ubuntu": "snap", "arch": "yay" }, + "priority": "low", + "optional": true, + "environment": ["personal"], + "description": "Music streaming" + }, + { + "name": "steam", + "package_manager": { "ubuntu": "apt", "arch": "yay" }, + "priority": "low", + "optional": true, + "environment": ["personal"], + "description": "Game library and storefront" + }, + { + "name": "bolt-launcher", + "package_manager": { "arch": "yay" }, + "priority": "low", + "optional": true, + "environment": ["personal"], + "description": "Open-source RuneScape/OSRS launcher for Linux — replaces Jagex Launcher" + }, + { + "name": "octave", + "package_manager": { "macos": "brew" }, + "priority": "none", + "optional": true, + "description": "MATLAB-compatible numerical computing environment" + }, + { + "name": "octave", + "package_manager": { "ubuntu": "apt", "arch": "yay" }, + "priority": "none", + "optional": true, + "environment": ["work"], + "description": "MATLAB-compatible numerical computing environment" + }, + { + "name": "qemu", + "package_manager": { "macos": "brew" }, + "priority": "none", + "optional": true, + "description": "Machine emulator — run x86, ARM, and other architectures on Apple Silicon" + }, + { + "name": "mongodb-community", + "package_manager": { "macos": "custom" }, + "priority": "none", + "optional": true, + "install_command": "brew tap mongodb/brew && brew install mongodb-community", + "description": "MongoDB Community Server — includes mongosh and mongodb-database-tools" + }, + { + "name": "Pixelmator Pro", + "package_manager": { "macos": "app-store" }, + "priority": "none", + "optional": true, + "description": "Photo editor — pay once" + }, + { + "name": "Amphetamine", + "package_manager": { "macos": "app-store" }, + "priority": "none", + "optional": true, + "description": "Keep Mac awake with scheduling and app triggers — more powerful alternative to Caffeine" + } +] diff --git a/platforms/arch.sh b/platforms/arch.sh new file mode 100755 index 0000000..f7d00cb --- /dev/null +++ b/platforms/arch.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# Arch (CachyOS/Manjaro/…) desktop quirks: yay bootstrap via pacman, hardened +# yay batch installs, pacman -T pyenv build deps, systemd service enables. + +# Bootstrap the AUR helper (yay lives in the CachyOS/Arch repos, so pacman +# installs it directly — the one place pacman is required instead of yay). +ensure_yay() { + if ! command -v yay &>/dev/null; then + printf '==> Bootstrapping yay (via pacman)...\n' + run sudo pacman -S --needed --noconfirm base-devel git yay + else + printf '==> yay already installed (%s)\n' "$(yay --version 2>/dev/null | head -1)" + fi +} + +platform_bootstrap() { + # yay covers repo + AUR, so no external repos needed. + ensure_yay + if ! command -v jq &>/dev/null; then + printf '==> Bootstrapping jq...\n' + run sudo pacman -S --needed --noconfirm jq + fi +} + +# Install repo + AUR packages via yay (handles both uniformly). +# If the batch fails (e.g. a single AUR build breaks), retry one package at a +# time so one failure can't abort the whole run (and skip shell/config steps). +yay_install_tier() { + local priority="$1" names pkg + names=$(pkg_names yay "$priority") + [[ -z "$names" ]] && return 0 + # shellcheck disable=SC2086 + if ! run yay -S --needed --noconfirm $names; then + printf ' batch install errored — retrying packages individually...\n' + for pkg in $names; do + run yay -S --needed --noconfirm "$pkg" || printf ' ✗ failed: %s\n' "$pkg" + done + fi +} + +platform_install_tier() { + case "$1" in + high) + printf '==> Installing high-priority packages (yay)...\n' + yay_install_tier high + ;; + medium) + printf '==> Installing medium-priority packages (yay)...\n' + yay_install_tier medium + # Arch ships bat and fd under their real names — no symlinks needed. + ;; + low) + yay_install_tier low + ;; + esac +} + +platform_pyenv_build_deps() { + # Only install deps not already satisfied. CachyOS ships zlib-ng-compat + # (which provides zlib), so explicitly requesting the zlib package would + # conflict; pacman -T treats it as already satisfied and skips it. + local _pyenv_deps=(base-devel openssl zlib xz tk) _pyenv_need=() _dep + for _dep in "${_pyenv_deps[@]}"; do + pacman -T "$_dep" >/dev/null 2>&1 || _pyenv_need+=("$_dep") + done + if [[ ${#_pyenv_need[@]} -gt 0 ]]; then + sudo pacman -S --needed --noconfirm "${_pyenv_need[@]}" + else + printf ' build deps already satisfied\n' + fi +} + +platform_tailscale_step() { + # tailscale was installed in the yay medium batch; just enable the daemon. + printf '\n==> Enabling tailscaled service...\n' + run sudo systemctl enable --now tailscaled +} + +platform_docker_optional() { + # docker was installed in the yay low batch; enable the daemon + group. + if command -v docker &>/dev/null || [[ "$DRY_RUN" == true ]]; then + printf '==> Enabling Docker...\n' + run sudo systemctl enable --now docker.service + run sudo usermod -aG docker "$USER" + printf ' Log out and back in for the docker group to take effect.\n' + fi +} + +platform_main() { + desktop_main +} diff --git a/platforms/macos.sh b/platforms/macos.sh new file mode 100755 index 0000000..c3f0050 --- /dev/null +++ b/platforms/macos.sh @@ -0,0 +1,269 @@ +#!/usr/bin/env bash +# macOS quirks: Homebrew bootstrap, brew/brew-cask/custom installs, +# expat-pinned pyenv build (Tahoe fix), App Store reminders. + +# Populated by brew_install_tier / brew_cask_install_tier; printed at the end. +BREW_FAILURES=() +BREW_TOTAL=0 + +brew_install_tier() { + local priority="$1" names name err reason + names=$(pkg_names brew "$priority") + [[ -z "$names" ]] && return 0 + for name in $names; do + if [[ "$DRY_RUN" == true ]]; then + printf ' [dry-run] brew install %s\n' "$name" + continue + fi + BREW_TOTAL=$((BREW_TOTAL + 1)) + if ! err=$(brew install "$name" 2>&1); then + reason=$(printf '%s\n' "$err" | grep -m1 'Error:' | sed 's/^Error: //') + [[ -z "$reason" ]] && reason=$(printf '%s\n' "$err" | tail -1) + BREW_FAILURES+=("brew/$name: $reason") + printf ' [FAIL] brew install %s — %s\n' "$name" "$reason" + fi + done +} + +brew_cask_install_tier() { + local priority="$1" names name err reason + names=$(pkg_names brew-cask "$priority") + [[ -z "$names" ]] && return 0 + # --adopt: take ownership of apps already in /Applications (manual installs) + # instead of hard-failing the whole tier (#31) + for name in $names; do + if [[ "$DRY_RUN" == true ]]; then + printf ' [dry-run] brew install --cask --adopt %s\n' "$name" + continue + fi + BREW_TOTAL=$((BREW_TOTAL + 1)) + if ! err=$(brew install --cask --adopt "$name" 2>&1); then + reason=$(printf '%s\n' "$err" | grep -m1 'Error:' | sed 's/^Error: //') + [[ -z "$reason" ]] && reason=$(printf '%s\n' "$err" | tail -1) + BREW_FAILURES+=("cask/$name: $reason") + printf ' [FAIL] brew install --cask %s — %s\n' "$name" "$reason" + fi + done +} + +mac_custom_install_tier() { + local priority="$1" + # shellcheck disable=SC2016 + jq -r --arg plat "$PLATFORM" --arg pr "$priority" \ + --arg w "$INCLUDE_WORK" --arg p "$INCLUDE_PERSONAL" \ + "$CORE_JQ_DEFS"'.[] | select( + .package_manager[$plat] == "custom" and .priority == $pr and + envok($w; $p) and (icfor($plat) != null) and .name != "nvm" + ) | icfor($plat)' "$PACKAGES_JSON" | + while read -r cmd; do + run_eval "$cmd" + done +} + +mac_pipx_install_tier() { + local priority="$1" names name + names=$(pkg_names pipx "$priority") + [[ -z "$names" ]] && return 0 + for name in $names; do + run pipx install "$name" + done +} + +print_app_store_reminders() { + local priority="$1" apps + # shellcheck disable=SC2016 + apps=$(jq -r --arg plat "$PLATFORM" --arg pr "$priority" \ + --arg w "$INCLUDE_WORK" --arg p "$INCLUDE_PERSONAL" \ + "$CORE_JQ_DEFS"'.[] | select( + .package_manager[$plat] == "app-store" and .priority == $pr and envok($w; $p) + ) | " - \(.name): \(.description)"' "$PACKAGES_JSON") + [[ -n "$apps" ]] && printf '%s\n' "$apps" + return 0 +} + +platform_main() { + # shellcheck disable=SC2034 # consumed by deploy_zshrc in lib/core.sh + CONFIG_SRC_DIR="$SETUP_ROOT/macOS" + # ── Homebrew ──────────────────────────────────────────────────────────────── + if ! command -v brew &>/dev/null; then + printf '==> Installing Homebrew...\n' + run /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + run_eval 'eval "$(/opt/homebrew/bin/brew shellenv)"' + fi + + # jq is system-provided on macOS; only install via brew if missing. + command -v jq &>/dev/null || run brew install jq + + # ── High-priority brew formulae (pyenv, expat, pipx) ─────────────────────── + printf '==> Installing high-priority brew formulae...\n' + brew_install_tier high + + # ── Python (pyenv) ────────────────────────────────────────────────────────── + # pyenv compiles Python from source using brew's expat, bypassing broken + # Homebrew Python bottles on macOS Tahoe (missing libexpat symbols). + local pyenv_python="$HOME/.pyenv/versions/$PYTHON_VERSION/bin/python3.12" + + if [[ "$DRY_RUN" == true ]]; then + printf ' [dry-run] pyenv install %s\n' "$PYTHON_VERSION" + printf ' [dry-run] pyenv global %s\n' "$PYTHON_VERSION" + elif ! pyenv versions 2>/dev/null | grep -q "$PYTHON_VERSION"; then + printf '==> Installing Python %s via pyenv...\n' "$PYTHON_VERSION" + LDFLAGS="-L/opt/homebrew/opt/expat/lib" \ + CPPFLAGS="-I/opt/homebrew/opt/expat/include" \ + PKG_CONFIG_PATH="/opt/homebrew/opt/expat/lib/pkgconfig" \ + pyenv install "$PYTHON_VERSION" + pyenv global "$PYTHON_VERSION" + fi + + if [[ "$DRY_RUN" == false ]]; then + export PYENV_ROOT="$HOME/.pyenv" + export PATH="$PYENV_ROOT/bin:$PATH" + eval "$(pyenv init -)" + export PIPX_DEFAULT_PYTHON="$pyenv_python" + fi + + # ── Node (nvm) ────────────────────────────────────────────────────────────── + # nvm installed via curl — brew install nvm causes PATH issues. + # nvm is kept last in .zshrc so it wins over brew's node. + if [[ "$DRY_RUN" == true ]]; then + printf ' [dry-run] install nvm via curl\n' + printf ' [dry-run] nvm install lts/* && nvm alias default lts/*\n' + elif [[ ! -d "$HOME/.nvm" ]]; then + printf '==> Installing nvm...\n' + eval "$(custom_cmd nvm)" + fi + + if [[ "$DRY_RUN" == false ]]; then + export NVM_DIR="$HOME/.nvm" + [[ -s "$NVM_DIR/nvm.sh" ]] && \. "$NVM_DIR/nvm.sh" + nvm install 'lts/*' + nvm alias default 'lts/*' + nvm use default + fi + + # Apply the supply-chain cooldown before any package install runs below. + configure_npm_cooldown + configure_pnpm + + # ── Medium-priority brew casks ────────────────────────────────────────────── + printf '==> Installing brew casks...\n' + brew_cask_install_tier medium + + # ── Claude Code ───────────────────────────────────────────────────────────── + # Official curl installer instead of brew cask — self-updates in place (#39). + claude_code_step + + # ── Ghostty config ────────────────────────────────────────────────────────── + # Written to the XDG path (~/.config/ghostty/) as plain `config`, matching + # the Linux deploy — Ghostty supports XDG on macOS too. macOS ALSO loads + # ~/Library/Application Support/com.mitchellh.ghostty/ after XDG with later + # values overriding, and either location may carry a pre-1.2.3 `config` or a + # 1.2.3+ `config.ghostty` — rename all of them so the deployed file is the + # single source of truth. + local ghostty_cfg_dir="${XDG_CONFIG_HOME:-$HOME/.config}/ghostty" + local ghostty_cfg="$ghostty_cfg_dir/config" + local macos_ghostty_dir="$HOME/Library/Application Support/com.mitchellh.ghostty" + local stale + + if [[ "$DRY_RUN" == true ]]; then + printf ' [dry-run] mkdir -p %s\n' "$ghostty_cfg_dir" + [[ -f "$ghostty_cfg" ]] && printf ' [dry-run] backup %s → %s.bak\n' "$ghostty_cfg" "$ghostty_cfg" + printf ' [dry-run] cp ghostty.config → %s\n' "$ghostty_cfg" + for stale in "$ghostty_cfg_dir/config.ghostty" "$macos_ghostty_dir/config.ghostty" "$macos_ghostty_dir/config"; do + [[ -f "$stale" ]] && printf ' [dry-run] rename conflicting config %s → %s.bak\n' "$stale" "$stale" + done + else + mkdir -p "$ghostty_cfg_dir" + [[ -f "$ghostty_cfg" ]] && cp "$ghostty_cfg" "${ghostty_cfg}.bak" + cp "$SETUP_ROOT/dotfiles/ghostty.config" "$ghostty_cfg" + printf '==> Ghostty config written to %s\n' "$ghostty_cfg" + for stale in "$ghostty_cfg_dir/config.ghostty" "$macos_ghostty_dir/config.ghostty" "$macos_ghostty_dir/config"; do + if [[ -f "$stale" ]]; then + mv "$stale" "${stale}.bak" + printf '==> Renamed conflicting Ghostty config %s → .bak\n' "$stale" + fi + done + fi + + # ── tmux config ───────────────────────────────────────────────────────────── + # New with the dotfiles consolidation — the legacy macOS setup never deployed + # a tmux config, so Macs were stuck with the default green status bar. + printf '\n' + deploy_config "$SETUP_ROOT/dotfiles/tmux.conf" "$HOME/.tmux.conf" "tmux.conf" yes + + # ── zshrc + antidote plugins ──────────────────────────────────────────────── + # The shared dotfiles zshrc replaces the legacy append-lines approach; the + # previous ~/.zshrc is backed up. macOS bits in it are guarded on + # /opt/homebrew and $OSTYPE. + printf '\n' + deploy_zshrc + printf '\n' + deploy_config "$SETUP_ROOT/dotfiles/zsh_plugins.txt" "$HOME/.zsh_plugins.txt" "" no + printf '\n' + deploy_config "$SETUP_ROOT/dotfiles/p10k.zsh.example" "$HOME/.p10k.zsh" "p10k.zsh.example" yes + printf '\n' + + # ── Git: GPG signing ──────────────────────────────────────────────────────── + run git config --global gpg.program /opt/homebrew/bin/gpg + + # ── Medium-priority pipx packages ─────────────────────────────────────────── + printf '==> Installing pipx packages...\n' + if [[ "$DRY_RUN" == false ]]; then + rm -rf ~/.local/pipx/shared + pipx ensurepath + fi + mac_pipx_install_tier medium + + # ── Medium-priority pnpm packages ─────────────────────────────────────────── + printf '==> Installing pnpm packages...\n' + export PNPM_HOME="$HOME/.local/share/pnpm" + export PATH="$PNPM_HOME/bin:$PATH" + if [[ "$DRY_RUN" == true ]] || command -v pnpm &>/dev/null; then + pnpm_install_tier medium + + # Post-install: codeburn menubar (macOS native Swift app) + run codeburn menubar + else + printf ' pnpm not found — skipping (run corepack enable)\n' + fi + + # ── Medium-priority brew formulae ─────────────────────────────────────────── + printf '==> Installing brew formulae...\n' + brew_install_tier medium + + # ── Optional low-priority packages (--optional flag) ──────────────────────── + # priority "none" packages are never auto-installed regardless of flags + if [[ "$INCLUDE_OPTIONAL" == true ]]; then + printf '==> Installing optional (low) packages...\n' + brew_install_tier low + mac_custom_install_tier low + brew_cask_install_tier low + mac_pipx_install_tier low + command -v pnpm &>/dev/null && pnpm_install_tier low + fi + + # ── App Store reminders ───────────────────────────────────────────────────── + printf '\n' + printf 'Install these manually from the App Store:\n' + print_app_store_reminders medium + [[ "$INCLUDE_OPTIONAL" == true ]] && print_app_store_reminders low + + printf '\n' + printf 'Optional — run these from the repo root as needed:\n' + print_related_scripts + + if [[ "$DRY_RUN" == false && $BREW_TOTAL -gt 0 ]]; then + local ok=$((BREW_TOTAL - ${#BREW_FAILURES[@]})) + printf '\n==> Brew install summary: %d/%d succeeded\n' "$ok" "$BREW_TOTAL" + if [[ ${#BREW_FAILURES[@]} -gt 0 ]]; then + printf ' Failed:\n' + for f in "${BREW_FAILURES[@]}"; do + printf ' - %s\n' "$f" + done + printf ' Run setup.sh again after resolving the above.\n' + fi + fi + + printf '\n' + [[ "$DRY_RUN" == true ]] && printf 'Dry run complete — nothing was installed.\n' || printf 'Done! Restart your terminal or open a new tab.\n' +} diff --git a/platforms/server.sh b/platforms/server.sh new file mode 100755 index 0000000..bbdca3a --- /dev/null +++ b/platforms/server.sh @@ -0,0 +1,221 @@ +#!/usr/bin/env bash +# Headless server (Ubuntu Server LTS) profile: apt + snap, no GUI packages, +# SSH/Tailscale/Docker services, homepage dashboard stack. +# (The Raspberry Pi — Debian proper, no snapd — is a future target; TODO.md.) + +CONFIG_SRC_DIR="$SETUP_ROOT/linux-server" + +server_apt_install_tier() { + local priority="$1" names + names=$(pkg_names apt "$priority") + [[ -z "$names" ]] && return 0 + # shellcheck disable=SC2086 + run sudo apt install -y $names +} + +platform_main() { + apt_bootstrap + + printf '\n==> Installing high-priority packages...\n' + server_apt_install_tier high + + printf '\n==> Installing medium-priority packages...\n' + server_apt_install_tier medium + + printf '\n==> Installing snap packages...\n' + snap_install_tier medium + + if [[ "$INCLUDE_OPTIONAL" == true ]]; then + printf '\n==> Installing optional (low) packages...\n' + server_apt_install_tier low + fi + + setup_bat_fd_symlinks + set_default_shell + + printf '\n' + deploy_zshrc + printf '\n' + deploy_config "$SETUP_ROOT/dotfiles/tmux.conf" "$HOME/.tmux.conf" "tmux.conf" yes + printf '\n' + deploy_config "$CONFIG_SRC_DIR/zsh_plugins.txt" "$HOME/.zsh_plugins.txt" "linux-server/zsh_plugins.txt (platform override)" no + + # Pre-clone the plugins now while network is provably up; otherwise the + # first interactive login does the GitHub clones lazily. + printf '\n==> Pre-cloning antidote plugins...\n' + run zsh -c 'source /usr/share/zsh-antidote/antidote.zsh && antidote bundle <"$HOME/.zsh_plugins.txt" >/dev/null' + + # ── Tailscale ─────────────────────────────────────────────────────────────── + printf '\n' + if ! command -v tailscale &>/dev/null; then + printf '==> Installing Tailscale...\n' + run_eval "$(custom_cmd tailscale)" + else + printf '==> Tailscale already installed (%s)\n' "$(tailscale version | head -1)" + fi + printf " Run 'sudo tailscale up' to authenticate and connect to your Tailnet.\n" + + # ── Docker ────────────────────────────────────────────────────────────────── + printf '\n' + if ! command -v docker &>/dev/null; then + printf '==> Installing Docker...\n' + run_eval "$(custom_cmd docker-ce)" + run sudo usermod -aG docker "$USER" + printf ' Docker installed. Log out and back in for the docker group to take effect.\n' + else + printf '==> Docker already installed (%s)\n' "$(docker --version | head -1)" + fi + + # ── Cockpit ───────────────────────────────────────────────────────────────── + printf '\n' + if systemctl is-active --quiet cockpit.socket 2>/dev/null; then + printf '==> Cockpit already running\n' + else + printf '==> Enabling cockpit...\n' + run sudo systemctl enable --now cockpit.socket + fi + + # ── Tailscale web service ─────────────────────────────────────────────────── + printf '\n' + local service_dst="$HOME/.config/systemd/user/tailscale-web.service" + if [[ ! -f "$service_dst" ]] || ! diff -q "$CONFIG_SRC_DIR/tailscale-web.service" "$service_dst" &>/dev/null; then + printf '==> Installing Tailscale web service...\n' + run mkdir -p "$HOME/.config/systemd/user" + run cp "$CONFIG_SRC_DIR/tailscale-web.service" "$service_dst" + run systemctl --user daemon-reload + run systemctl --user enable --now tailscale-web + else + printf '==> Tailscale web service already installed\n' + fi + run loginctl enable-linger "$USER" + + # ── claude-code ───────────────────────────────────────────────────────────── + printf '\n' + if ! command -v claude &>/dev/null; then + printf '==> Installing claude-code...\n' + run_eval "$(custom_cmd claude-code)" + else + printf '==> claude-code already installed\n' + fi + + # ── AdGuard: free port 53 ─────────────────────────────────────────────────── + printf '\n' + local resolved_conf="/etc/systemd/resolved.conf" + if ! grep -q "^DNSStubListener=no" "$resolved_conf" 2>/dev/null; then + printf '==> Freeing port 53 for AdGuard...\n' + run sudo sed -i 's/#DNSStubListener=yes/DNSStubListener=no/' "$resolved_conf" + run sudo systemctl restart systemd-resolved + else + printf '==> Port 53 already free for AdGuard\n' + fi + + # ── Docker .env files ─────────────────────────────────────────────────────── + printf '\n' + printf '==> Setting up Docker service .env files...\n' + local svc + for svc in homepage speedtest-tracker filebrowser tailscale-proxy; do + if [[ -f "$CONFIG_SRC_DIR/$svc/.env.example" && ! -f "$CONFIG_SRC_DIR/$svc/.env" ]]; then + run cp "$CONFIG_SRC_DIR/$svc/.env.example" "$CONFIG_SRC_DIR/$svc/.env" + printf ' created %s/.env\n' "$svc" + else + printf ' ✓ %s/.env\n' "$svc" + fi + done + + # Fill derivable values into homepage/.env (replace placeholders or append if missing) + local homepage_env="$CONFIG_SRC_DIR/homepage/.env" + if [[ -f "$homepage_env" ]]; then + _fill_env() { + local key="$1" val="$2" + if [[ "$DRY_RUN" == true ]]; then + printf ' [dry-run] %s → %s\n' "$key" "$val" + return + fi + if grep -q "^$key=" "$homepage_env"; then + sed -i "s|^$key=.*|$key=$val|" "$homepage_env" + else + echo "$key=$val" >> "$homepage_env" + fi + printf ' %s → %s\n' "$key" "$val" + } + + _fill_env HOSTNAME "$(hostname)" + _fill_env SERVER_IP "$(hostname -I | awk '{print $1}')" + + if command -v tailscale &>/dev/null; then + local ts_hostname + ts_hostname=$(tailscale status --json 2>/dev/null | jq -r '.Self.DNSName // empty' | sed 's/\.$//') + if [[ -n "$ts_hostname" ]]; then + _fill_env TAILSCALE_HOSTNAME "$ts_hostname" + else + printf " TAILSCALE_HOSTNAME → (skipped — run 'tailscale up' then re-run setup.sh)\n" + fi + fi + fi + + # Auto-generate APP_KEY for speedtest-tracker + if [[ -f "$CONFIG_SRC_DIR/speedtest-tracker/.env" ]]; then + if ! grep -qE "^APP_KEY=base64:.+" "$CONFIG_SRC_DIR/speedtest-tracker/.env" 2>/dev/null; then + if [[ "$DRY_RUN" == true ]]; then + printf ' [dry-run] generate APP_KEY in speedtest-tracker/.env\n' + else + local app_key + app_key="base64:$(openssl rand -base64 32)" + sed -i "s|^APP_KEY=.*|APP_KEY=$app_key|" "$CONFIG_SRC_DIR/speedtest-tracker/.env" + printf ' auto-generated APP_KEY for speedtest-tracker\n' + fi + fi + fi + + # ── Docker services ───────────────────────────────────────────────────────── + printf '\n' + printf '==> Starting Docker services...\n' + local svc_dir + for svc in homepage portainer glances speedtest-tracker filebrowser watchtower \ + uptime-kuma nginx-proxy-manager ntfy syncthing adguard tailscale-proxy; do + svc_dir="$CONFIG_SRC_DIR/$svc" + if [[ -d "$svc_dir" && -f "$svc_dir/docker-compose.yml" ]]; then + printf ' %s...\n' "$svc" + if ! run sudo docker compose -f "$svc_dir/docker-compose.yml" up -d; then + printf ' Warning: %s failed to start\n' "$svc" + fi + fi + done + + # ── Manual install reminders ──────────────────────────────────────────────── + custom_reminders_section + + # ── Done ──────────────────────────────────────────────────────────────────── + printf '\n' + if [[ "$DRY_RUN" == true ]]; then + printf 'Dry run complete — nothing was installed.\n' + return 0 + fi + printf '================================================================\n' + printf ' Done. A few manual steps remain:\n' + printf '================================================================\n' + printf '\n' + printf " 1. Log out and back in — activates zsh and 'docker' without sudo\n" + printf '\n' + printf ' 2. Authenticate Tailscale:\n' + printf ' sudo tailscale up\n' + printf ' sudo tailscale set --operator=$USER\n' + printf '\n' + printf ' 3. SSH / GPG keys:\n' + printf ' bash SSH_and_GPG/create_ssh_key.sh\n' + printf ' bash SSH_and_GPG/create_gpg_key.sh\n' + printf '\n' + printf ' 4. Authenticate Tailscale, then re-run setup.sh to auto-fill TAILSCALE_HOSTNAME:\n' + printf ' sudo tailscale up\n' + printf ' bash setup.sh --profile server\n' + printf ' Then fill in any remaining values in linux-server/homepage/.env and restart:\n' + printf ' cd linux-server/homepage && docker compose restart\n' + printf '\n' + printf '================================================================\n' + printf '\n' + if command -v bat &>/dev/null; then + bat "$CONFIG_SRC_DIR/post-install.md" + else + cat "$CONFIG_SRC_DIR/post-install.md" + fi +} diff --git a/platforms/ubuntu.sh b/platforms/ubuntu.sh new file mode 100755 index 0000000..1a5b851 --- /dev/null +++ b/platforms/ubuntu.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# Ubuntu/Debian desktop quirks: apt + snap, external apt repos, batcat/fdfind +# symlinks, curl-based Tailscale/Docker installs. + +platform_bootstrap() { + apt_bootstrap +} + +apt_install_tier() { + local priority="$1" names + names=$(pkg_names apt "$priority") + [[ -z "$names" ]] && return 0 + # shellcheck disable=SC2086 + run sudo apt install -y $names +} + +platform_install_tier() { + case "$1" in + high) + printf '==> Installing high-priority apt packages...\n' + apt_install_tier high + ;; + medium) + printf '==> Installing medium-priority apt packages...\n' + apt_install_tier medium + printf '\n==> Installing snap packages...\n' + snap_install_tier medium + setup_bat_fd_symlinks + ;; + low) + apt_install_tier low + snap_install_tier low + ;; + esac +} + +platform_pyenv_build_deps() { + sudo apt install -y libssl-dev libffi-dev libncurses-dev libreadline-dev \ + libbz2-dev libsqlite3-dev liblzma-dev zlib1g-dev tk-dev +} + +platform_tailscale_step() { + printf '\n' + if [[ "$DRY_RUN" == true ]]; then + command -v tailscale &>/dev/null \ + && printf '==> Tailscale already installed\n' \ + || printf ' [dry-run] eval: curl -fsSL https://tailscale.com/install.sh | sudo sh\n' + elif ! command -v tailscale &>/dev/null; then + printf '==> Installing Tailscale...\n' + eval "$(custom_cmd tailscale)" + else + printf '==> Tailscale already installed (%s)\n' "$(tailscale version | head -1)" + fi +} + +platform_docker_optional() { + if ! command -v docker &>/dev/null; then + printf '==> Installing Docker...\n' + run_eval "curl -fsSL https://get.docker.com | sudo sh" + run sudo usermod -aG docker "$USER" + printf ' Log out and back in for the docker group to take effect.\n' + else + printf '==> Docker already installed (%s)\n' "$(docker --version | head -1)" + fi +} + +platform_main() { + desktop_main +} diff --git a/scripts/dryrun-smoke.sh b/scripts/dryrun-smoke.sh new file mode 100755 index 0000000..bbc071e --- /dev/null +++ b/scripts/dryrun-smoke.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Post-unification smoke gate (UNIFICATION.md Phase 4): the root setup.sh must +# complete a --dry-run on every platform and emit install actions. Replaces the +# legacy-parity gates deleted in Phase 4 (the per-folder scripts are shims now, +# so there is nothing left to compare against); catches engine and +# platform-module breakage on the platforms CI can't run live. +# +# A stub `brew` is put on PATH so the macOS Homebrew bootstrap (which curls the +# install script even in dry-run) is skipped. +# +# Usage: bash scripts/dryrun-smoke.sh + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +STUB_DIR="$(mktemp -d)" +trap 'rm -rf "$STUB_DIR"' EXIT +printf '#!/usr/bin/env bash\nexit 0\n' > "$STUB_DIR/brew" +chmod +x "$STUB_DIR/brew" + +FAILURES=0 +for platform in macos ubuntu arch server; do + out="" + if ! out="$(PATH="$STUB_DIR:$PATH" bash "$REPO_ROOT/setup.sh" \ + --dry-run --optional --work --personal --platform "$platform" 2>&1)"; then + printf 'FAIL %s: setup.sh exited non-zero; last lines:\n' "$platform" >&2 + tail -n 5 <<< "$out" | sed 's/^/ /' >&2 + FAILURES=$((FAILURES + 1)) + continue + fi + n="$(grep -c '\[dry-run\]' <<< "$out" || true)" + if (( n < 10 )); then + printf 'FAIL %s: only %d dry-run actions (expected a full install plan)\n' \ + "$platform" "$n" >&2 + FAILURES=$((FAILURES + 1)) + else + printf 'ok %s: %d dry-run actions\n' "$platform" "$n" + fi +done + +if (( FAILURES > 0 )); then + printf 'dryrun-smoke: FAILED (%d platforms)\n' "$FAILURES" >&2 + exit 1 +fi +printf 'dryrun-smoke: PASSED — root setup.sh dry-runs clean on all four platforms\n' diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..f4109d0 --- /dev/null +++ b/setup.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Unified setup entrypoint (UNIFICATION.md, issue #36). +# Usage: bash setup.sh [--optional] [--work] [--personal] [--dry-run] +# [--platform ] [--profile ] +# --optional also install low-priority optional packages +# --work also install work-only packages +# --personal also install personal-only packages +# --dry-run print all commands without executing anything +# --platform

force platform; default: auto-detect (uname / /etc/os-release). +# --distro is accepted as an alias. +# --profile server headless server profile (apt only, no GUI packages); +# never auto-detected — Debian desktops and the Pi look alike. +# +# Package data lives in packages.json; the engine is lib/core.sh; per-platform +# quirks live in platforms/.sh. + +set -euo pipefail + +SETUP_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# shellcheck source=lib/core.sh +source "$SETUP_ROOT/lib/core.sh" + +core_parse_args "$@" +core_detect_platform + +# shellcheck source=/dev/null +source "$SETUP_ROOT/platforms/$PLATFORM.sh" + +platform_main diff --git a/verify.sh b/verify.sh new file mode 100755 index 0000000..e036019 --- /dev/null +++ b/verify.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Unified read-only install verification (UNIFICATION.md Phase 3, issue #36). +# Usage: bash verify.sh [--optional] [--work] [--personal] [--all] +# [--platform ] +# --optional also check low-priority optional packages +# --work also check work-only packages +# --personal also check personal-only packages +# --all check everything (implies --optional --work --personal +# + priority "none") +# --platform

force platform; default: auto-detect (uname / +# /etc/os-release). --distro is accepted as an alias. +# +# Mirrors setup.sh's selection logic, so the packages checked here match what +# `setup.sh` with the same flags would install. Read-only — installs nothing. + +set -uo pipefail + +SETUP_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# shellcheck source=lib/core.sh +source "$SETUP_ROOT/lib/core.sh" +# shellcheck source=lib/verify.sh +source "$SETUP_ROOT/lib/verify.sh" + +verify_parse_args "$@" +core_detect_platform + +verify_main