diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 10509c4..12b28cf 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -35,6 +35,14 @@ jobs: done exit $failed + validate-packages: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Validate packages.json schema + run: bash scripts/validate-packages.sh + dryrun-smoke: runs-on: ubuntu-latest steps: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e869012..f4dd17a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,6 +14,13 @@ repos: types: [file] files: (\.zsh$|zshrc\.example$) + - id: validate-packages + name: validate packages.json schema + language: system + entry: bash scripts/validate-packages.sh + files: ^packages\.json$ + pass_filenames: false + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: diff --git a/CLAUDE.md b/CLAUDE.md index 5eca18e..717c4d0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,8 +8,18 @@ tracked in TODO.md.) ## Entrypoints - `setup.sh` — installs everything for the detected platform. - Flags: `--optional --work --personal --dry-run --platform --profile `. + Flags: `--optional --work --personal --base --tags --dry-run --platform --profile `. The server platform is never auto-detected (`--profile server` or `--platform server` required). + Category selection: `--base` installs only the high-priority base set; `--tags + development,terminal` installs base + those `packages.json` tag categories + (enabled work/personal apps install regardless of category). Run bare on a TTY + with no selection flag and `core_maybe_prompt_selection` prompts interactively; + it is skipped on the server profile and in non-interactive/CI runs. The filter + is implemented by `tagok()` in `CORE_JQ_DEFS`, which reads `TAG_FILTER_ACTIVE` + and `SELECTED_TAGS` from the environment — inactive by default, so flag-driven + and CI runs are unchanged. The dedicated custom-install steps (tailscale, + claude-code, docker) gate on `pkg_selected` so they honor the selection too; + the server profile keeps the filter inactive, so they install as before there. - `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 @@ -19,10 +29,23 @@ tracked in TODO.md.) - `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`; + overrides the install token; `environment` gates 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. + - `priority`, `optional`, `environment`, and `install_command` each accept a + **scalar** (applies to every platform) **or a per-platform object** keyed by + platform (e.g. `"priority": { "macos": "medium", "ubuntu": "none" }`). The + engine resolves them via the `prfor`/`optfor`/`envfor`/`icfor` jq defs in + `lib/core.sh`. This is what lets one entry serve platforms that differ in + tier/optionality/gating, instead of splitting into duplicate entries. + - **`environment` caveat:** its scalar form is itself an *array* (`["work"]`), + so the per-platform form is detected as an *object* (`{ "ubuntu": ["work"] }`) + — array means legacy/all-platforms, object means per-platform. Keep the + per-platform value an object-of-arrays. + - `tags` — required non-empty array of descriptive categories from the + controlled vocabulary in `scripts/validate-packages.sh`. Metadata only + (grouping/docs); the install engine ignores them. - `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 @@ -47,7 +70,10 @@ phases. ## Conventions - Pre-commit runs `shellcheck --severity=warning` on all shell scripts; - `zsh -n` checks `.zsh` files and `zshrc.example`. + `zsh -n` checks `.zsh` files and `zshrc.example`; `scripts/validate-packages.sh` + enforces the `packages.json` schema (platform vocabulary, controlled tag set, + and the "no silent drop" rule — every platform a package targets must resolve a + valid priority tier and a boolean optional). All three also run in CI. - 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. diff --git a/README.md b/README.md index 6c77b53..dc2d566 100644 --- a/README.md +++ b/README.md @@ -75,8 +75,8 @@ For OS-specific software and install instructions, see: 1. Official GitHub CLI — create PRs, manage issues, clone repos, and run Actions from the terminal 3. pyenv | macOS · Linux | [GitHub](https://github.com/pyenv/pyenv) | [brew](https://formulae.brew.sh/formula/pyenv#default) 1. Python version and virtual environment manager -4. poetry | macOS · Linux · Windows | [GitHub](https://github.com/python-poetry/poetry) - 1. Python project manager +4. uv | macOS · Linux · Windows | [Docs](https://docs.astral.sh/uv/) | [GitHub](https://github.com/astral-sh/uv) | [brew](https://formulae.brew.sh/formula/uv) + 1. Fast Python package and project manager — pip/venv/pyenv successor 5. pipx | macOS · Linux · Windows | [GitHub](https://github.com/pypa/pipx) | [brew](https://formulae.brew.sh/formula/pipx) 1. Install Python CLI tools in isolated virtual environments so they don't pollute the global Python install 6. git-lfs | macOS · Linux · Windows | [Download](https://git-lfs.com/) | [GitHub](https://github.com/git-lfs/git-lfs) | [brew](https://formulae.brew.sh/formula/git-lfs) diff --git a/lib/core.sh b/lib/core.sh index c89cb73..6aa19a0 100755 --- a/lib/core.sh +++ b/lib/core.sh @@ -21,12 +21,34 @@ INCLUDE_WORK=false INCLUDE_PERSONAL=false DRY_RUN=false +# Category selection. When TAG_FILTER_ACTIVE is "true" the engine installs only +# the base set (high-priority foundational packages), enabled work/personal +# apps, and packages whose tags intersect SELECTED_TAGS — see tagok() in +# CORE_JQ_DEFS, which reads both from the environment. SELECTION_EXPLICIT tracks +# whether the user passed any selection flag, so a bare interactive run knows to +# prompt instead. +SELECTION_EXPLICIT=false +TAG_FILTER_ACTIVE=false +SELECTED_TAGS="[]" +export TAG_FILTER_ACTIVE SELECTED_TAGS + core_parse_args() { while [[ $# -gt 0 ]]; do case "$1" in - --optional) INCLUDE_OPTIONAL=true ;; - --work) INCLUDE_WORK=true ;; - --personal) INCLUDE_PERSONAL=true ;; + --optional) INCLUDE_OPTIONAL=true; SELECTION_EXPLICIT=true ;; + --work) INCLUDE_WORK=true; SELECTION_EXPLICIT=true ;; + --personal) INCLUDE_PERSONAL=true; SELECTION_EXPLICIT=true ;; + --base) + TAG_FILTER_ACTIVE=true; SELECTED_TAGS="[]"; SELECTION_EXPLICIT=true ;; + --tags) + if [[ -z "${2:-}" || "${2:-}" == -* ]]; then + printf 'ERROR: --tags requires a comma-separated list (e.g. development,terminal).\n' >&2 + exit 1 + fi + SELECTED_TAGS="$(core_csv_to_json "$2")" + [[ "$SELECTED_TAGS" == "[]" ]] && \ + printf 'WARNING: --tags resolved to no categories — installing base only.\n' >&2 + TAG_FILTER_ACTIVE=true; SELECTION_EXPLICIT=true; shift ;; --dry-run) DRY_RUN=true ;; --distro|--platform) if [[ -z "${2:-}" || "${2:-}" == -* ]]; then @@ -80,6 +102,120 @@ core_detect_platform() { if [[ "$PLATFORM" == "server" ]]; then SERVER_PROFILE=true; fi } +# ── Interactive category selection ──────────────────────────────────────────── + +# Newline-separated list of every tag category present in packages.json. +core_tag_vocabulary() { + jq -r '[.[].tags[]] | unique | .[]' "$PACKAGES_JSON" +} + +# "dev, terminal ,local-llm" -> ["dev","terminal","local-llm"] (built without jq +# so --tags works before the jq bootstrap; tags are simple [a-z-] tokens). +core_csv_to_json() { + local csv="$1" json="[" first=1 tok + local IFS=',' + local -a arr + read -ra arr <<< "$csv" + for tok in "${arr[@]}"; do + tok="${tok#"${tok%%[![:space:]]*}"}" + tok="${tok%"${tok##*[![:space:]]}"}" + [[ -z "$tok" ]] && continue + [[ "$first" -eq 1 ]] && first=0 || json+="," + json+="\"$tok\"" + done + json+="]" + printf '%s' "$json" +} + +# Prompt the user to pick categories when setup is run bare on a terminal. +# Sets SELECTED_TAGS / TAG_FILTER_ACTIVE and the INCLUDE_* toggles in place. +core_prompt_selection() { + if ! command -v jq >/dev/null; then + printf ' (jq not found — skipping interactive selection; installing the full default set)\n' + return 0 + fi + local -a cats=() + local cat line tok num i col=0 json first=1 + while IFS= read -r cat; do cats+=("$cat"); done \ + < <(core_tag_vocabulary) + + printf '\nNo options given — choose what to install.\n' + printf 'Base essentials (shell, git, core tools) are always included.\n\n' + printf 'Categories:\n' + for i in "${!cats[@]}"; do + printf ' %2d) %-18s' "$((i + 1))" "${cats[$i]}" + col=$((col + 1)) + [[ $((col % 3)) -eq 0 ]] && printf '\n' + done + [[ $((col % 3)) -ne 0 ]] && printf '\n' + printf '\nExtras: w) work apps p) personal apps o) optional low-priority\n' + printf '\nEnter numbers/letters separated by spaces (e.g. "2 5 w"), "all", or Enter for base only: ' + + IFS= read -r line || line="" + + if [[ "$line" == "all" || "$line" == "a" ]]; then + TAG_FILTER_ACTIVE=false + INCLUDE_WORK=true; INCLUDE_PERSONAL=true; INCLUDE_OPTIONAL=true + else + json="[" + # shellcheck disable=SC2086 # intentional word-splitting on the input line + for tok in $line; do + case "$tok" in + w|W) INCLUDE_WORK=true ;; + p|P) INCLUDE_PERSONAL=true ;; + o|O) INCLUDE_OPTIONAL=true ;; + ''|*[!0-9]*) printf ' (ignoring "%s")\n' "$tok" >&2 ;; + *) + num=$((10#$tok)) + if [[ "$num" -ge 1 && "$num" -le "${#cats[@]}" ]]; then + [[ "$first" -eq 1 ]] && first=0 || json+="," + json+="\"${cats[$((num - 1))]}\"" + else + printf ' (ignoring out-of-range "%s")\n' "$tok" >&2 + fi ;; + esac + done + json+="]" + SELECTED_TAGS="$json" + TAG_FILTER_ACTIVE=true + fi + export TAG_FILTER_ACTIVE SELECTED_TAGS + + printf '\n==> Installing: base' + if [[ "$TAG_FILTER_ACTIVE" == true && "$SELECTED_TAGS" != "[]" ]]; then + printf ' + %s' "$(printf '%s' "$SELECTED_TAGS" | jq -r 'join(", ")')" + fi + [[ "$INCLUDE_WORK" == true ]] && printf ' + work' + [[ "$INCLUDE_PERSONAL" == true ]] && printf ' + personal' + [[ "$INCLUDE_OPTIONAL" == true ]] && printf ' + optional' + [[ "$TAG_FILTER_ACTIVE" != true ]] && printf ' (full set)' + printf '\n' +} + +# Warn (don't fail) when --tags names a category that matches no package — tags +# are metadata, so a typo would otherwise silently install only the base set. +core_validate_tag_selection() { + [[ "$TAG_FILTER_ACTIVE" == true ]] || return 0 + [[ "$SELECTED_TAGS" == "[]" ]] && return 0 + command -v jq >/dev/null || return 0 + local vocab tag + vocab="$(core_tag_vocabulary 2>/dev/null)" || return 0 + while IFS= read -r tag; do + [[ -z "$tag" ]] && continue + grep -qxF -- "$tag" <<< "$vocab" \ + || printf 'WARNING: --tags category "%s" matches no packages (ignored).\n' "$tag" >&2 + done < <(printf '%s' "$SELECTED_TAGS" | jq -r '.[]') +} + +# Prompt only when run interactively with no selection flags; never on the +# headless server profile or in non-interactive/CI contexts. +core_maybe_prompt_selection() { + [[ "$SERVER_PROFILE" == true ]] && return 0 + [[ "$SELECTION_EXPLICIT" == true ]] && return 0 + [[ -t 0 ]] || return 0 + core_prompt_selection +} + # ── Run helpers ─────────────────────────────────────────────────────────────── run() { @@ -150,10 +286,21 @@ configure_pnpm() { # 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 prfor($plat): (.priority | if type == "object" then .[$plat] else . end); +def optfor($plat): (.optional | if type == "object" then .[$plat] else . end); +def envfor($plat): (.environment | if type == "object" then .[$plat] else . end); +def envok($plat; $w; $p): + (envfor($plat) as $e | + ($e == null) or + (($w == "true") and ($e | index("work"))) or + (($p == "true") and ($e | index("personal")))); +def tagok($plat): + if (env.TAG_FILTER_ACTIVE != "true") then true + elif (prfor($plat) == "high") then true + elif (envfor($plat) != null) then true + else ((env.SELECTED_TAGS // "[]" | fromjson) as $sel + | (.tags // []) | any(. as $t | ($sel | index($t)) != null)) + end; def icfor($plat): (.install_command | if type == "object" then .[$plat] else . end); def pname($plat): (.[$plat + "_name"] // .name); @@ -166,10 +313,28 @@ pkg_names() { 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) + .package_manager[$plat] == $m and prfor($plat) == $pr and envok($plat; $w; $p) and tagok($plat) ) | pname($plat)] | join(" ")' "$PACKAGES_JSON" } +# True (exit 0) when the named package is selected for the current platform +# under the active flags and tag filter — mirrors the tier/envok/tagok logic the +# install loops use (high/medium always, low only with --optional, none never). +# Lets the dedicated custom-install steps (tailscale, claude-code, docker) honor +# --base/--tags instead of always running. On the server profile the tag filter +# is inactive, so these resolve exactly as before. +pkg_selected() { + # shellcheck disable=SC2016 + jq -e --arg plat "$PLATFORM" --arg n "$1" \ + --arg w "$INCLUDE_WORK" --arg p "$INCLUDE_PERSONAL" --arg opt "$INCLUDE_OPTIONAL" \ + "$CORE_JQ_DEFS"'any(.[]; + .name == $n and .package_manager[$plat] != null + and envok($plat; $w; $p) and tagok($plat) + and (prfor($plat) as $pr + | $pr == "high" or $pr == "medium" or ($pr == "low" and $opt == "true")))' \ + "$PACKAGES_JSON" >/dev/null 2>&1 +} + # install_command of a single package for this platform. custom_cmd() { # shellcheck disable=SC2016 @@ -186,8 +351,8 @@ print_custom_reminders() { 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) + .package_manager[$plat] == "custom" and prfor($plat) == $pr and + (.handled_by_setup != true) and envok($plat; $w; $p) and tagok($plat) ) | " - \(.name)\n \(.description)\n Install: \(icfor($plat))"' \ "$PACKAGES_JSON") [[ -n "$items" ]] && printf '%s\n' "$items" @@ -224,7 +389,7 @@ pipx_install_tier() { 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) + .package_manager[$plat] == "pipx" and prfor($plat) == $pr and envok($plat; $w; $p) and tagok($plat) ) | (icfor($plat) // ("pipx install " + .name))' "$PACKAGES_JSON" | while read -r cmd; do run_eval "$cmd" @@ -237,7 +402,7 @@ pnpm_install_tier() { 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) + .package_manager[$plat] == "pnpm" and prfor($plat) == $pr and envok($plat; $w; $p) and tagok($plat) ) | .name] | join(" ")' "$PACKAGES_JSON") [[ -z "$names" ]] && return 0 # shellcheck disable=SC2086 @@ -316,9 +481,19 @@ 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 + # Bootstrap the essentials the rest of setup relies on before the package + # tiers run: curl/wget drive the nvm/pyenv/tailscale/claude install scripts and + # the apt-repo key fetches below; jq drives package selection. Fresh Ubuntu + # desktop ships none of curl/jq, so the first curl step (nvm) used to fail. + local boot=() b + for b in curl wget jq; do + command -v "$b" &>/dev/null || boot+=("$b") + done + if [[ "$DRY_RUN" == true ]]; then + printf ' [dry-run] ensure ca-certificates curl wget jq are present\n' + elif [[ ${#boot[@]} -gt 0 ]]; then + printf '==> Bootstrapping essentials: %s\n' "${boot[*]}" + run sudo apt install -y ca-certificates "${boot[@]}" fi local need_update=false @@ -381,16 +556,16 @@ snap_install_tier() { 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) + .package_manager[$plat] == "snap" and prfor($plat) == $pr and + envok($plat; $w; $p) and (icfor($plat) == null) and tagok($plat) ) | 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) + .package_manager[$plat] == "snap" and prfor($plat) == $pr and + envok($plat; $w; $p) and (icfor($plat) != null) and tagok($plat) ) | "\(.name)|\(icfor($plat))"' "$PACKAGES_JSON") if [[ -n "$regular_snaps" ]]; then @@ -444,6 +619,7 @@ platform_install_tier() { # Tailscale via the official curl installer — apt-family default (ubuntu + server). # arch.sh overrides this (tailscale ships in the yay batch; just enable the daemon). platform_tailscale_step() { + pkg_selected tailscale || return 0 printf '\n' if [[ "$DRY_RUN" == true ]]; then command -v tailscale &>/dev/null \ @@ -457,12 +633,14 @@ platform_tailscale_step() { fi } -# Docker via get.docker.com — apt-family default (ubuntu + server). -# arch.sh overrides this (docker ships in the yay batch; just enable the daemon). +# Docker — apt-family default (ubuntu + server); the install command comes from +# packages.json (docker's install_command) via custom_cmd, the post-install +# group step lives here. arch.sh overrides this (docker ships in the yay batch; +# just enable the daemon). 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_eval "$(custom_cmd docker)" run sudo usermod -aG docker "$USER" printf ' Log out and back in for the docker group to take effect.\n' else @@ -575,6 +753,7 @@ rust_toolchain_step() { } claude_code_step() { + pkg_selected claude-code || return 0 printf '\n' if [[ "$DRY_RUN" == true ]]; then command -v claude &>/dev/null \ @@ -733,7 +912,7 @@ linux_main() { platform_install_tier low pipx_install_tier low pnpm_install_tier low - platform_docker_optional + if pkg_selected docker; then platform_docker_optional; fi fi custom_reminders_section diff --git a/lib/verify.sh b/lib/verify.sh index 35103bd..74b0db2 100755 --- a/lib/verify.sh +++ b/lib/verify.sh @@ -106,8 +106,8 @@ verify_section() { 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' \ + .package_manager[$plat] != null and prfor($plat) == $pr and envok($plat; $w; $p) + ) | [.name, .package_manager[$plat], pname($plat), (optfor($plat) | tostring)] | @tsv' \ "$PACKAGES_JSON") [[ -z "$rows" ]] && return 0 diff --git a/macOS/README.md b/macOS/README.md index 5d384c7..5e6342a 100644 --- a/macOS/README.md +++ b/macOS/README.md @@ -4,10 +4,10 @@ 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`, `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. Packages: `gh`, `hf`, `git-lfs`, `git-xet`, `fastfetch`, `pipx`, `pyenv`, `uv`, `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 + 2. Packages: `fluidtop`, `exifread`, and more ## Development diff --git a/macOS/pipx_packages.md b/macOS/pipx_packages.md index 6233ffd..d68030b 100644 --- a/macOS/pipx_packages.md +++ b/macOS/pipx_packages.md @@ -2,9 +2,7 @@ Install via `pipx install `. Requires [pipx](https://formulae.brew.sh/formula/pipx). -1. poetry | [PyPI](https://pypi.org/project/poetry/) | [GitHub](https://github.com/python-poetry/poetry) - 1. Python project and dependency manager — handles virtualenvs, packaging, and publishing -2. fluidtop | [PyPI](https://pypi.org/project/fluidtop/) | [GitHub](https://github.com/FluidInference/fluidtop) +1. fluidtop | [PyPI](https://pypi.org/project/fluidtop/) | [GitHub](https://github.com/FluidInference/fluidtop) 1. Real-time Apple Silicon performance monitor with AI workload focus — enhanced alternative to asitop with M1/M2/M3/M4 support -3. exifread | [PyPI](https://pypi.org/project/exifread/) | [GitHub](https://github.com/ianare/exif-py) +2. exifread | [PyPI](https://pypi.org/project/exifread/) | [GitHub](https://github.com/ianare/exif-py) 1. Python library and CLI (`EXIF.py`) for reading EXIF metadata from JPEG and TIFF photos diff --git a/packages.json b/packages.json index 65d7cbb..c8bd271 100644 --- a/packages.json +++ b/packages.json @@ -1,6 +1,7 @@ [ { "name": "zsh", + "tags": ["terminal"], "package_manager": { "ubuntu": "apt", "arch": "yay", "server": "apt" }, "priority": "high", "optional": false, @@ -8,6 +9,7 @@ }, { "name": "zsh-antidote", + "tags": ["terminal"], "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, "macos_name": "antidote", "priority": "high", @@ -16,6 +18,7 @@ }, { "name": "zoxide", + "tags": ["terminal"], "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay" }, "priority": "high", "optional": false, @@ -23,6 +26,7 @@ }, { "name": "expat", + "tags": ["development"], "package_manager": { "macos": "brew" }, "priority": "high", "optional": false, @@ -30,6 +34,7 @@ }, { "name": "pyenv", + "tags": ["development"], "package_manager": { "macos": "brew", "ubuntu": "custom", "arch": "custom" }, "priority": "high", "optional": false, @@ -39,6 +44,7 @@ }, { "name": "nvm", + "tags": ["development"], "package_manager": { "macos": "custom", "ubuntu": "custom", "arch": "custom", "server": "custom" }, "priority": "high", "optional": false, @@ -48,6 +54,7 @@ }, { "name": "pipx", + "tags": ["development"], "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay" }, "arch_name": "python-pipx", "priority": "high", @@ -56,13 +63,15 @@ }, { "name": "uv", + "tags": ["development"], "package_manager": { "macos": "brew", "ubuntu": "pipx", "arch": "yay" }, - "priority": "medium", + "priority": "high", "optional": false, "description": "Fast Python package and project manager — pip/venv/pyenv successor" }, { "name": "build-essential", + "tags": ["development"], "package_manager": { "ubuntu": "apt", "arch": "yay", "server": "apt" }, "arch_name": "base-devel", "priority": "high", @@ -71,20 +80,15 @@ }, { "name": "git", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, - "priority": "high", + "tags": ["development"], + "package_manager": { "ubuntu": "apt", "arch": "yay", "server": "apt" }, + "priority": { "ubuntu": "high", "arch": "high", "server": "medium" }, "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", + "tags": ["development"], "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, "ubuntu_name": "rustup", "arch_name": "rustup", @@ -95,6 +99,7 @@ }, { "name": "go", + "tags": ["development"], "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay" }, "ubuntu_name": "golang-go", "priority": "medium", @@ -103,6 +108,7 @@ }, { "name": "ghostty", + "tags": ["terminal"], "package_manager": { "macos": "brew-cask", "ubuntu": "snap", "arch": "yay" }, "priority": "medium", "optional": false, @@ -111,6 +117,7 @@ }, { "name": "fastfetch", + "tags": ["terminal", "system-monitoring"], "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, "priority": "medium", "optional": false, @@ -118,6 +125,7 @@ }, { "name": "gh", + "tags": ["development"], "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, "arch_name": "github-cli", "priority": "medium", @@ -126,6 +134,7 @@ }, { "name": "forgejo-cli", + "tags": ["development"], "package_manager": { "macos": "brew", "ubuntu": "custom", "arch": "custom" }, "priority": "medium", "optional": false, @@ -134,6 +143,7 @@ }, { "name": "git-lfs", + "tags": ["development"], "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, "priority": "medium", "optional": false, @@ -141,22 +151,17 @@ }, { "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" }, + "tags": ["development", "local-llm"], + "package_manager": { "macos": "brew", "ubuntu": "custom", "arch": "custom" }, "priority": "medium", "optional": false, - "environment": ["personal"], + "environment": { "ubuntu": ["personal"], "arch": ["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", + "tags": ["development"], "package_manager": { "ubuntu": "pipx", "arch": "yay" }, "priority": "medium", "optional": false, @@ -164,6 +169,7 @@ }, { "name": "shellcheck", + "tags": ["development"], "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, "priority": "medium", "optional": false, @@ -171,6 +177,7 @@ }, { "name": "codespell", + "tags": ["development"], "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay" }, "priority": "medium", "optional": false, @@ -178,6 +185,7 @@ }, { "name": "pytest", + "tags": ["development"], "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, "ubuntu_name": "python3-pytest", "arch_name": "python-pytest", @@ -188,6 +196,7 @@ }, { "name": "swiftlint", + "tags": ["development"], "package_manager": { "macos": "brew" }, "priority": "medium", "optional": false, @@ -195,6 +204,7 @@ }, { "name": "swiftformat", + "tags": ["development"], "package_manager": { "macos": "brew" }, "priority": "medium", "optional": false, @@ -202,6 +212,7 @@ }, { "name": "xcodegen", + "tags": ["development"], "package_manager": { "macos": "brew" }, "priority": "medium", "optional": false, @@ -209,6 +220,7 @@ }, { "name": "gnupg", + "tags": ["security"], "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, "priority": "medium", "optional": false, @@ -216,22 +228,17 @@ }, { "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" }, + "tags": ["local-llm"], + "package_manager": { "macos": "brew", "ubuntu": "pipx", "arch": "pipx" }, "priority": "medium", "optional": false, - "environment": ["personal"], - "install_command": "pipx install huggingface-hub[cli]", + "environment": { "ubuntu": ["personal"], "arch": ["personal"] }, + "install_command": { "ubuntu": "pipx install huggingface-hub[cli]", "arch": "pipx install huggingface-hub[cli]" }, "description": "Hugging Face Hub CLI — download, upload, and manage ML models" }, { "name": "htop", + "tags": ["system-monitoring"], "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, "priority": "medium", "optional": false, @@ -239,21 +246,16 @@ }, { "name": "llama.cpp", - "package_manager": { "macos": "brew" }, - "priority": "medium", - "optional": false, + "tags": ["local-llm"], + "package_manager": { "macos": "brew", "ubuntu": "custom", "arch": "yay" }, + "priority": { "macos": "medium", "ubuntu": "none", "arch": "none" }, + "optional": { "macos": false, "ubuntu": true, "arch": true }, + "install_command": { "ubuntu": "See https://github.com/ggml-org/llama.cpp#build" }, "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", + "tags": ["local-llm"], "package_manager": { "macos": "brew" }, "priority": "medium", "optional": false, @@ -261,6 +263,7 @@ }, { "name": "mactop", + "tags": ["system-monitoring"], "package_manager": { "macos": "brew" }, "priority": "medium", "optional": false, @@ -268,6 +271,7 @@ }, { "name": "micro", + "tags": ["terminal"], "package_manager": { "macos": "brew", "ubuntu": "snap", "arch": "yay", "server": "snap" }, "priority": "medium", "optional": false, @@ -276,6 +280,7 @@ }, { "name": "nvtop", + "tags": ["system-monitoring"], "package_manager": { "macos": "brew", "ubuntu": "snap", "arch": "yay", "server": "snap" }, "priority": "medium", "optional": false, @@ -283,6 +288,7 @@ }, { "name": "lact", + "tags": ["system-monitoring"], "package_manager": { "arch": "yay" }, "priority": "medium", "optional": false, @@ -290,6 +296,7 @@ }, { "name": "sshpass", + "tags": ["networking"], "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, "priority": "medium", "optional": false, @@ -297,6 +304,7 @@ }, { "name": "tmux", + "tags": ["terminal"], "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, "priority": "medium", "optional": false, @@ -304,6 +312,7 @@ }, { "name": "eza", + "tags": ["terminal"], "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, "priority": "medium", "optional": false, @@ -311,6 +320,7 @@ }, { "name": "terminal-notifier", + "tags": ["desktop-utility"], "package_manager": { "macos": "brew" }, "priority": "medium", "optional": false, @@ -318,6 +328,7 @@ }, { "name": "fzf", + "tags": ["terminal"], "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, "priority": "medium", "optional": false, @@ -325,6 +336,7 @@ }, { "name": "ripgrep", + "tags": ["terminal"], "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, "priority": "medium", "optional": false, @@ -332,6 +344,7 @@ }, { "name": "bat", + "tags": ["terminal"], "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, "priority": "medium", "optional": false, @@ -339,6 +352,7 @@ }, { "name": "fd", + "tags": ["terminal"], "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, "ubuntu_name": "fd-find", "server_name": "fd-find", @@ -348,20 +362,15 @@ }, { "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" + "tags": ["development"], + "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, + "priority": { "macos": "low", "ubuntu": "medium", "arch": "medium", "server": "medium" }, + "optional": { "macos": true, "ubuntu": false, "arch": false, "server": false }, + "description": "JSON processor — required by setup.sh to parse this file; system-provided on macOS so the brew build is low-priority/optional there" }, { "name": "curl", + "tags": ["networking"], "package_manager": { "ubuntu": "apt", "arch": "yay", "server": "apt" }, "priority": "medium", "optional": false, @@ -369,6 +378,7 @@ }, { "name": "wget", + "tags": ["networking"], "package_manager": { "ubuntu": "apt", "arch": "yay", "server": "apt" }, "priority": "medium", "optional": false, @@ -376,6 +386,7 @@ }, { "name": "libnotify-bin", + "tags": ["desktop-utility"], "package_manager": { "ubuntu": "apt", "arch": "yay" }, "arch_name": "libnotify", "priority": "medium", @@ -384,6 +395,7 @@ }, { "name": "ncdu", + "tags": ["system-monitoring"], "package_manager": { "server": "apt" }, "priority": "medium", "optional": false, @@ -391,6 +403,7 @@ }, { "name": "cockpit", + "tags": ["system-monitoring", "networking"], "package_manager": { "server": "apt" }, "priority": "medium", "optional": false, @@ -398,6 +411,7 @@ }, { "name": "tailscale", + "tags": ["networking"], "package_manager": { "macos": "app-store", "ubuntu": "custom", "arch": "yay", "server": "custom" }, "macos_name": "Tailscale", "priority": "medium", @@ -408,7 +422,8 @@ }, { "name": "docker", - "package_manager": { "macos": "brew-cask", "ubuntu": "custom", "arch": "yay" }, + "tags": ["development", "containers"], + "package_manager": { "macos": "brew-cask", "ubuntu": "custom", "arch": "yay", "server": "custom" }, "macos_name": "docker-desktop", "priority": "low", "optional": true, @@ -418,6 +433,7 @@ }, { "name": "claude-code", + "tags": ["ai-coding"], "package_manager": { "macos": "custom", "ubuntu": "custom", "arch": "custom", "server": "custom" }, "priority": "medium", "optional": false, @@ -427,6 +443,7 @@ }, { "name": "opencode", + "tags": ["ai-coding"], "package_manager": { "macos": "brew", "ubuntu": "custom", "arch": "yay" }, "macos_name": "anomalyco/tap/opencode", "arch_name": "opencode-bin", @@ -437,6 +454,7 @@ }, { "name": "claude-desktop", + "tags": ["ai-coding"], "package_manager": { "macos": "brew-cask", "ubuntu": "custom", "arch": "yay" }, "macos_name": "claude", "arch_name": "claude-desktop-bin", @@ -447,6 +465,7 @@ }, { "name": "visual-studio-code", + "tags": ["development"], "package_manager": { "macos": "brew-cask", "ubuntu": "snap", "arch": "yay" }, "ubuntu_name": "code", "arch_name": "visual-studio-code-bin", @@ -457,6 +476,7 @@ }, { "name": "firefox", + "tags": ["browser"], "package_manager": { "macos": "brew-cask", "ubuntu": "snap", "arch": "yay" }, "priority": "medium", "optional": false, @@ -464,6 +484,7 @@ }, { "name": "zen-browser", + "tags": ["browser"], "package_manager": { "macos": "brew-cask", "ubuntu": "custom", "arch": "yay" }, "macos_name": "zen", "arch_name": "zen-browser-bin", @@ -474,6 +495,7 @@ }, { "name": "chromium", + "tags": ["browser"], "package_manager": { "ubuntu": "snap", "arch": "yay" }, "priority": "medium", "optional": false, @@ -482,6 +504,7 @@ }, { "name": "obs-studio", + "tags": ["media"], "package_manager": { "macos": "brew-cask", "ubuntu": "apt", "arch": "yay" }, "macos_name": "obs", "priority": "medium", @@ -490,6 +513,7 @@ }, { "name": "vlc", + "tags": ["media"], "package_manager": { "macos": "brew-cask", "ubuntu": "apt", "arch": "yay" }, "priority": "medium", "optional": false, @@ -497,21 +521,16 @@ }, { "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" }, + "tags": ["communication", "entertainment"], + "package_manager": { "macos": "brew-cask", "ubuntu": "snap", "arch": "yay" }, "priority": "medium", "optional": false, - "environment": ["personal"], + "environment": { "ubuntu": ["personal"], "arch": ["personal"] }, "description": "Voice, video, and text chat platform" }, { "name": "bitwarden", + "tags": ["security"], "package_manager": { "macos": "app-store", "ubuntu": "snap", "arch": "yay" }, "macos_name": "Bitwarden", "priority": "medium", @@ -520,6 +539,7 @@ }, { "name": "slack", + "tags": ["communication"], "package_manager": { "macos": "brew-cask", "ubuntu": "snap", "arch": "yay" }, "arch_name": "slack-desktop", "priority": "medium", @@ -529,6 +549,7 @@ }, { "name": "zoom", + "tags": ["communication"], "package_manager": { "macos": "brew-cask", "ubuntu": "snap", "arch": "yay" }, "ubuntu_name": "zoom-client", "priority": "medium", @@ -538,6 +559,7 @@ }, { "name": "microsoft-outlook", + "tags": ["communication"], "package_manager": { "macos": "brew-cask" }, "priority": "medium", "optional": false, @@ -546,22 +568,16 @@ }, { "name": "teams-for-linux", + "tags": ["communication"], "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", + "tags": ["development", "ai-coding"], "package_manager": { "macos": "pnpm", "ubuntu": "pnpm", "arch": "pnpm" }, "priority": "medium", "optional": false, @@ -569,6 +585,7 @@ }, { "name": "mac-mouse-fix", + "tags": ["desktop-utility"], "package_manager": { "macos": "brew-cask" }, "priority": "medium", "optional": false, @@ -576,6 +593,7 @@ }, { "name": "rectangle", + "tags": ["desktop-utility"], "package_manager": { "macos": "brew-cask" }, "priority": "medium", "optional": false, @@ -583,6 +601,7 @@ }, { "name": "caffeine", + "tags": ["desktop-utility"], "package_manager": { "macos": "brew-cask" }, "priority": "medium", "optional": false, @@ -590,6 +609,7 @@ }, { "name": "google-drive", + "tags": ["cloud-storage"], "package_manager": { "macos": "brew-cask" }, "priority": "medium", "optional": false, @@ -597,6 +617,7 @@ }, { "name": "amazon-photos", + "tags": ["photos", "cloud-storage"], "package_manager": { "macos": "brew-cask" }, "priority": "medium", "optional": false, @@ -604,23 +625,18 @@ }, { "name": "notion", - "package_manager": { "macos": "brew-cask" }, - "priority": "medium", - "optional": false, - "description": "Notes and project management" - }, - { - "name": "notion", - "package_manager": { "ubuntu": "snap", "arch": "yay" }, + "tags": ["productivity"], + "package_manager": { "macos": "brew-cask", "ubuntu": "snap", "arch": "yay" }, "ubuntu_name": "notion-snap-reborn", "arch_name": "notion-app-electron", - "priority": "low", - "optional": true, - "environment": ["personal"], + "priority": { "macos": "medium", "ubuntu": "low", "arch": "low" }, + "optional": { "macos": false, "ubuntu": true, "arch": true }, + "environment": { "ubuntu": ["personal"], "arch": ["personal"] }, "description": "Notes and project management — unofficial snap/AUR packages on Linux" }, { "name": "obsidian", + "tags": ["productivity"], "package_manager": { "macos": "brew-cask", "ubuntu": "snap", "arch": "yay" }, "priority": "medium", "optional": false, @@ -629,6 +645,7 @@ }, { "name": "anki", + "tags": ["productivity"], "package_manager": { "macos": "brew-cask", "ubuntu": "custom", "arch": "yay" }, "priority": "medium", "optional": false, @@ -637,6 +654,7 @@ }, { "name": "Spark Mail – AI Email Inbox", + "tags": ["communication"], "macos_name": "Spark Desktop", "package_manager": { "macos": "app-store" }, "priority": "medium", @@ -645,6 +663,7 @@ }, { "name": "Photomator", + "tags": ["photos"], "package_manager": { "macos": "app-store" }, "priority": "medium", "optional": false, @@ -652,6 +671,7 @@ }, { "name": "net-tools", + "tags": ["networking"], "package_manager": { "ubuntu": "apt", "arch": "yay", "server": "apt" }, "priority": "low", "optional": true, @@ -659,6 +679,7 @@ }, { "name": "poppler", + "tags": ["media"], "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay" }, "ubuntu_name": "poppler-utils", "priority": "low", @@ -667,6 +688,7 @@ }, { "name": "ffmpeg", + "tags": ["media"], "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, "priority": "low", "optional": true, @@ -674,6 +696,7 @@ }, { "name": "exiftool", + "tags": ["media", "photos"], "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, "ubuntu_name": "libimage-exiftool-perl", "arch_name": "perl-image-exiftool", @@ -684,6 +707,7 @@ }, { "name": "tesseract", + "tags": ["media"], "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay" }, "ubuntu_name": "tesseract-ocr", "priority": "low", @@ -692,6 +716,7 @@ }, { "name": "smartmontools", + "tags": ["system-monitoring"], "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, "priority": "low", "optional": true, @@ -699,6 +724,7 @@ }, { "name": "nmap", + "tags": ["networking"], "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay", "server": "apt" }, "priority": "low", "optional": true, @@ -706,6 +732,7 @@ }, { "name": "samba", + "tags": ["networking"], "package_manager": { "server": "apt" }, "priority": "low", "optional": true, @@ -713,6 +740,7 @@ }, { "name": "gogcli", + "tags": ["productivity"], "package_manager": { "macos": "brew" }, "priority": "low", "optional": true, @@ -720,6 +748,7 @@ }, { "name": "fluidtop", + "tags": ["system-monitoring"], "package_manager": { "macos": "pipx" }, "priority": "low", "optional": true, @@ -727,6 +756,7 @@ }, { "name": "exifread", + "tags": ["media", "photos"], "package_manager": { "macos": "pipx" }, "priority": "low", "optional": true, @@ -734,6 +764,7 @@ }, { "name": "Goodnotes", + "tags": ["productivity"], "package_manager": { "macos": "app-store" }, "priority": "low", "optional": true, @@ -741,6 +772,7 @@ }, { "name": "spotify", + "tags": ["entertainment"], "package_manager": { "ubuntu": "snap", "arch": "yay" }, "priority": "low", "optional": true, @@ -749,6 +781,7 @@ }, { "name": "steam", + "tags": ["entertainment"], "package_manager": { "ubuntu": "apt", "arch": "yay" }, "priority": "low", "optional": true, @@ -757,6 +790,7 @@ }, { "name": "bolt-launcher", + "tags": ["entertainment"], "package_manager": { "arch": "yay" }, "priority": "low", "optional": true, @@ -765,21 +799,16 @@ }, { "name": "octave", - "package_manager": { "macos": "brew" }, - "priority": "none", - "optional": true, - "description": "MATLAB-compatible numerical computing environment" - }, - { - "name": "octave", - "package_manager": { "ubuntu": "apt", "arch": "yay" }, + "tags": ["science"], + "package_manager": { "macos": "brew", "ubuntu": "apt", "arch": "yay" }, "priority": "none", "optional": true, - "environment": ["work"], + "environment": { "ubuntu": ["work"], "arch": ["work"] }, "description": "MATLAB-compatible numerical computing environment" }, { "name": "qemu", + "tags": ["containers"], "package_manager": { "macos": "brew" }, "priority": "none", "optional": true, @@ -787,6 +816,7 @@ }, { "name": "mongodb-community", + "tags": ["database"], "package_manager": { "macos": "custom" }, "priority": "none", "optional": true, @@ -795,6 +825,7 @@ }, { "name": "Pixelmator Pro", + "tags": ["photos"], "package_manager": { "macos": "app-store" }, "priority": "none", "optional": true, @@ -802,6 +833,7 @@ }, { "name": "Amphetamine", + "tags": ["desktop-utility"], "package_manager": { "macos": "app-store" }, "priority": "none", "optional": true, diff --git a/platforms/macos.sh b/platforms/macos.sh index 43be527..fb1cfe8 100755 --- a/platforms/macos.sh +++ b/platforms/macos.sh @@ -52,8 +52,8 @@ mac_custom_install_tier() { 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" + .package_manager[$plat] == "custom" and prfor($plat) == $pr and + envok($plat; $w; $p) and (icfor($plat) != null) and .name != "nvm" and tagok($plat) ) | icfor($plat)' "$PACKAGES_JSON" | while read -r cmd; do run_eval "$cmd" @@ -75,7 +75,7 @@ print_app_store_reminders() { 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) + .package_manager[$plat] == "app-store" and prfor($plat) == $pr and envok($plat; $w; $p) and tagok($plat) ) | " - \(.name): \(.description)"' "$PACKAGES_JSON") [[ -n "$apps" ]] && printf '%s\n' "$apps" return 0 diff --git a/scripts/validate-packages.sh b/scripts/validate-packages.sh new file mode 100755 index 0000000..01d5587 --- /dev/null +++ b/scripts/validate-packages.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +# Schema validator for packages.json. Enforces the invariants the install +# engine relies on but cannot check at runtime — most importantly the +# "no silent drop" rule: every platform listed in a package's package_manager +# must resolve to a valid priority tier and a boolean optional, so a typo'd or +# missing per-platform key fails here at commit time instead of silently +# dropping the package from every install. +# +# Validates: +# - package_manager is an object; its keys are known platforms +# - per-platform priority/optional/environment/install_command objects only +# key platforms that the package actually targets +# - every targeted platform resolves a valid priority (high/medium/low/none) +# and a boolean optional (scalar or per-platform object form both allowed) +# - environment is a tag array (legacy) or a per-platform object of tag arrays +# - tags is a non-empty array drawn from the controlled vocabulary +# +# Usage: bash scripts/validate-packages.sh + +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PKG="${1:-$ROOT/packages.json}" + +command -v jq >/dev/null || { printf 'error: jq is required\n' >&2; exit 1; } + +if ! jq empty "$PKG" 2>/dev/null; then + printf 'error: %s is not valid JSON\n' "$PKG" >&2 + exit 1 +fi + +if ! jq -e 'type == "array"' "$PKG" >/dev/null 2>&1; then + printf 'error: %s top-level value must be a JSON array\n' "$PKG" >&2 + exit 1 +fi + +# shellcheck disable=SC2016 # $vars below are jq variables, not shell expansions +errors=$(jq -r ' + def PLATFORMS: ["macos","ubuntu","arch","server"]; + def TIERS: ["high","medium","low","none"]; + def ENVS: ["work","personal"]; + def TAGS: ["ai-coding","browser","cloud-storage","communication","containers", + "database","desktop-utility","development","entertainment","local-llm", + "media","networking","photos","productivity","science","security", + "system-monitoring","terminal"]; + def prfor($p): (.priority | if type == "object" then .[$p] else . end); + def optfor($p): (.optional | if type == "object" then .[$p] else . end); + + ( group_by(.name)[] | select(length > 1) + | "duplicate name \"\(.[0].name // "(unnamed)")\" — \(length) entries" ), + ( to_entries[] + | .key as $i | .value as $e + | ($e.name // "") as $nm + | $e + | [ + (if (.name | type) != "string" or (.name | length) == 0 + then ": name must be a non-empty string" else empty end), + + (if (.package_manager | type) != "object" + then "\($nm): package_manager must be an object" + elif (.package_manager | length) == 0 + then "\($nm): package_manager must list at least one platform" + else empty end), + + (if (.package_manager | type) == "object" + then ((.package_manager | keys[]) as $k + | if (PLATFORMS | index($k)) == null + then "\($nm): unknown platform \"\($k)\" in package_manager" else empty end) + else empty end), + + (["priority","optional","environment","install_command"][] as $f + | .[$f] as $v + | if ($v | type) == "object" and (.package_manager | type) == "object" + then (($v | keys[]) as $k + | if ((.package_manager | keys) | index($k)) == null + then "\($nm): \($f) keys platform \"\($k)\" not in package_manager" else empty end) + else empty end), + + (if (.package_manager | type) == "object" + then ((.package_manager | keys[]) as $p + | (prfor($p) as $pr + | if (TIERS | index($pr)) == null + then "\($nm): priority for \"\($p)\" is \($pr | tojson) — must be high/medium/low/none" else empty end), + (optfor($p) as $o + | if ($o | type) != "boolean" + then "\($nm): optional for \"\($p)\" is \($o | tojson) — must be boolean" else empty end)) + else empty end), + + (.environment as $env + | if $env == null then empty + elif ($env | type) == "array" + then ($env[] as $x + | if (ENVS | index($x)) == null + then "\($nm): environment value \"\($x)\" — must be work/personal" else empty end) + elif ($env | type) == "object" + then (($env | to_entries[]) as $kv + | if ($kv.value | type) != "array" + then "\($nm): environment.\($kv.key) must be an array" + else ($kv.value[] as $x + | if (ENVS | index($x)) == null + then "\($nm): environment.\($kv.key) value \"\($x)\" — must be work/personal" else empty end) + end) + else "\($nm): environment must be a tag array or per-platform object" end), + + (if (.tags | type) != "array" then "\($nm): tags must be an array" + elif (.tags | length) == 0 then "\($nm): tags must be non-empty" + else (.tags[] as $tg + | if (TAGS | index($tg)) == null + then "\($nm): unknown tag \"\($tg)\" — not in the controlled vocabulary" else empty end) + end) + ] + | .[] ) +' "$PKG") + +if [[ -n "$errors" ]]; then + printf '%s\n' "$errors" >&2 + printf '\nvalidate-packages: FAILED\n' >&2 + exit 1 +fi + +printf 'validate-packages: PASSED (%d entries)\n' "$(jq length "$PKG")" diff --git a/setup.sh b/setup.sh index f4109d0..1a47b51 100755 --- a/setup.sh +++ b/setup.sh @@ -1,11 +1,18 @@ #!/usr/bin/env bash # Unified setup entrypoint (UNIFICATION.md, issue #36). -# Usage: bash setup.sh [--optional] [--work] [--personal] [--dry-run] -# [--platform ] [--profile ] +# Usage: bash setup.sh [--optional] [--work] [--personal] [--base] [--tags ] +# [--dry-run] [--platform ] +# [--profile ] # --optional also install low-priority optional packages # --work also install work-only packages # --personal also install personal-only packages +# --base install only the base essentials (no category packages) +# --tags install base + only the named categories, e.g. +# --tags development,terminal (categories are packages.json tags) # --dry-run print all commands without executing anything +# +# Run bare on a terminal (no selection flags) and setup prompts you to pick +# categories interactively; base essentials are always installed. # --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); @@ -23,6 +30,8 @@ source "$SETUP_ROOT/lib/core.sh" core_parse_args "$@" core_detect_platform +core_validate_tag_selection +core_maybe_prompt_selection # shellcheck source=/dev/null source "$SETUP_ROOT/platforms/$PLATFORM.sh"