From 77554b751a8809191b7dc86e728a60369c9f7646 Mon Sep 17 00:00:00 2001 From: Jon B Date: Tue, 3 Feb 2026 13:26:03 -0600 Subject: [PATCH 01/33] chore: initial release setup --- .github/dependabot.yml | 9 + .github/workflows/ci.yml | 17 + .github/workflows/dependabot-automerge.yml | 17 + .github/workflows/docs.yml | 43 ++ .github/workflows/pr-title.yml | 19 + .github/workflows/release-please.yml | 16 + .github/workflows/release.yml | 21 + .pre-commit-config.yaml | 28 ++ .release-please-config.json | 6 + .release-please-manifest.json | 3 + AGENTS.md | 168 ++++++++ CODEOWNERS | 1 + CONTRIBUTING.md | 60 +++ Makefile | 8 + README.md | 52 +++ SECURITY.md | 11 + TOOLS.md | 108 +++++ bash_profile | 26 ++ bashrc | 18 + bashrc.d/00-options.sh | 21 + bashrc.d/10-helpers.sh | 18 + bashrc.d/20-path.sh | 31 ++ bashrc.d/30-buildflags.sh | 20 + bashrc.d/40-completions.sh | 22 + bashrc.d/50-tool-init.sh | 11 + bashrc.d/60-asdf.sh | 13 + bashrc.d/65-tools.sh | 23 + bashrc.d/66-doppler.sh | 22 + bashrc.d/70-env.sh | 6 + bashrc.d/80-aliases.sh | 11 + bashrc.d/90-functions.sh | 24 ++ bashrc.d/95-ssh-agent.sh | 11 + bashrc.d/99-secrets.sh | 14 + bin/README.md | 6 + bin/gen_tool_versions | 17 + bin/ram_usage | 311 ++++++++++++++ docs/CONFIG.md | 63 +++ docs/INDEX.md | 11 + docs/INSTALLER.md | 60 +++ docs/INSTALLERS.md | 249 +++++++++++ docs/INSTALLERS_HELPERS.md | 55 +++ docs/MODULES.md | 35 ++ docs/README.md | 26 ++ docs/SHDOC.md | 38 ++ install.sh | 468 +++++++++++++++++++++ installers/README.md | 11 + installers/_helpers.sh | 101 +++++ installers/actionlint.sh | 19 + installers/asdf.sh | 25 ++ installers/awscli.sh | 19 + installers/bashate.sh | 19 + installers/bat.sh | 19 + installers/bats.sh | 19 + installers/brew.sh | 25 ++ installers/curl.sh | 19 + installers/dialog.sh | 28 ++ installers/direnv.sh | 25 ++ installers/doppler.sh | 25 ++ installers/fd.sh | 19 + installers/fzf.sh | 19 + installers/gh.sh | 19 + installers/git-lfs.sh | 19 + installers/git.sh | 19 + installers/gnu-tools.sh | 21 + installers/gnupg.sh | 19 + installers/helm.sh | 19 + installers/java.sh | 32 ++ installers/jq.sh | 19 + installers/kubectl.sh | 19 + installers/nodejs.sh | 32 ++ installers/pre-commit.sh | 19 + installers/python.sh | 32 ++ installers/rg.sh | 19 + installers/shdoc.sh | 61 +++ installers/shellcheck.sh | 19 + installers/starship.sh | 25 ++ installers/stern.sh | 19 + installers/terraform.sh | 19 + installers/tree.sh | 19 + installers/wget.sh | 19 + installers/yq.sh | 19 + memory-bank/activeContext.md | 14 + memory-bank/productContext.md | 6 + memory-bank/progress.md | 16 + memory-bank/projectbrief.md | 6 + memory-bank/systemPatterns.md | 7 + memory-bank/techContext.md | 9 + profiles/README.md | 14 + profiles/dev.env | 2 + profiles/minimal.env | 2 + profiles/ops.env | 2 + scripts/ci-setup.sh | 21 + scripts/gen-docs.sh | 56 +++ scripts/package.sh | 23 + scripts/pre-commit-ci.sh | 18 + secrets.d/README.md | 9 + tests/install.bats | 15 + 97 files changed, 3317 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/dependabot-automerge.yml create mode 100644 .github/workflows/docs.yml create mode 100644 .github/workflows/pr-title.yml create mode 100644 .github/workflows/release-please.yml create mode 100644 .github/workflows/release.yml create mode 100644 .pre-commit-config.yaml create mode 100644 .release-please-config.json create mode 100644 .release-please-manifest.json create mode 100644 AGENTS.md create mode 100644 CODEOWNERS create mode 100644 CONTRIBUTING.md create mode 100644 Makefile create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 TOOLS.md create mode 100644 bash_profile create mode 100644 bashrc create mode 100644 bashrc.d/00-options.sh create mode 100644 bashrc.d/10-helpers.sh create mode 100644 bashrc.d/20-path.sh create mode 100644 bashrc.d/30-buildflags.sh create mode 100644 bashrc.d/40-completions.sh create mode 100644 bashrc.d/50-tool-init.sh create mode 100644 bashrc.d/60-asdf.sh create mode 100644 bashrc.d/65-tools.sh create mode 100644 bashrc.d/66-doppler.sh create mode 100644 bashrc.d/70-env.sh create mode 100644 bashrc.d/80-aliases.sh create mode 100644 bashrc.d/90-functions.sh create mode 100644 bashrc.d/95-ssh-agent.sh create mode 100644 bashrc.d/99-secrets.sh create mode 100644 bin/README.md create mode 100755 bin/gen_tool_versions create mode 100755 bin/ram_usage create mode 100644 docs/CONFIG.md create mode 100644 docs/INDEX.md create mode 100644 docs/INSTALLER.md create mode 100644 docs/INSTALLERS.md create mode 100644 docs/INSTALLERS_HELPERS.md create mode 100644 docs/MODULES.md create mode 100644 docs/README.md create mode 100644 docs/SHDOC.md create mode 100755 install.sh create mode 100644 installers/README.md create mode 100755 installers/_helpers.sh create mode 100755 installers/actionlint.sh create mode 100755 installers/asdf.sh create mode 100755 installers/awscli.sh create mode 100755 installers/bashate.sh create mode 100755 installers/bat.sh create mode 100755 installers/bats.sh create mode 100755 installers/brew.sh create mode 100755 installers/curl.sh create mode 100755 installers/dialog.sh create mode 100755 installers/direnv.sh create mode 100755 installers/doppler.sh create mode 100755 installers/fd.sh create mode 100755 installers/fzf.sh create mode 100755 installers/gh.sh create mode 100755 installers/git-lfs.sh create mode 100755 installers/git.sh create mode 100755 installers/gnu-tools.sh create mode 100755 installers/gnupg.sh create mode 100755 installers/helm.sh create mode 100755 installers/java.sh create mode 100755 installers/jq.sh create mode 100755 installers/kubectl.sh create mode 100755 installers/nodejs.sh create mode 100755 installers/pre-commit.sh create mode 100755 installers/python.sh create mode 100755 installers/rg.sh create mode 100755 installers/shdoc.sh create mode 100755 installers/shellcheck.sh create mode 100755 installers/starship.sh create mode 100755 installers/stern.sh create mode 100755 installers/terraform.sh create mode 100755 installers/tree.sh create mode 100755 installers/wget.sh create mode 100755 installers/yq.sh create mode 100644 memory-bank/activeContext.md create mode 100644 memory-bank/productContext.md create mode 100644 memory-bank/progress.md create mode 100644 memory-bank/projectbrief.md create mode 100644 memory-bank/systemPatterns.md create mode 100644 memory-bank/techContext.md create mode 100644 profiles/README.md create mode 100644 profiles/dev.env create mode 100644 profiles/minimal.env create mode 100644 profiles/ops.env create mode 100755 scripts/ci-setup.sh create mode 100755 scripts/gen-docs.sh create mode 100755 scripts/package.sh create mode 100755 scripts/pre-commit-ci.sh create mode 100644 secrets.d/README.md create mode 100755 tests/install.bats diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..62b5749 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,9 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + labels: + - "dependencies" + open-pull-requests-limit: 10 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5eae8ca --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,17 @@ +name: CI +on: + push: + branches: [ main ] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - name: CI setup (get-bashed) + run: ./scripts/ci-setup.sh "bats,shellcheck,actionlint,bashate,pre_commit" + - name: Run tests + run: bats tests + - name: Pre-commit + run: ./scripts/pre-commit-ci.sh diff --git a/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml new file mode 100644 index 0000000..4daef5b --- /dev/null +++ b/.github/workflows/dependabot-automerge.yml @@ -0,0 +1,17 @@ +name: Dependabot Auto-merge +on: + pull_request: + +permissions: + contents: write + pull-requests: write + +jobs: + auto-merge: + if: github.actor == 'dependabot[bot]' + runs-on: ubuntu-latest + steps: + - uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # v2.5.0 + - uses: peter-evans/enable-pull-request-automerge@a660677d5469627102a1c1e11409dd063606628d # v3.0.0 + with: + merge-method: squash diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..a58172e --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,43 @@ +name: Docs +on: + push: + branches: [ main ] + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - name: CI setup (get-bashed) + run: ./scripts/ci-setup.sh "shdoc" + - name: Generate docs + run: ./scripts/gen-docs.sh + - name: Ensure landing page + run: | + if [[ ! -f docs/index.md ]]; then + cp docs/INDEX.md docs/index.md + fi + - name: Upload pages artifact + uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0 + with: + path: docs + + deploy: + runs-on: ubuntu-latest + needs: build + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml new file mode 100644 index 0000000..a0d67cf --- /dev/null +++ b/.github/workflows/pr-title.yml @@ -0,0 +1,19 @@ +name: PR Title +on: + pull_request: + types: [opened, edited, synchronize, reopened] + +jobs: + lint-title: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - name: Validate PR title + run: | + title="${{ github.event.pull_request.title }}" + pattern='^(feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert)(\([a-z0-9._-]+\))?: .+' + if [[ ! "$title" =~ $pattern ]]; then + echo "Invalid PR title: $title" >&2 + echo "Expected Conventional Commit format, e.g., 'feat: add installer'." >&2 + exit 1 + fi diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..3b548ad --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,16 @@ +name: Release Please +on: + push: + branches: [ main ] + +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: googleapis/release-please-action@16a9c90856f42705d54a6fda1823352bdc62cf38 # v4.4.0 + with: + release-type: simple + exclude-paths: | + .github/** + docs/** + .pre-commit-config.yaml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1533083 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,21 @@ +name: Release Artifacts +on: + release: + types: [ published ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - name: Package + run: ./scripts/package.sh dist "${GITHUB_REF_NAME}" + - name: Upload tarball + uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # v1.0.2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: dist/get-bashed-${{ github.ref_name }}.tar.gz + asset_name: get-bashed-${{ github.ref_name }}.tar.gz + asset_content_type: application/gzip diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..c7d984d --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,28 @@ +repos: + - repo: https://github.com/gitleaks/gitleaks + rev: v8.16.3 + hooks: + - id: gitleaks + - repo: https://github.com/jumanjihouse/pre-commit-hooks + rev: 3.0.0 + hooks: + - id: shellcheck + - repo: https://github.com/sirosen/check-jsonschema + rev: b035497fb64e3f9faa91e833331688cc185891e6 + hooks: + - id: check-github-workflows + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c + hooks: + - id: detect-private-key + - id: trailing-whitespace + - id: end-of-file-fixer + - repo: https://github.com/rhysd/actionlint + rev: v1.7.4 + hooks: + - id: actionlint + - repo: https://github.com/openstack/bashate + rev: 2.1.1 + hooks: + - id: bashate + diff --git a/.release-please-config.json b/.release-please-config.json new file mode 100644 index 0000000..4a3bc2d --- /dev/null +++ b/.release-please-config.json @@ -0,0 +1,6 @@ +{ + "release-type": "simple", + "packages": { + ".": {} + } +} diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..466df71 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.1.0" +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..4f4fcab --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,168 @@ +# Repository Guidelines + +This repository contains the **get-bashed** modular Bash setup. It is intended to be portable, auditable, and safe to install on macOS, Linux, and WSL. + +## Project Structure & Module Organization + +- `bashrc` / `bash_profile`: entrypoints sourced by the installer. +- `bashrc.d/`: ordered modules (`00-` to `99-`) loaded in sequence. +- `bin/`: curated helper scripts meant to be portable and non-sensitive. +- `install.sh`: idempotent installer that wires user dotfiles. +- `installers/`: dependency-aware installers with metadata. +- `tests/`: BATS test suite. +- `.github/workflows/`: CI and release automation. + +## Build, Test, and Development Commands + +- `./install.sh --prefix ~/.get-bashed`: install locally for testing. +- `bats tests`: run installer tests. +- `./scripts/package.sh dist `: build a release tarball. +- `./scripts/gen-docs.sh`: generate shdoc-based docs. + +## Coding Style & Naming Conventions + +- Shell: POSIX-ish Bash with strict mode in scripts (`set -euo pipefail`). +- Modules: two-digit prefix and hyphenated names (e.g., `20-path.sh`). +- Keep config portable; avoid hardcoding user-specific paths. + +## Testing Guidelines + +- Tests use BATS and should only validate install behavior and module wiring. +- Keep tests deterministic and side-effect free (use temp `HOME`). + +## Commit & Pull Request Guidelines + +- Commits should be small, focused, and explain intent. +- PRs should include: + - Summary of changes + - Install impact (if any) + - Updated docs when behavior changes + +## Security & Secrets + +- Secrets live in `~/.get-bashed/secrets.d/` (ignored by git). +- `bashrc.d/99-secrets.sh` sources everything in `secrets.d/`. + +# Project Memory Bank + +I am an expert software engineer with a unique characteristic: my memory resets completely between sessions. This isn't a limitation - it's what drives me to maintain precise documentation. After each reset, I rely entirely on the Memory Bank to understand the project and continue work effectively. I must read ALL memory bank files at the start of every task - this is not optional. + +## Memory Bank Structure + +The Memory Bank consists of core files and optional context files, all in Markdown format. Files build upon each other in a clear hierarchy: + +``` +flowchart TD + PB[projectbrief.md] --> PC[productContext.md] + PB --> SP[systemPatterns.md] + PB --> TC[techContext.md] + + PC --> AC[activeContext.md] + SP --> AC + TC --> AC + + AC --> P[progress.md] +``` + +### Core Files (Required) +1. `projectbrief.md` + - Foundation document that shapes all other files + - Created at project start if it doesn't exist + - Defines core requirements and goals + - Source of truth for project scope + +2. `productContext.md` + - Why this project exists + - Problems it solves + - How it should work + - User experience goals + +3. `activeContext.md` + - Current work focus + - Recent changes + - Next steps + - Active decisions and considerations + - Important patterns and preferences + - Learnings and project insights + +4. `systemPatterns.md` + - System architecture + - Key technical decisions + - Design patterns in use + - Component relationships + - Critical implementation paths + +5. `techContext.md` + - Technologies used + - Development setup + - Technical constraints + - Dependencies + - Tool usage patterns + +6. `progress.md` + - What works + - What's left to build + - Current status + - Known issues + - Evolution of project decisions + +### Additional Context +Create additional files/folders within `memory-bank/` when they help organize: +- Complex feature documentation +- Integration specifications +- API documentation +- Testing strategies +- Deployment procedures + +## Core Workflows + +### Plan Mode +``` +flowchart TD + Start[Start] --> ReadFiles[Read Memory Bank] + ReadFiles --> CheckFiles{Files Complete?} + + CheckFiles -->|No| Plan[Create Plan] + Plan --> Document[Document in Chat] + + CheckFiles -->|Yes| Verify[Verify Context] + Verify --> Strategy[Develop Strategy] + Strategy --> Present[Present Approach] +``` + +### Act Mode +``` +flowchart TD + Start[Start] --> Context[Check Memory Bank] + Context --> Update[Update Documentation] + Update --> Execute[Execute Task] + Execute --> Document[Document Changes] +``` + +## Documentation Updates + +Memory Bank updates occur when: +1. Discovering new project patterns +2. After implementing significant changes +3. When user requests **update memory bank** (must review ALL files) +4. When context needs clarification + +``` +flowchart TD + Start[Update Process] + + subgraph Process + P1[Review ALL Files] + P2[Document Current State] + P3[Clarify Next Steps] + P4[Document Insights & Patterns] + + P1 --> P2 --> P3 --> P4 + end + + Start --> Process +``` + +Note: When triggered by **update memory bank**, I must review every memory bank file, even if some don't require updates. Focus particularly on `activeContext.md` and `progress.md` as they track current state. + +Remember: After every memory reset, I begin completely fresh. The Memory Bank is my only link to previous work. It must be maintained with precision and clarity, as my effectiveness depends entirely on its accuracy. diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..84222e3 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @jbdevprimary diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b7b51aa --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,60 @@ +# Contributing + +Thanks for helping improve get-bashed. + +## Architecture + +- `bashrc` / `bash_profile` are the entrypoints. +- `bashrc.d/` is the ordered module system. +- `install.sh` is the installer and config generator. +- `installers/` contains dependency-aware installers. +- `scripts/` holds CI and doc helpers. + +## Development + +```bash +make docs +make lint +``` + +## Pre-commit + +Install hooks: +```bash +pre-commit install +``` + +Run all checks: +```bash +pre-commit run --all-files +``` + +## Tests + +```bash +bats tests +``` + +## Docs + +```bash +./scripts/gen-docs.sh +``` + +## CI + +CI uses `scripts/ci-setup.sh` to bootstrap tools into `GET_BASHED_HOME`. + +## Guidelines + +- Keep scripts portable and dependency-light. +- Avoid hardcoding user-specific paths. +- Add shdoc annotations for new scripts. + +## Conventional Commits + +PR titles must follow Conventional Commits (e.g., `feat: add installer`). + +## Branch Protection + +See `docs/CONFIG.md` for recommended required checks and CODEOWNERS review. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e0b7d10 --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +# Docs and lint targets for get-bashed. +.PHONY: docs lint + +docs: + ./scripts/gen-docs.sh + +lint: + pre-commit run --all-files diff --git a/README.md b/README.md new file mode 100644 index 0000000..b950a04 --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# get-bashed + +[![CI](https://github.com/jbdevprimary/get-bashed/actions/workflows/ci.yml/badge.svg)](https://github.com/jbdevprimary/get-bashed/actions/workflows/ci.yml) +[![Docs](https://github.com/jbdevprimary/get-bashed/actions/workflows/docs.yml/badge.svg)](https://github.com/jbdevprimary/get-bashed/actions/workflows/docs.yml) +[![Release](https://github.com/jbdevprimary/get-bashed/actions/workflows/release-please.yml/badge.svg)](https://github.com/jbdevprimary/get-bashed/actions/workflows/release-please.yml) + +A modern, modular Bash environment you can install anywhere. get-bashed is designed to be readable, portable, and safe to extend, with a clean installer that supports interactive and non-interactive setups. + +## Why get-bashed + +- Modular `bashrc.d` architecture +- Cross-platform defaults (macOS, Linux, WSL) +- Optional GNU tooling on macOS +- Reproducible installs with profiles and features +- Safe secrets handling via `secrets.d` + +## Quick Start + +```bash +curl -fsSL https://raw.githubusercontent.com/jbdevprimary/get-bashed/main/install.sh | bash +``` + +## Learn More + +Docs: `https://jonbogaty.com/get-bashed` + +## Features + +Use `--features` with comma lists (supports `no-` prefix): +- `gnu_over_bsd` +- `build_flags` +- `auto_tools` +- `ssh_agent` +- `doppler_env` +- `dev_tools` (bundle) +- `ops_tools` (bundle) + +## Installers + +Available installers (comma list): +- `brew`, `asdf`, `doppler`, `dialog`, `starship`, `direnv`, `gnu_tools` +- `git`, `curl`, `wget`, `gnupg` +- `rg`, `fd`, `bat`, `fzf`, `jq`, `yq`, `tree` +- `gh`, `git_lfs`, `terraform`, `awscli`, `kubectl`, `helm`, `stern` +- `nodejs`, `python`, `java` +- `pre_commit`, `bashate`, `shellcheck`, `actionlint`, `bats`, `shdoc` + +## Support + +- Issues and feature requests: use GitHub Issues for this repository. +- Security: see `SECURITY.md`. +- Contributing: see `CONTRIBUTING.md`. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..25c2477 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,11 @@ +# Security Policy + +## Reporting a Vulnerability + +Please report security issues privately. + +Use GitHub Security Advisories for this repository, or contact the maintainer directly. + +## Supported Versions + +Only the latest release is supported with security updates. diff --git a/TOOLS.md b/TOOLS.md new file mode 100644 index 0000000..fe39a28 --- /dev/null +++ b/TOOLS.md @@ -0,0 +1,108 @@ +# Tools & Language Management + +This project favors **asdf** for multi-version language management and keeps shell behavior deterministic. + +## asdf + +- Recommended for: Node.js, Python, Java, and other multi-version runtimes. +- `bashrc.d/60-asdf.sh` activates asdf when present. + +Example: +```bash +asdf plugin add nodejs +asdf install nodejs 22.22.0 +asdf set --home nodejs 22.22.0 +``` + +## Build Flags (macOS) + +When building runtimes from source (Python, Ruby, etc.), macOS often requires explicit paths to Homebrew libraries. If you enable: + +```bash +GET_BASHED_BUILD_FLAGS=1 +``` + +then `bashrc.d/30-buildflags.sh` exports: +- `CPPFLAGS`, `LDFLAGS`, `PKG_CONFIG_PATH` +- `LIBRARY_PATH`, `CPATH` +- `PYTHON_CONFIGURE_OPTS` + +These are derived from Homebrew prefixes for `openssl@3`, `readline`, `gettext`, and `zstd`. + +## GNU Tooling (macOS) + +If you want GNU tooling on macOS, install the core GNU packages via Homebrew and enable: + +```bash +GET_BASHED_GNU=1 +``` + +This adds `coreutils`, `findutils`, `gnu-sed`, and `gnu-tar` gnubin paths ahead of BSD tools. + +## Optional CLI Tools + +`bashrc.d/65-tools.sh` includes a helper to install optional CLIs using Node: +- `@google/gemini-cli` +- `@sonar/scan` + +## Doppler (Optional) + +If you use Doppler for secrets, install the CLI and enable: + +```bash +GET_BASHED_USE_DOPPLER=1 +``` + +You can install via the installer: + +```bash +./install.sh --install doppler +``` + +Note: we do not auto-source doppler in shell init. Use `doppler_shell` to start a doppler-enabled subshell. + +## Curation Policy + +- Keep `bin/` small and portable. +- Prefer scripts that are OS-safe, dependency-light, and non-sensitive. + +## Installer UI + +If you pass `--with-ui`, the installer will try to use a curses UI (`dialog`). +If `dialog` is not present, it will attempt to install it using the system package +manager (Homebrew, apt, dnf, yum). Otherwise it falls back to plain prompts. + +## Listing and Dry Run + +- `./install.sh --list` shows the catalog of installers. +- `./install.sh --list-profiles` shows available profiles. +- `./install.sh --list-features` shows available features. +- `./install.sh --list-installers` shows the installer catalog. +- `./install.sh --dry-run --install ` shows what would be installed. + +## Docs + +Generate docs with: +```bash +./scripts/gen-docs.sh +``` + +## Feature Names + +Use `--features` with comma-separated values: +- `gnu_over_bsd` +- `build_flags` +- `auto_tools` +- `ssh_agent` +- `doppler_env` +- `dev_tools` +- `ops_tools` + +## Language Installers + +Installers exist for `nodejs`, `python`, and `java`. If `asdf` is installed, they +will use it. Otherwise they fall back to the system package manager. + +## Profiles + +Use `--profiles minimal|dev|ops` to apply presets before any manual selections. diff --git a/bash_profile b/bash_profile new file mode 100644 index 0000000..6c34dc6 --- /dev/null +++ b/bash_profile @@ -0,0 +1,26 @@ +# @file bash_profile +# @brief get-bashed login entrypoint. +# @description +# Loads Homebrew shellenv (if present) then delegates to bashrc. + +# Return early if not interactive +[[ $- != *i* ]] && return + +# Homebrew shellenv (optional) +if command -v brew >/dev/null 2>&1; then + if [[ "$(uname -m)" == "arm64" ]]; then + eval "$(/opt/homebrew/bin/brew shellenv)" + elif [[ -x /usr/local/bin/brew ]]; then + eval "$(/usr/local/bin/brew shellenv)" + else + eval "$(brew shellenv)" + fi +fi + +# Hand off to interactive rc +GET_BASHED_HOME="${GET_BASHED_HOME:-$HOME/.get-bashed}" +if [[ -r "$GET_BASHED_HOME/bashrc" ]]; then + source "$GET_BASHED_HOME/bashrc" +elif [[ -r "$HOME/.bashrc" ]]; then + source "$HOME/.bashrc" +fi diff --git a/bashrc b/bashrc new file mode 100644 index 0000000..da30781 --- /dev/null +++ b/bashrc @@ -0,0 +1,18 @@ +# @file bashrc +# @brief get-bashed interactive entrypoint. +# @description +# Loads modular runtime files in order. + +# Return early if not interactive +[[ $- != *i* ]] && return + +GET_BASHED_HOME="${GET_BASHED_HOME:-$HOME/.get-bashed}" +GET_BASHED_RC_DIR="${GET_BASHED_RC_DIR:-$GET_BASHED_HOME/bashrc.d}" + +if [[ -r "$GET_BASHED_HOME/get-bashedrc.sh" ]]; then + source "$GET_BASHED_HOME/get-bashedrc.sh" +fi + +for f in "$GET_BASHED_RC_DIR"/[0-9][0-9]-*.sh; do + [[ -r "$f" ]] && source "$f" +done diff --git a/bashrc.d/00-options.sh b/bashrc.d/00-options.sh new file mode 100644 index 0000000..8f24f34 --- /dev/null +++ b/bashrc.d/00-options.sh @@ -0,0 +1,21 @@ +# @file 00-options +# @brief get-bashed module: 00-options +# @description +# Runtime module loaded by get-bashed in lexicographic order. + +# Shell options, history, editor +shopt -s histappend checkwinsize cmdhist lithist autocd cdspell checkjobs expand_aliases +HISTIGNORE="&:history:ls:ls * ps:ps -A:[bf]g:exit:${HISTIGNORE}" + +# Editor default (respect existing) +: "${EDITOR:=vim}" +export EDITOR + +# macOS: silence legacy bash warning + raise soft fd limit +if [[ "$(uname -s)" == "Darwin" ]]; then + export BASH_SILENCE_DEPRECATION_WARNING=1 + ulimit -n 524288 2>/dev/null || true +fi + +# Misc +export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES diff --git a/bashrc.d/10-helpers.sh b/bashrc.d/10-helpers.sh new file mode 100644 index 0000000..88ecd44 --- /dev/null +++ b/bashrc.d/10-helpers.sh @@ -0,0 +1,18 @@ +# @file 10-helpers +# @brief get-bashed module: 10-helpers +# @description +# Runtime module loaded by get-bashed in lexicographic order. + +# PATH helpers + safe source +_path_add_front() { [[ -d "$1" ]] && PATH="$1:${PATH}"; } +_path_add_back() { [[ -d "$1" ]] && PATH="${PATH}:$1"; } +_path_dedupe() { + awk -v RS=: '!seen[$0]++ { out = out (NR==1?"":":") $0 } END{ print out }' <<<"$PATH" +} + +declare -f path_add >/dev/null 2>&1 || path_add() { + _path_add_front "$@" + PATH="$(_path_dedupe)" +} + +_maybe_source() { [[ -r "$1" ]] && source "$1"; } diff --git a/bashrc.d/20-path.sh b/bashrc.d/20-path.sh new file mode 100644 index 0000000..c0ab004 --- /dev/null +++ b/bashrc.d/20-path.sh @@ -0,0 +1,31 @@ +# @file 20-path +# @brief get-bashed module: 20-path +# @description +# Runtime module loaded by get-bashed in lexicographic order. + +# Base PATH (keep minimal and predictable) +GET_BASHED_HOME="${GET_BASHED_HOME:-$HOME/.get-bashed}" +path_add "$GET_BASHED_HOME/bin" +path_add "$HOME/bin" +path_add "$HOME/.local/bin" +path_add "$HOME/.cargo/bin" + +# Optional: Go +export GOBIN="${GOBIN:-$HOME/go/bin}" +path_add "$GOBIN" + +# Optional: ASDF shims +path_add "$HOME/.asdf/shims" + +# Optional: prefer GNU tools on macOS (requires Homebrew coreutils, gnu-sed, etc.) +# Set GET_BASHED_GNU=1 to enable. +if [[ "${GET_BASHED_GNU:-0}" == "1" ]] && command -v brew >/dev/null 2>&1; then + path_add "$(brew --prefix coreutils)/libexec/gnubin" + path_add "$(brew --prefix findutils)/libexec/gnubin" + path_add "$(brew --prefix gnu-sed)/libexec/gnubin" + path_add "$(brew --prefix gnu-tar)/libexec/gnubin" +fi + +# Deduplicate once at end +PATH="$(_path_dedupe)" +export PATH diff --git a/bashrc.d/30-buildflags.sh b/bashrc.d/30-buildflags.sh new file mode 100644 index 0000000..5b42eb1 --- /dev/null +++ b/bashrc.d/30-buildflags.sh @@ -0,0 +1,20 @@ +# @file 30-buildflags +# @brief get-bashed module: 30-buildflags +# @description +# Runtime module loaded by get-bashed in lexicographic order. + +# Build flags for compiling language runtimes (optional) +# Enable with GET_BASHED_BUILD_FLAGS=1 +if [[ "${GET_BASHED_BUILD_FLAGS:-0}" == "1" ]] && command -v brew >/dev/null 2>&1; then + OPENSSL_PREFIX="$(brew --prefix openssl@3 2>/dev/null || brew --prefix openssl 2>/dev/null)" + READLINE_PREFIX="$(brew --prefix readline 2>/dev/null)" + GETTEXT_PREFIX="$(brew --prefix gettext 2>/dev/null)" + ZSTD_PREFIX="$(brew --prefix zstd 2>/dev/null)" + + export LDFLAGS="-L${OPENSSL_PREFIX}/lib -L${READLINE_PREFIX}/lib -L${GETTEXT_PREFIX}/lib -L${ZSTD_PREFIX}/lib ${LDFLAGS}" + export CPPFLAGS="-I${OPENSSL_PREFIX}/include -I${READLINE_PREFIX}/include -I${GETTEXT_PREFIX}/include -I${ZSTD_PREFIX}/include ${CPPFLAGS}" + export PKG_CONFIG_PATH="${OPENSSL_PREFIX}/lib/pkgconfig:${READLINE_PREFIX}/lib/pkgconfig:${GETTEXT_PREFIX}/lib/pkgconfig:${ZSTD_PREFIX}/lib/pkgconfig:${PKG_CONFIG_PATH}" + export LIBRARY_PATH="${OPENSSL_PREFIX}/lib:${READLINE_PREFIX}/lib:${GETTEXT_PREFIX}/lib:${ZSTD_PREFIX}/lib:${LIBRARY_PATH}" + export CPATH="${OPENSSL_PREFIX}/include:${READLINE_PREFIX}/include:${GETTEXT_PREFIX}/include:${ZSTD_PREFIX}/include:${CPATH}" + export PYTHON_CONFIGURE_OPTS="--with-openssl=${OPENSSL_PREFIX} --with-readline=editline" +fi diff --git a/bashrc.d/40-completions.sh b/bashrc.d/40-completions.sh new file mode 100644 index 0000000..fc8bc85 --- /dev/null +++ b/bashrc.d/40-completions.sh @@ -0,0 +1,22 @@ +# @file 40-completions +# @brief get-bashed module: 40-completions +# @description +# Runtime module loaded by get-bashed in lexicographic order. + +# Bash completion core (Homebrew) +if command -v brew >/dev/null 2>&1; then + export BASH_COMPLETION_USER_DIR="$HOME/.local/share/bash-completion" + if [[ -r "/opt/homebrew/etc/profile.d/bash_completion.sh" ]]; then + . "/opt/homebrew/etc/profile.d/bash_completion.sh" + elif [[ -r "/usr/local/etc/profile.d/bash_completion.sh" ]]; then + . "/usr/local/etc/profile.d/bash_completion.sh" + fi +fi + +# Sudo completion +complete -cf sudo + +# asdf completions +if command -v asdf >/dev/null 2>&1; then + . <(asdf completion bash) +fi diff --git a/bashrc.d/50-tool-init.sh b/bashrc.d/50-tool-init.sh new file mode 100644 index 0000000..75138bd --- /dev/null +++ b/bashrc.d/50-tool-init.sh @@ -0,0 +1,11 @@ +# @file 50-tool-init +# @brief get-bashed module: 50-tool-init +# @description +# Runtime module loaded by get-bashed in lexicographic order. + +# Cargo (idempotent) +_maybe_source "$HOME/.cargo/env" + +# Prompt + env managers +command -v starship >/dev/null 2>&1 && eval "$(starship init bash)" +command -v direnv >/dev/null 2>&1 && eval "$(direnv hook bash)" diff --git a/bashrc.d/60-asdf.sh b/bashrc.d/60-asdf.sh new file mode 100644 index 0000000..f4603e1 --- /dev/null +++ b/bashrc.d/60-asdf.sh @@ -0,0 +1,13 @@ +# @file 60-asdf +# @brief get-bashed module: 60-asdf +# @description +# Runtime module loaded by get-bashed in lexicographic order. + +# asdf: full activation for interactive shells +if command -v asdf >/dev/null 2>&1; then + if [[ -r "$HOME/.asdf/asdf.sh" ]]; then + . "$HOME/.asdf/asdf.sh" + elif [[ -r "/opt/homebrew/opt/asdf/libexec/asdf.sh" ]]; then + . "/opt/homebrew/opt/asdf/libexec/asdf.sh" + fi +fi diff --git a/bashrc.d/65-tools.sh b/bashrc.d/65-tools.sh new file mode 100644 index 0000000..d12ab30 --- /dev/null +++ b/bashrc.d/65-tools.sh @@ -0,0 +1,23 @@ +# @file 65-tools +# @brief get-bashed module: 65-tools +# @description +# Runtime module loaded by get-bashed in lexicographic order. + +# Optional CLI tool installer (manual) +# Set GET_BASHED_AUTO_TOOLS=1 to run on shell startup. + +install_cli_tools() { + command -v asdf >/dev/null 2>&1 || return 0 + + if ! command -v gemini >/dev/null 2>&1; then + asdf exec npm install -g @google/gemini-cli + fi + + if ! command -v sonar >/dev/null 2>&1; then + asdf exec npm install -g @sonar/scan + fi +} + +if [[ "${GET_BASHED_AUTO_TOOLS:-0}" == "1" ]]; then + install_cli_tools +fi diff --git a/bashrc.d/66-doppler.sh b/bashrc.d/66-doppler.sh new file mode 100644 index 0000000..370dae1 --- /dev/null +++ b/bashrc.d/66-doppler.sh @@ -0,0 +1,22 @@ +# @file 66-doppler +# @brief get-bashed module: 66-doppler +# @description +# Runtime module loaded by get-bashed in lexicographic order. + +# Optional Doppler integration (requires doppler CLI) +# Enable with GET_BASHED_USE_DOPPLER=1 +# +# NOTE: We intentionally do not auto-source doppler in shell init. +# Some integrated terminals can break if doppler is invoked during startup. + +if [[ "${GET_BASHED_USE_DOPPLER:-0}" == "1" ]] && command -v doppler >/dev/null 2>&1; then + export DOPPLER_PROJECT="${DOPPLER_PROJECT:-}" + export DOPPLER_CONFIG="${DOPPLER_CONFIG:-}" + + # @description Start a subshell with doppler-injected env. + # @example + # doppler_shell + doppler_shell() { + doppler run -- bash + } +fi diff --git a/bashrc.d/70-env.sh b/bashrc.d/70-env.sh new file mode 100644 index 0000000..1f31d57 --- /dev/null +++ b/bashrc.d/70-env.sh @@ -0,0 +1,6 @@ +# @file 70-env +# @brief get-bashed module: 70-env +# @description +# Runtime module loaded by get-bashed in lexicographic order. + +# Non-secret, project-wide env (keep minimal; secrets go in 99-secrets.sh) diff --git a/bashrc.d/80-aliases.sh b/bashrc.d/80-aliases.sh new file mode 100644 index 0000000..d74a597 --- /dev/null +++ b/bashrc.d/80-aliases.sh @@ -0,0 +1,11 @@ +# @file 80-aliases +# @brief get-bashed module: 80-aliases +# @description +# Runtime module loaded by get-bashed in lexicographic order. + +alias cp="cp -i" +alias df='df -h' +alias free='free -m' +alias more=less +alias lcm="git log -1 --pretty=%B" +alias gcb="git branch --show-current" diff --git a/bashrc.d/90-functions.sh b/bashrc.d/90-functions.sh new file mode 100644 index 0000000..4732cb3 --- /dev/null +++ b/bashrc.d/90-functions.sh @@ -0,0 +1,24 @@ +# @file 90-functions +# @brief get-bashed module: 90-functions +# @description +# Runtime module loaded by get-bashed in lexicographic order. + +# Quick extractor +ex () { + local f="$1" + [[ -f "$f" ]] || { echo "'$f' is not a valid file"; return 1; } + case "$f" in + *.tar.bz2) tar xjf "$f" ;; + *.tar.gz) tar xzf "$f" ;; + *.bz2) bunzip2 "$f" ;; + *.rar) unrar x "$f" ;; + *.gz) gunzip "$f" ;; + *.tar) tar xf "$f" ;; + *.tbz2) tar xjf "$f" ;; + *.tgz) tar xzf "$f" ;; + *.zip) unzip "$f" ;; + *.Z) uncompress "$f" ;; + *.7z) 7z x "$f" ;; + *) echo "'$f' cannot be extracted via ex()" ;; + esac +} diff --git a/bashrc.d/95-ssh-agent.sh b/bashrc.d/95-ssh-agent.sh new file mode 100644 index 0000000..2908056 --- /dev/null +++ b/bashrc.d/95-ssh-agent.sh @@ -0,0 +1,11 @@ +# @file 95-ssh-agent +# @brief get-bashed module: 95-ssh-agent +# @description +# Runtime module loaded by get-bashed in lexicographic order. + +# Start SSH agent in interactive TTYs +if [[ "${GET_BASHED_SSH_AGENT:-0}" == "1" ]] && [[ -t 1 ]]; then + eval "$(ssh-agent -s)" >/dev/null + [[ -f "$HOME/.ssh/id_rsa" ]] && ssh-add "$HOME/.ssh/id_rsa" 2>/dev/null || true + [[ -f "$HOME/.ssh/id_ed25519" ]] && ssh-add "$HOME/.ssh/id_ed25519" 2>/dev/null || true +fi diff --git a/bashrc.d/99-secrets.sh b/bashrc.d/99-secrets.sh new file mode 100644 index 0000000..693a2d1 --- /dev/null +++ b/bashrc.d/99-secrets.sh @@ -0,0 +1,14 @@ +# @file 99-secrets +# @brief get-bashed module: 99-secrets +# @description +# Runtime module loaded by get-bashed in lexicographic order. + +# Source all secret snippets from ~/.get-bashed/secrets.d +GET_BASHED_HOME="${GET_BASHED_HOME:-$HOME/.get-bashed}" +GET_BASHED_SECRETS_DIR="${GET_BASHED_SECRETS_DIR:-$GET_BASHED_HOME/secrets.d}" + +if [[ -d "$GET_BASHED_SECRETS_DIR" ]]; then + for f in "$GET_BASHED_SECRETS_DIR"/*.sh; do + [[ -r "$f" ]] && source "$f" + done +fi diff --git a/bin/README.md b/bin/README.md new file mode 100644 index 0000000..68b09ea --- /dev/null +++ b/bin/README.md @@ -0,0 +1,6 @@ +# bin + +Curated helper scripts intended to be safe, portable, and generally useful. + +- `ram_usage`: macOS RAM usage analyzer. +- `gen_tool_versions`: asdf helper to pin latest installed versions. diff --git a/bin/gen_tool_versions b/bin/gen_tool_versions new file mode 100755 index 0000000..393959a --- /dev/null +++ b/bin/gen_tool_versions @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +for plugin in $(asdf plugin list); do + asdf install "$plugin" latest +done + +for plugin in $(asdf plugin list); do + versions=$(asdf list "$plugin" | sed 's/^[[:space:]]*//' | grep -v '^latest$') + if [[ -z "$versions" ]]; then + echo "No versions installed for $plugin" + continue + fi + + latest_version=$(echo "$versions" | sort -V | tail -n 1) + echo "Setting $plugin to $latest_version" + asdf set "$plugin" "$latest_version" +done diff --git a/bin/ram_usage b/bin/ram_usage new file mode 100755 index 0000000..61accef --- /dev/null +++ b/bin/ram_usage @@ -0,0 +1,311 @@ +#!/usr/bin/env python3 +""" +macRAM.py - MacOS RAM Usage Analyzer +A Python script to analyze and display RAM usage on macOS systems. +""" + +import subprocess +import re +import os +import sys +from collections import defaultdict +import math +from datetime import datetime + +# ANSI colors for terminal output +class Colors: + BLUE = '\033[94m' + GREEN = '\033[92m' + YELLOW = '\033[93m' + RED = '\033[91m' + BOLD = '\033[1m' + RESET = '\033[0m' + +def get_command_output(command): + """Run a shell command and return its output as a string.""" + try: + result = subprocess.run(command, shell=True, check=True, capture_output=True, text=True) + return result.stdout + except subprocess.CalledProcessError as e: + print(f"Error running command: {command}") + print(f"Error message: {e.stderr}") + return "" + +def get_total_ram(): + """Get the total physical RAM in the system.""" + output = get_command_output("sysctl hw.memsize") + if output: + match = re.search(r'hw.memsize: (\d+)', output) + if match: + return int(match.group(1)) + return 0 + +def format_bytes(bytes, precision=2): + """Format bytes to human-readable format.""" + if bytes == 0: + return "0 B" + + size_names = ["B", "KB", "MB", "GB", "TB"] + i = int(math.floor(math.log(bytes, 1024))) + p = math.pow(1024, i) + s = round(bytes / p, precision) + + return f"{s} {size_names[i]}" + +def parse_vm_stat(): + """Parse vm_stat output to get memory statistics.""" + output = get_command_output("vm_stat") + if not output: + return {} + + # Extract page size from the first line + first_line = output.split('\n')[0] + page_size_match = re.search(r'page size of (\d+) bytes', first_line) + if not page_size_match: + return {} + + page_size = int(page_size_match.group(1)) + + # Parse the rest of the output + memory_stats = {} + + # Define the patterns to extract from vm_stat + patterns = { + 'free': r'Pages free:\s+(\d+)', + 'active': r'Pages active:\s+(\d+)', + 'inactive': r'Pages inactive:\s+(\d+)', + 'speculative': r'Pages speculative:\s+(\d+)', + 'wired': r'Pages wired down:\s+(\d+)', + 'compressed': r'Pages occupied by compressor:\s+(\d+)', + 'purgeable': r'Pages purgeable:\s+(\d+)', + } + + for key, pattern in patterns.items(): + match = re.search(pattern, output) + if match: + # Convert from page count to bytes + pages = int(match.group(1).replace('.', '')) + memory_stats[key] = pages * page_size + + # Calculate additional metrics + memory_stats['total'] = get_total_ram() + + # Available = free + purgeable + inactive (potentially) + memory_stats['available'] = memory_stats.get('free', 0) + memory_stats.get('purgeable', 0) + + # Used = total - available + memory_stats['used'] = memory_stats['total'] - memory_stats['available'] + + # Used percentage + if memory_stats['total'] > 0: + memory_stats['used_percent'] = (memory_stats['used'] / memory_stats['total']) * 100 + else: + memory_stats['used_percent'] = 0 + + return memory_stats + +def get_process_memory(): + """Get memory usage per process.""" + output = get_command_output("ps -eo pid,rss,vsz,user,comm") + + processes = [] + for line in output.strip().split('\n')[1:]: # Skip header + parts = line.split(None, 4) + if len(parts) >= 5: + try: + pid, rss, vsz, user, command = parts + processes.append({ + 'pid': int(pid), + 'rss': int(rss) * 1024, # Convert to bytes + 'vsz': int(vsz) * 1024, # Convert to bytes + 'user': user, + 'command': command + }) + except (ValueError, IndexError): + continue + + return processes + +def group_processes_by_app(processes): + """Group processes by application and sum their memory usage.""" + app_groups = defaultdict(lambda: {'count': 0, 'memory': 0, 'pids': []}) + + for process in processes: + # Extract base app name from command + command = process['command'] + app_name = os.path.basename(command) + + # Normalize app names + if 'chrome' in command.lower() or 'google chrome' in command.lower(): + app_name = 'Google Chrome' + elif 'firefox' in command.lower(): + app_name = 'Firefox' + elif 'safari' in command.lower(): + app_name = 'Safari' + elif 'slack' in command.lower(): + app_name = 'Slack' + elif 'vs code' in command.lower() or 'code helper' in command.lower(): + app_name = 'VS Code' + elif 'cursor' in command.lower(): + app_name = 'Cursor' + elif 'iterm' in command.lower(): + app_name = 'iTerm' + elif 'terminal' in command.lower(): + app_name = 'Terminal' + elif 'finder' in command.lower(): + app_name = 'Finder' + elif 'kernel' in command.lower(): + app_name = 'Kernel' + elif 'launchd' in command.lower(): + app_name = 'System (launchd)' + elif 'windowserver' in command.lower(): + app_name = 'WindowServer' + + # Add to the group + app_groups[app_name]['count'] += 1 + app_groups[app_name]['memory'] += process['rss'] + app_groups[app_name]['pids'].append(process['pid']) + + return app_groups + +def print_header(memory_stats): + """Print header with system information.""" + print(f"{Colors.BLUE}{'=' * 60}{Colors.RESET}") + print(f"{Colors.BLUE}{Colors.BOLD} MacOS RAM Usage Monitor {Colors.RESET}") + print(f"{Colors.BLUE}{'=' * 60}{Colors.RESET}") + + total = format_bytes(memory_stats['total']) + used = format_bytes(memory_stats['used']) + available = format_bytes(memory_stats['available']) + + print(f"{Colors.GREEN}Total RAM:{Colors.RESET} {total}") + print(f"{Colors.GREEN}Used RAM:{Colors.RESET} {used} ({memory_stats['used_percent']:.2f}%)") + print(f"{Colors.GREEN}Available RAM:{Colors.RESET} {available}") + + # Memory pressure category + if memory_stats['used_percent'] < 70: + print(f"{Colors.GREEN}Memory Pressure: Low{Colors.RESET}") + elif memory_stats['used_percent'] < 85: + print(f"{Colors.YELLOW}Memory Pressure: Medium{Colors.RESET}") + else: + print(f"{Colors.RED}Memory Pressure: High{Colors.RESET}") + + print(f"{Colors.BLUE}{'=' * 60}{Colors.RESET}") + print() + +def print_memory_breakdown(memory_stats): + """Print detailed memory breakdown.""" + print(f"{Colors.GREEN}Memory Breakdown:{Colors.RESET}") + + categories = [ + ('active', "Active", "Apps currently in use"), + ('wired', "Wired", "System/kernel memory"), + ('inactive', "Inactive", "Recently used, can be freed"), + ('compressed', "Compressed", "Compressed to save space"), + ('free', "Free", "Immediately available"), + ('purgeable', "Purgeable", "Can be reclaimed if needed") + ] + + for key, name, description in categories: + if key in memory_stats: + value = format_bytes(memory_stats[key]) + print(f" {name}: {value} ({description})") + + # Calculate unaccounted memory + accounted = ( + memory_stats.get('active', 0) + + memory_stats.get('wired', 0) + + memory_stats.get('inactive', 0) + + memory_stats.get('compressed', 0) + + memory_stats.get('free', 0) + ) + + unaccounted = memory_stats['total'] - accounted + if unaccounted > 0: + print(f" Unaccounted: {format_bytes(unaccounted)} (Memory used by GPU/file cache)") + + print() + +def print_top_processes(processes, count=10): + """Print top processes by memory usage.""" + # Sort processes by RSS (Resident Set Size) in descending order + sorted_processes = sorted(processes, key=lambda p: p['rss'], reverse=True) + total_ram = get_total_ram() + + print(f"{Colors.GREEN}Top {count} Processes by RAM Usage:{Colors.RESET}") + print(f"{Colors.BLUE}{'PID':<8}{'MEM':<12}{'%MEM':<8}{'USER':<15}{'COMMAND'}{Colors.RESET}") + + for proc in sorted_processes[:count]: + mem = format_bytes(proc['rss']) + mem_percent = (proc['rss'] / total_ram) * 100 + + # Color code based on memory percentage + if mem_percent > 10: + color = Colors.RED + elif mem_percent > 5: + color = Colors.YELLOW + else: + color = '' + + print(f"{color}{proc['pid']:<8}{mem:<12}{mem_percent:.1f}%{' ':<5}{proc['user']:<15}{proc['command']}{Colors.RESET}") + + print() + +def print_app_groups(app_groups, total_ram): + """Print processes grouped by application.""" + print(f"{Colors.GREEN}Memory Usage by Application Group:{Colors.RESET}") + print(f"{Colors.BLUE}{'APPLICATION':<25}{'PROCESSES':<12}{'MEMORY':<15}{'%TOTAL'}{Colors.RESET}") + + # Sort by memory usage + sorted_apps = sorted(app_groups.items(), key=lambda x: x[1]['memory'], reverse=True) + + total_shown_memory = 0 + for app_name, data in sorted_apps[:20]: # Show top 20 + count = data['count'] + memory = data['memory'] + memory_percent = (memory / total_ram) * 100 + total_shown_memory += memory + + # Color code based on memory percentage + if memory_percent > 10: + color = Colors.RED + elif memory_percent > 5: + color = Colors.YELLOW + else: + color = '' + + print(f"{color}{app_name:<25}{count:<12}{format_bytes(memory):<15}{memory_percent:.1f}%{Colors.RESET}") + + print(f"\n{Colors.YELLOW}Total Memory from Top Apps: {format_bytes(total_shown_memory)}{Colors.RESET}") + print() + +def main(): + # Get memory statistics + memory_stats = parse_vm_stat() + if not memory_stats: + print("Error: Could not get memory statistics") + return 1 + + # Get process information + processes = get_process_memory() + if not processes: + print("Error: Could not get process information") + return 1 + + # Group processes by application + app_groups = group_processes_by_app(processes) + + # Print report + print_header(memory_stats) + print_memory_breakdown(memory_stats) + print_top_processes(processes) + print_app_groups(app_groups, memory_stats['total']) + + # Print timestamp + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + print(f"{Colors.BLUE}Report generated: {now}{Colors.RESET}") + + return 0 + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/docs/CONFIG.md b/docs/CONFIG.md new file mode 100644 index 0000000..a6fcc24 --- /dev/null +++ b/docs/CONFIG.md @@ -0,0 +1,63 @@ +# Configuration + +get-bashed writes a generated config file at: + +``` +~/.get-bashed/get-bashedrc.sh +``` + +This file contains the resolved configuration from the installer and is sourced by the runtime. You can edit it manually after install if desired. + +## Keys + +- `GET_BASHED_GNU` (0/1) +- `GET_BASHED_BUILD_FLAGS` (0/1) +- `GET_BASHED_AUTO_TOOLS` (0/1) +- `GET_BASHED_SSH_AGENT` (0/1) +- `GET_BASHED_USE_DOPPLER` (0/1) + +## Feature Bundles + +These are feature keywords that expand to installer sets: +- `dev_tools` +- `ops_tools` + +## Defaults + +If you run the installer with no flags, the defaults are conservative: + +- GNU tools: off +- Build flags: off +- Auto tools: off +- SSH agent: off +- Doppler: off + +These defaults are written into `get-bashedrc.sh` so installs are reproducible. + +## Override Install Prefix + +Set `GET_BASHED_HOME` to override the install prefix (used by the installer and runtime). +Example for CI: + +```bash +export GET_BASHED_HOME=\"$RUNNER_TEMP/get-bashed\" +./install.sh --auto --install shdoc +``` + +## Profiles + +- `minimal`: all flags off. +- `dev`: `GET_BASHED_GNU=1`, `GET_BASHED_BUILD_FLAGS=1`, `GET_BASHED_AUTO_TOOLS=1`. +- `ops`: `dev` plus `GET_BASHED_SSH_AGENT=1` and `GET_BASHED_USE_DOPPLER=1`. + +Profiles live in `profiles/*.env` and can include both `FEATURES` and `INSTALLS`. + + +## Branch Protection + +Recommended required checks: +- CI workflow (`ci.yml`) +- PR title lint (`pr-title.yml`) +- Docs build (`docs.yml`) if you publish Pages + +Also consider requiring CODEOWNERS review. diff --git a/docs/INDEX.md b/docs/INDEX.md new file mode 100644 index 0000000..10e15e3 --- /dev/null +++ b/docs/INDEX.md @@ -0,0 +1,11 @@ +# get-bashed Docs + +Generated docs: +- [CONFIG.md](CONFIG.md) +- [INDEX.md](INDEX.md) +- [INSTALLER.md](INSTALLER.md) +- [INSTALLERS.md](INSTALLERS.md) +- [INSTALLERS_HELPERS.md](INSTALLERS_HELPERS.md) +- [MODULES.md](MODULES.md) +- [README.md](README.md) +- [SHDOC.md](SHDOC.md) diff --git a/docs/INSTALLER.md b/docs/INSTALLER.md new file mode 100644 index 0000000..946f832 --- /dev/null +++ b/docs/INSTALLER.md @@ -0,0 +1,60 @@ +# get-bashed-installer + +Installer and configurator for get-bashed. + +## Overview + +Supports non-interactive and interactive installation with profiles, +feature flags, and installer bundles. + +## Index + +* [usage](#usage) +* [apply_profile](#applyprofile) +* [apply_feature](#applyfeature) +* [split_csv](#splitcsv) + +### usage + +Print usage help. + +_Function has no arguments._ + +### apply_profile + +Apply a built-in profile. + +#### Arguments + +* **$1** (string): Profile name. + +#### Exit codes + +* **0**: If applied. +* **1**: If unknown. + +### apply_feature + +Apply a feature toggle. + +#### Arguments + +* **$1** (string): Feature name (supports no- prefix). + +#### Exit codes + +* **0**: If applied. +* **1**: If unknown. + +### split_csv + +Split a comma-delimited list into space-delimited output. + +#### Arguments + +* **$1** (string): Comma list. + +#### Output on stdout + +* Space-delimited items. + diff --git a/docs/INSTALLERS.md b/docs/INSTALLERS.md new file mode 100644 index 0000000..5129a6e --- /dev/null +++ b/docs/INSTALLERS.md @@ -0,0 +1,249 @@ +# yq + +Installer: yq + +## Overview + +Installer script for get-bashed. + +## Index + +* [install_actionlint](#installactionlint) +* [install_asdf](#installasdf) +* [install_awscli](#installawscli) +* [install_bashate](#installbashate) +* [install_bat](#installbat) +* [install_bats](#installbats) +* [install_brew](#installbrew) +* [install_curl](#installcurl) +* [install_dialog](#installdialog) +* [install_direnv](#installdirenv) +* [install_doppler](#installdoppler) +* [install_fd](#installfd) +* [install_fzf](#installfzf) +* [install_gh](#installgh) +* [install_git_lfs](#installgitlfs) +* [install_git](#installgit) +* [install_gnu_tools](#installgnutools) +* [install_gnupg](#installgnupg) +* [install_helm](#installhelm) +* [install_java](#installjava) +* [install_jq](#installjq) +* [install_kubectl](#installkubectl) +* [install_nodejs](#installnodejs) +* [install_pre_commit](#installprecommit) +* [install_python](#installpython) +* [install_rg](#installrg) +* [install_shdoc](#installshdoc) +* [install_shellcheck](#installshellcheck) +* [install_starship](#installstarship) +* [install_stern](#installstern) +* [install_terraform](#installterraform) +* [install_tree](#installtree) +* [install_wget](#installwget) +* [install_yq](#installyq) + +### install_actionlint + +Run installer. + +_Function has no arguments._ + +### install_asdf + +Run installer. + +_Function has no arguments._ + +### install_awscli + +Run installer. + +_Function has no arguments._ + +### install_bashate + +Run installer. + +_Function has no arguments._ + +### install_bat + +Run installer. + +_Function has no arguments._ + +### install_bats + +Run installer. + +_Function has no arguments._ + +### install_brew + +Run installer. + +_Function has no arguments._ + +### install_curl + +Run installer. + +_Function has no arguments._ + +### install_dialog + +Run installer. + +_Function has no arguments._ + +### install_direnv + +Run installer. + +_Function has no arguments._ + +### install_doppler + +Run installer. + +_Function has no arguments._ + +### install_fd + +Run installer. + +_Function has no arguments._ + +### install_fzf + +Run installer. + +_Function has no arguments._ + +### install_gh + +Run installer. + +_Function has no arguments._ + +### install_git_lfs + +Run installer. + +_Function has no arguments._ + +### install_git + +Run installer. + +_Function has no arguments._ + +### install_gnu_tools + +Run installer. + +_Function has no arguments._ + +### install_gnupg + +Run installer. + +_Function has no arguments._ + +### install_helm + +Run installer. + +_Function has no arguments._ + +### install_java + +Run installer. + +_Function has no arguments._ + +### install_jq + +Run installer. + +_Function has no arguments._ + +### install_kubectl + +Run installer. + +_Function has no arguments._ + +### install_nodejs + +Run installer. + +_Function has no arguments._ + +### install_pre_commit + +Run installer. + +_Function has no arguments._ + +### install_python + +Run installer. + +_Function has no arguments._ + +### install_rg + +Run installer. + +_Function has no arguments._ + +### install_shdoc + +Run installer. + +_Function has no arguments._ + +### install_shellcheck + +Run installer. + +_Function has no arguments._ + +### install_starship + +Run installer. + +_Function has no arguments._ + +### install_stern + +Run installer. + +_Function has no arguments._ + +### install_terraform + +Run installer. + +_Function has no arguments._ + +### install_tree + +Run installer. + +_Function has no arguments._ + +### install_wget + +Run installer. + +_Function has no arguments._ + +### install_yq + +Run installer. + +_Function has no arguments._ + diff --git a/docs/INSTALLERS_HELPERS.md b/docs/INSTALLERS_HELPERS.md new file mode 100644 index 0000000..58c66d8 --- /dev/null +++ b/docs/INSTALLERS_HELPERS.md @@ -0,0 +1,55 @@ +# installers-helpers + +Shared helpers for installers. + +## Overview + +Provides platform detection and package manager helpers used by +installer scripts. + +## Index + +* [asdf_has_plugin](#asdfhasplugin) +* [asdf_install_plugin](#asdfinstallplugin) +* [pipx_install](#pipxinstall) + +### asdf_has_plugin + +Check if an asdf plugin is installed. + +#### Arguments + +* **$1** (string): Plugin name. + +#### Exit codes + +* **0**: If installed. +* **1**: If missing. + +### asdf_install_plugin + +Install an asdf plugin if missing. + +#### Arguments + +* **$1** (string): Plugin name. +* **$2** (string): Plugin repo (optional). + +#### Exit codes + +* **0**: If installed or already present. +* **1**: If asdf not available. + +### pipx_install + +Install a Python tool via pipx (fallback to pip). + +#### Arguments + +* **$1** (string): Package name. + +#### Exit codes + +* **0**: If installed. +* **1**: If pipx/pip missing. + diff --git a/docs/MODULES.md b/docs/MODULES.md new file mode 100644 index 0000000..84019b3 --- /dev/null +++ b/docs/MODULES.md @@ -0,0 +1,35 @@ +# 99-secrets + +get-bashed module: 99-secrets + +## Overview + +Runtime module loaded by get-bashed in lexicographic order. + +## Index + +* [_path_add_front](#pathaddfront) +* [install_cli_tools](#installclitools) +* [doppler_shell](#dopplershell) +* [ex](#ex) + +### _path_add_front + +Runtime module loaded by get-bashed in lexicographic order. + +### install_cli_tools + +Runtime module loaded by get-bashed in lexicographic order. + +### doppler_shell + +#### Example + +```bash +doppler_shell +``` + +### ex + +Runtime module loaded by get-bashed in lexicographic order. + diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..5c432cd --- /dev/null +++ b/docs/README.md @@ -0,0 +1,26 @@ +# Docs Pipeline + +This repo uses `shdoc` to generate documentation from shell scripts. + +## Generate + +```bash +./scripts/gen-docs.sh +``` + +Outputs: +- `docs/INSTALLER.md` +- `docs/INSTALLERS_HELPERS.md` +- `docs/INSTALLERS.md` +- `docs/MODULES.md` +- `docs/INDEX.md` + +## GitHub Pages + +The `docs.yml` workflow builds and publishes `docs/` to GitHub Pages. The entry +page is `docs/index.md`. + +## CI Setup + +CI uses `scripts/ci-setup.sh` to install tools into `GET_BASHED_HOME` (defaults +to `RUNNER_TEMP` on GitHub Actions). diff --git a/docs/SHDOC.md b/docs/SHDOC.md new file mode 100644 index 0000000..40e49ba --- /dev/null +++ b/docs/SHDOC.md @@ -0,0 +1,38 @@ +# shdoc + +This repo uses `shdoc` to generate documentation from shell scripts. + +## Install + +Arch Linux (AUR): +```bash +yay -S shdoc-git +``` + +Using Git (requires gawk): +```bash +sudo apt-get install gawk + +git clone --recursive https://github.com/reconquest/shdoc +cd shdoc +sudo make install +``` + +Local (no sudo) to get-bashed prefix: +```bash +GET_BASHED_HOME="$HOME/.get-bashed" +mkdir -p "$GET_BASHED_HOME/bin" + +git clone --recursive https://github.com/reconquest/shdoc +cd shdoc +make install PREFIX="$GET_BASHED_HOME" +``` + +Note: shdoc requires Bash 4+ for `;;&` case labels. On macOS, install a newer +Bash via Homebrew and use that when building from source. + +## Generate + +```bash +./scripts/gen-docs.sh +``` diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..4be8d7c --- /dev/null +++ b/install.sh @@ -0,0 +1,468 @@ +#!/usr/bin/env bash +# @file install +# @name get-bashed-installer +# @brief Installer and configurator for get-bashed. +# @description +# Supports non-interactive and interactive installation with profiles, +# feature flags, and installer bundles. + +set -euo pipefail + +# @description Print usage help. +# @noargs +usage() { + cat <<'USAGE' +Usage: install.sh [--prefix PATH] [--force] [--with-ui] + [--auto] [--yes] + [--profiles minimal|dev|ops[,..]] + [--features gnu_over_bsd,build_flags,...] + [--install brew,asdf,doppler,...] + [--list] [--list-profiles] [--list-features] [--list-installers] + [--dry-run] + +Notes: +- --auto disables prompts. +- --yes auto-accepts prompts. +- profiles set defaults; features override defaults. +USAGE +} + +REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PREFIX="${GET_BASHED_HOME:-$HOME/.get-bashed}" +FORCE=0 +WITH_UI=0 +AUTO=0 +YES=0 +PROFILES="" +FEATURES="" +INSTALLS="" +LIST=0 +DRY_RUN=0 +LIST_PROFILES=0 +LIST_FEATURES=0 +LIST_INSTALLERS=0 +GROUP_INSTALLS="" + +# Feature flags (defaults) +GET_BASHED_GNU=0 +GET_BASHED_BUILD_FLAGS=0 +GET_BASHED_AUTO_TOOLS=0 +GET_BASHED_SSH_AGENT=0 +GET_BASHED_USE_DOPPLER=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --prefix) + PREFIX="$2"; shift 2 ;; + --force) + FORCE=1; shift ;; + --with-ui) + WITH_UI=1; shift ;; + --auto|-a) + AUTO=1; shift ;; + --yes|-y) + YES=1; shift ;; + --profiles|-w) + PROFILES="$2"; shift 2 ;; + --features) + FEATURES="$2"; shift 2 ;; + --install|-i) + INSTALLS="$2"; shift 2 ;; + --list) + LIST=1; shift ;; + --list-profiles) + LIST_PROFILES=1; shift ;; + --list-features) + LIST_FEATURES=1; shift ;; + --list-installers) + LIST_INSTALLERS=1; shift ;; + --dry-run) + DRY_RUN=1; shift ;; + -h|--help) + usage; exit 0 ;; + *) + echo "Unknown argument: $1"; usage; exit 1 ;; + esac +done + +# @description Apply a built-in profile. +# @arg $1 string Profile name. +# @exitcode 0 If applied. +# @exitcode 1 If unknown. +apply_profile() { + local p="$1" + case "$p" in + minimal) + GET_BASHED_GNU=0 + GET_BASHED_BUILD_FLAGS=0 + GET_BASHED_AUTO_TOOLS=0 + GET_BASHED_SSH_AGENT=0 + GET_BASHED_USE_DOPPLER=0 + ;; + dev) + GET_BASHED_GNU=1 + GET_BASHED_BUILD_FLAGS=1 + GET_BASHED_AUTO_TOOLS=1 + GET_BASHED_SSH_AGENT=0 + GET_BASHED_USE_DOPPLER=0 + ;; + ops) + GET_BASHED_GNU=1 + GET_BASHED_BUILD_FLAGS=1 + GET_BASHED_AUTO_TOOLS=1 + GET_BASHED_SSH_AGENT=1 + GET_BASHED_USE_DOPPLER=1 + ;; + *) + return 1 + ;; + esac +} + +# @description Apply a feature toggle. +# @arg $1 string Feature name (supports no- prefix). +# @exitcode 0 If applied. +# @exitcode 1 If unknown. +apply_feature() { + local f="$1" v=1 + if [[ "$f" == no-* ]]; then + v=0 + f="${f#no-}" + fi + case "$f" in + gnu_over_bsd) GET_BASHED_GNU=$v ;; + build_flags) GET_BASHED_BUILD_FLAGS=$v ;; + auto_tools) GET_BASHED_AUTO_TOOLS=$v ;; + ssh_agent) GET_BASHED_SSH_AGENT=$v ;; + doppler_env) GET_BASHED_USE_DOPPLER=$v ;; + dev_tools) GROUP_INSTALLS="${GROUP_INSTALLS},rg,fd,bat,fzf,jq,yq,tree,direnv,starship,nodejs,python" ;; + ops_tools) GROUP_INSTALLS="${GROUP_INSTALLS},gh,git_lfs,terraform,awscli,kubectl,helm,stern,doppler,nodejs,python,java" ;; + *) return 1 ;; + esac +} + +# @description Split a comma-delimited list into space-delimited output. +# @arg $1 string Comma list. +# @stdout Space-delimited items. +split_csv() { + local s="$1"; IFS=',' read -r -a _parts <<<"$s"; echo "${_parts[@]}"; +} + +# @internal +install_dialog() { + if command -v dialog >/dev/null 2>&1; then + return 0 + fi + if command -v brew >/dev/null 2>&1; then + brew install dialog + elif command -v apt-get >/dev/null 2>&1; then + sudo apt-get update && sudo apt-get install -y dialog + elif command -v dnf >/dev/null 2>&1; then + sudo dnf install -y dialog + elif command -v yum >/dev/null 2>&1; then + sudo yum install -y dialog + fi +} + +# @internal +prompt_yes_no() { + local label="$1" answer + if [[ "$YES" -eq 1 ]]; then + return 0 + fi + read -r -p "$label [y/N]: " answer + [[ "$answer" =~ ^[Yy]$ ]] +} + +if [[ "$WITH_UI" -eq 1 ]] && [[ "$AUTO" -eq 0 ]]; then + install_dialog || true +fi + +# Apply profiles first +if [[ -n "$PROFILES" ]]; then + for p in $(split_csv "$PROFILES"); do + # Load profile file if present + PROFILE_FILE="$REPO_DIR/profiles/${p}.env" + if [[ -r "$PROFILE_FILE" ]]; then + # shellcheck disable=SC1090 + source "$PROFILE_FILE" + if [[ -n "${FEATURES:-}" ]]; then + for f in $(split_csv "$FEATURES"); do + apply_feature "$f" || { echo "Unknown feature: $f"; exit 1; } + done + fi + if [[ -n "${INSTALLS:-}" ]]; then + GROUP_INSTALLS="${GROUP_INSTALLS},${INSTALLS}" + fi + else + apply_profile "$p" || { echo "Unknown profile: $p"; exit 1; } + fi + done +fi + +# Apply features overrides +if [[ -n "$FEATURES" ]]; then + for f in $(split_csv "$FEATURES"); do + apply_feature "$f" || { echo "Unknown feature: $f"; exit 1; } + done +fi + +# Interactive selection +if [[ "$AUTO" -eq 0 ]]; then + if [[ "$WITH_UI" -eq 1 ]] && command -v dialog >/dev/null 2>&1; then + if [[ "$YES" -ne 1 ]]; then + PROFILE_CHOICE=$(dialog --clear --title "get-bashed" --menu "Select a profile" 12 60 3 \ + minimal "Minimal defaults" \ + dev "Developer workstation" \ + ops "Ops/Platform workstation" \ + 3>&1 1>&2 2>&3) || true + if [[ -n "$PROFILE_CHOICE" ]]; then + apply_profile "$PROFILE_CHOICE" + fi + + CHOICES=$(dialog --clear --title "get-bashed" --checklist "Enable features" 18 70 8 \ + gnu_over_bsd "Prefer GNU tools on macOS" "$( [[ "$GET_BASHED_GNU" -eq 1 ]] && echo on || echo off )" \ + build_flags "Enable runtime build flags" "$( [[ "$GET_BASHED_BUILD_FLAGS" -eq 1 ]] && echo on || echo off )" \ + auto_tools "Auto-install optional tools" "$( [[ "$GET_BASHED_AUTO_TOOLS" -eq 1 ]] && echo on || echo off )" \ + ssh_agent "Auto-start ssh-agent" "$( [[ "$GET_BASHED_SSH_AGENT" -eq 1 ]] && echo on || echo off )" \ + doppler_env "Enable Doppler env usage" "$( [[ "$GET_BASHED_USE_DOPPLER" -eq 1 ]] && echo on || echo off )" \ + dev_tools "Developer tool bundle" off \ + ops_tools "Ops tool bundle" off \ + 3>&1 1>&2 2>&3) || true + + GET_BASHED_GNU=0 + GET_BASHED_BUILD_FLAGS=0 + GET_BASHED_AUTO_TOOLS=0 + GET_BASHED_SSH_AGENT=0 + GET_BASHED_USE_DOPPLER=0 + + for choice in $CHOICES; do + apply_feature "${choice//\"/}" || true + done + + dialog_opts=() + for id in $INSTALLERS; do + desc_var="INSTALL_DESC_${id}" + desc="${!desc_var}" + [[ -z "$desc" ]] && desc="$id" + default_state="off" + if [[ "$id" == "dialog" ]]; then + default_state="on" + fi + dialog_opts+=("$id" "$desc" "$default_state") + done + + INSTALLS_DIALOG=$(dialog --clear --title "get-bashed" --checklist "Select installers" 20 80 12 \ + "${dialog_opts[@]}" \ + 3>&1 1>&2 2>&3) || true + if [[ -n "$INSTALLS_DIALOG" ]]; then + INSTALLS="${INSTALLS_DIALOG//\"/}" + INSTALLS="${INSTALLS// /,}" + fi + + dialog --clear --title "get-bashed" --yesno \ + "Proceed with installation?\n\nFeatures: gnu_over_bsd=${GET_BASHED_GNU} build_flags=${GET_BASHED_BUILD_FLAGS} auto_tools=${GET_BASHED_AUTO_TOOLS} ssh_agent=${GET_BASHED_SSH_AGENT} doppler_env=${GET_BASHED_USE_DOPPLER}\nInstallers: ${INSTALLS}" \ + 12 70 || exit 0 + fi + else + if [[ "$YES" -ne 1 ]]; then + echo "Configure installation options (interactive)" + read -r -p "Profile (minimal/dev/ops, enter to skip): " PROFILE_CHOICE + if [[ -n "$PROFILE_CHOICE" ]]; then + apply_profile "$PROFILE_CHOICE" || true + fi + + prompt_yes_no "Enable GNU tools on macOS (gnu_over_bsd)?" && GET_BASHED_GNU=1 + prompt_yes_no "Enable build flags (build_flags)?" && GET_BASHED_BUILD_FLAGS=1 + prompt_yes_no "Auto-install optional tools (auto_tools)?" && GET_BASHED_AUTO_TOOLS=1 + prompt_yes_no "Start ssh-agent automatically (ssh_agent)?" && GET_BASHED_SSH_AGENT=1 + prompt_yes_no "Enable Doppler env support (doppler_env)?" && GET_BASHED_USE_DOPPLER=1 + prompt_yes_no "Include developer tool bundle (dev_tools)?" && apply_feature "dev_tools" + prompt_yes_no "Include ops tool bundle (ops_tools)?" && apply_feature "ops_tools" + + read -r -p "Installers (comma list, e.g. brew,asdf,doppler): " INSTALLS_INPUT + if [[ -n "$INSTALLS_INPUT" ]]; then + INSTALLS="$INSTALLS_INPUT" + fi + + echo "Proceeding with:" + echo " Features: gnu_over_bsd=${GET_BASHED_GNU} build_flags=${GET_BASHED_BUILD_FLAGS} auto_tools=${GET_BASHED_AUTO_TOOLS} ssh_agent=${GET_BASHED_SSH_AGENT} doppler_env=${GET_BASHED_USE_DOPPLER}" + echo " Installers: ${INSTALLS}" + prompt_yes_no "Continue?" || exit 0 + fi + fi +fi + +# Merge group installs into INSTALLS +if [[ -n "${GROUP_INSTALLS:-}" ]]; then + if [[ -n "$INSTALLS" ]]; then + INSTALLS="${INSTALLS},${GROUP_INSTALLS}" + else + INSTALLS="${GROUP_INSTALLS}" + fi +fi + +# Deduplicate installers +if [[ -n "$INSTALLS" ]]; then + INSTALLS="$(echo "$INSTALLS" | tr ',' '\n' | awk 'NF && !seen[$0]++' | paste -sd, -)" +fi + +# Installer registry +INSTALLERS="" +# @internal +load_installers() { + local f + # shellcheck disable=SC1090 + source "$REPO_DIR/installers/_helpers.sh" + for f in "$REPO_DIR/installers"/*.sh; do + [[ "$f" == "$REPO_DIR/installers/_helpers.sh" ]] && continue + # shellcheck disable=SC1090 + source "$f" + INSTALLERS="$INSTALLERS $INSTALL_ID" + eval "INSTALL_DEPS_${INSTALL_ID}=\"${INSTALL_DEPS}\"" + eval "INSTALL_DESC_${INSTALL_ID}=\"${INSTALL_DESC:-}\"" + eval "INSTALL_PLATFORMS_${INSTALL_ID}=\"${INSTALL_PLATFORMS:-}\"" + done +} + +# @internal +get_deps() { + local id="$1" + eval "echo \"\${INSTALL_DEPS_${id}:-}\"" +} + +# @internal +is_done() { + local id="$1" + eval "[[ \"\${INSTALLED_${id}:-0}\" == 1 ]]" +} + +# @internal +mark_done() { + local id="$1" + eval "INSTALLED_${id}=1" +} + +# @internal +run_install() { + local id="$1" dep + if is_done "$id"; then + return 0 + fi + for dep in $(get_deps "$id"); do + run_install "$dep" + done + if [[ "$DRY_RUN" -eq 1 ]]; then + echo "would install: $id" + else + if declare -f "install_${id}" >/dev/null 2>&1; then + "install_${id}" + else + echo "Installer not found: $id" >&2 + return 1 + fi + fi + mark_done "$id" +} + +load_installers + +if [[ "$LIST_FEATURES" -eq 1 ]]; then + echo "Features:" + echo " gnu_over_bsd" + echo " build_flags" + echo " auto_tools" + echo " ssh_agent" + echo " doppler_env" + echo " dev_tools (bundle)" + echo " ops_tools (bundle)" + exit 0 +fi + +if [[ "$LIST_PROFILES" -eq 1 ]]; then + echo "Profiles:" + for p in "$REPO_DIR"/profiles/*.env; do + [[ -e "$p" ]] || continue + echo " - $(basename "$p" .env)" + done + exit 0 +fi + +if [[ "$LIST_INSTALLERS" -eq 1 || "$LIST" -eq 1 ]]; then + echo "Available installers:" + for id in $INSTALLERS; do + desc_var="INSTALL_DESC_${id}" + plat_var="INSTALL_PLATFORMS_${id}" + desc="${!desc_var}" + plats="${!plat_var}" + printf " - %s%s%s\n" "$id" \ + "$( [[ -n "$desc" ]] && printf " :: %s" "$desc" )" \ + "$( [[ -n "$plats" ]] && printf " [%s]" "$plats" )" + done + exit 0 +fi + +if [[ -n "$INSTALLS" ]]; then + export GET_BASHED_HOME="$PREFIX" + for id in $(split_csv "$INSTALLS"); do + run_install "$id" + done +fi + +# Install files +if [[ -e "$PREFIX" && "$FORCE" -ne 1 ]]; then + BACKUP="${PREFIX}.bak.$(date +%Y%m%d%H%M%S)" + echo "Backing up existing $PREFIX to $BACKUP" + mv "$PREFIX" "$BACKUP" +fi + +mkdir -p "$PREFIX" + +rsync -a \ + --exclude '.git' \ + --exclude '.github' \ + --exclude 'tests' \ + --exclude 'docs' \ + "$REPO_DIR/" "$PREFIX/" + +chmod +x "$PREFIX/bin"/* 2>/dev/null || true + +# secrets.d bootstrap +mkdir -p "$PREFIX/secrets.d" +if [[ ! -e "$PREFIX/secrets.d/00-local.sh" ]]; then + cat <<'__SECRETS__' > "$PREFIX/secrets.d/00-local.sh" +# Local secrets for get-bashed. +# This file is intentionally untracked. +__SECRETS__ +fi + +CONFIG_FILE="$PREFIX/get-bashedrc.sh" +cat <<__CFG__ > "$CONFIG_FILE" +# Generated by get-bashed installer. Edit if needed. +export GET_BASHED_GNU=${GET_BASHED_GNU} +export GET_BASHED_BUILD_FLAGS=${GET_BASHED_BUILD_FLAGS} +export GET_BASHED_AUTO_TOOLS=${GET_BASHED_AUTO_TOOLS} +export GET_BASHED_SSH_AGENT=${GET_BASHED_SSH_AGENT} +export GET_BASHED_USE_DOPPLER=${GET_BASHED_USE_DOPPLER} +__CFG__ + +# @internal +ensure_block() { + local file="$1" marker="$2" content="$3" + mkdir -p "$(dirname "$file")" + touch "$file" + if ! grep -Fqs "$marker" "$file"; then + printf '\n%s\n%s\n' "$marker" "$content" >> "$file" + fi +} + +BASHRC_LINE="# get-bashed: source modular bashrc" +BASHRC_SNIP='if [[ -r "$HOME/.get-bashed/bashrc" ]]; then source "$HOME/.get-bashed/bashrc"; fi' + +BASH_PROFILE_LINE="# get-bashed: source login bash_profile" +BASH_PROFILE_SNIP='if [[ -r "$HOME/.get-bashed/bash_profile" ]]; then source "$HOME/.get-bashed/bash_profile"; fi' + +ensure_block "$HOME/.bashrc" "$BASHRC_LINE" "$BASHRC_SNIP" +ensure_block "$HOME/.bash_profile" "$BASH_PROFILE_LINE" "$BASH_PROFILE_SNIP" + +echo "Installed get-bashed to $PREFIX" diff --git a/installers/README.md b/installers/README.md new file mode 100644 index 0000000..a911a5e --- /dev/null +++ b/installers/README.md @@ -0,0 +1,11 @@ +# installers + +Each installer is a Bash script that declares: + +- `INSTALL_ID`: unique id +- `INSTALL_DEPS`: space-delimited list of other installers +- `INSTALL_DESC`: short description +- `INSTALL_PLATFORMS`: supported platforms +- `install_()`: function that performs installation + +The main installer resolves dependencies and executes installers idempotently. diff --git a/installers/_helpers.sh b/installers/_helpers.sh new file mode 100755 index 0000000..9b56a49 --- /dev/null +++ b/installers/_helpers.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +# @file installers-helpers +# @brief Shared helpers for installers. +# @description +# Provides platform detection and package manager helpers used by +# installer scripts. + +# @internal +_using_asdf() { command -v asdf >/dev/null 2>&1; } + +# @internal +_using_brew() { command -v brew >/dev/null 2>&1; } + +# @internal +_using_git() { command -v git >/dev/null 2>&1; } + +# @internal +_using_system() { + command -v apt-get >/dev/null 2>&1 || \ + command -v dnf >/dev/null 2>&1 || \ + command -v yum >/dev/null 2>&1 || \ + command -v pacman >/dev/null 2>&1 +} + +# @internal +_using_curl() { command -v curl >/dev/null 2>&1; } + +# @internal +_using_pipx() { command -v pipx >/dev/null 2>&1; } + +# @internal +_using_pip() { command -v python3 >/dev/null 2>&1 && python3 -m pip --version >/dev/null 2>&1; } + +# @description Install a package via available system package manager. +# @arg $1 string Brew package name. +# @arg $2 string Apt package name (optional). +# @arg $3 string Dnf package name (optional). +# @arg $4 string Yum package name (optional). +# @exitcode 0 If installed. +# @exitcode 1 If no supported package manager. +pkg_install() { + local brew_pkg="$1" apt_pkg="${2:-$1}" dnf_pkg="${3:-$1}" yum_pkg="${4:-$1}" + if _using_brew; then + brew install "$brew_pkg" + elif command -v apt-get >/dev/null 2>&1; then + sudo apt-get update && sudo apt-get install -y "$apt_pkg" + elif command -v dnf >/dev/null 2>&1; then + sudo dnf install -y "$dnf_pkg" + elif command -v yum >/dev/null 2>&1; then + sudo yum install -y "$yum_pkg" + elif command -v pacman >/dev/null 2>&1; then + sudo pacman -Sy --noconfirm "$brew_pkg" + else + echo "No supported package manager found for $brew_pkg" >&2 + return 1 + fi +} + +# @description Check if an asdf plugin is installed. +# @arg $1 string Plugin name. +# @exitcode 0 If installed. +# @exitcode 1 If missing. +asdf_has_plugin() { + local plugin="$1" + _using_asdf || return 1 + asdf plugin list | awk '{print $1}' | grep -qx "$plugin" +} + +# @description Install an asdf plugin if missing. +# @arg $1 string Plugin name. +# @arg $2 string Plugin repo (optional). +# @exitcode 0 If installed or already present. +# @exitcode 1 If asdf not available. +asdf_install_plugin() { + local plugin="$1" repo="${2:-}" + _using_asdf || return 1 + if asdf_has_plugin "$plugin"; then + return 0 + fi + if [[ -n "$repo" ]]; then + asdf plugin add "$plugin" "$repo" + else + asdf plugin add "$plugin" + fi +} + +# @description Install a Python tool via pipx (fallback to pip). +# @arg $1 string Package name. +# @exitcode 0 If installed. +# @exitcode 1 If pipx/pip missing. +pipx_install() { + local pkg="$1" + if _using_pipx; then + pipx install "$pkg" + elif _using_pip; then + python3 -m pip install --user "$pkg" + else + echo "pipx or pip is required to install $pkg" >&2 + return 1 + fi +} diff --git a/installers/actionlint.sh b/installers/actionlint.sh new file mode 100755 index 0000000..1b6a39a --- /dev/null +++ b/installers/actionlint.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# @file actionlint +# @brief Installer: actionlint +# @description +# Installer script for get-bashed. + +INSTALL_ID="actionlint" +INSTALL_DEPS="" +INSTALL_DESC="actionlint" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_actionlint() { + if command -v actionlint >/dev/null 2>&1; then + return 0 + fi + pkg_install actionlint +} diff --git a/installers/asdf.sh b/installers/asdf.sh new file mode 100755 index 0000000..fea5133 --- /dev/null +++ b/installers/asdf.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# @file asdf +# @brief Installer: asdf +# @description +# Installer script for get-bashed. + +INSTALL_ID="asdf" +INSTALL_DEPS="brew" +INSTALL_DESC="asdf version manager" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_asdf() { + if _using_asdf; then + return 0 + fi + + if _using_brew; then + brew install asdf + else + echo "asdf install requires Homebrew on macOS or a supported package manager." >&2 + return 1 + fi +} diff --git a/installers/awscli.sh b/installers/awscli.sh new file mode 100755 index 0000000..86b0382 --- /dev/null +++ b/installers/awscli.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# @file awscli +# @brief Installer: awscli +# @description +# Installer script for get-bashed. + +INSTALL_ID="awscli" +INSTALL_DEPS="" +INSTALL_DESC="AWS CLI" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_awscli() { + if command -v aws >/dev/null 2>&1; then + return 0 + fi + pkg_install awscli awscli +} diff --git a/installers/bashate.sh b/installers/bashate.sh new file mode 100755 index 0000000..4027fa7 --- /dev/null +++ b/installers/bashate.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# @file bashate +# @brief Installer: bashate +# @description +# Installer script for get-bashed. + +INSTALL_ID="bashate" +INSTALL_DEPS="" +INSTALL_DESC="bashate" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_bashate() { + if command -v bashate >/dev/null 2>&1; then + return 0 + fi + pipx_install bashate +} diff --git a/installers/bat.sh b/installers/bat.sh new file mode 100755 index 0000000..26bebee --- /dev/null +++ b/installers/bat.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# @file bat +# @brief Installer: bat +# @description +# Installer script for get-bashed. + +INSTALL_ID="bat" +INSTALL_DEPS="" +INSTALL_DESC="bat" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_bat() { + if command -v bat >/dev/null 2>&1; then + return 0 + fi + pkg_install bat +} diff --git a/installers/bats.sh b/installers/bats.sh new file mode 100755 index 0000000..76433c1 --- /dev/null +++ b/installers/bats.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# @file bats +# @brief Installer: bats +# @description +# Installer script for get-bashed. + +INSTALL_ID="bats" +INSTALL_DEPS="" +INSTALL_DESC="Bats (bash testing)" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_bats() { + if command -v bats >/dev/null 2>&1; then + return 0 + fi + pkg_install bats-core bats +} diff --git a/installers/brew.sh b/installers/brew.sh new file mode 100755 index 0000000..3009a03 --- /dev/null +++ b/installers/brew.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# @file brew +# @brief Installer: brew +# @description +# Installer script for get-bashed. + +INSTALL_ID="brew" +INSTALL_DEPS="" +INSTALL_DESC="Homebrew/Linuxbrew installer" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_brew() { + if _using_brew; then + return 0 + fi + + if _using_curl; then + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + else + echo "curl is required to install Homebrew." >&2 + return 1 + fi +} diff --git a/installers/curl.sh b/installers/curl.sh new file mode 100755 index 0000000..5b658db --- /dev/null +++ b/installers/curl.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# @file curl +# @brief Installer: curl +# @description +# Installer script for get-bashed. + +INSTALL_ID="curl" +INSTALL_DEPS="" +INSTALL_DESC="curl" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_curl() { + if command -v curl >/dev/null 2>&1; then + return 0 + fi + pkg_install curl +} diff --git a/installers/dialog.sh b/installers/dialog.sh new file mode 100755 index 0000000..f025877 --- /dev/null +++ b/installers/dialog.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# @file dialog +# @brief Installer: dialog +# @description +# Installer script for get-bashed. + +INSTALL_ID="dialog" +INSTALL_DEPS="" +INSTALL_DESC="curses dialog UI" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_dialog() { + if command -v dialog >/dev/null 2>&1; then + return 0 + fi + + if _using_brew; then + brew install dialog + elif command -v apt-get >/dev/null 2>&1; then + sudo apt-get update && sudo apt-get install -y dialog + elif command -v dnf >/dev/null 2>&1; then + sudo dnf install -y dialog + elif command -v yum >/dev/null 2>&1; then + sudo yum install -y dialog + fi +} diff --git a/installers/direnv.sh b/installers/direnv.sh new file mode 100755 index 0000000..ea56b35 --- /dev/null +++ b/installers/direnv.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# @file direnv +# @brief Installer: direnv +# @description +# Installer script for get-bashed. + +INSTALL_ID="direnv" +INSTALL_DEPS="brew" +INSTALL_DESC="direnv" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_direnv() { + if command -v direnv >/dev/null 2>&1; then + return 0 + fi + + if _using_brew; then + brew install direnv + else + echo "direnv install requires Homebrew or manual install." >&2 + return 1 + fi +} diff --git a/installers/doppler.sh b/installers/doppler.sh new file mode 100755 index 0000000..ba9e52c --- /dev/null +++ b/installers/doppler.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# @file doppler +# @brief Installer: doppler +# @description +# Installer script for get-bashed. + +INSTALL_ID="doppler" +INSTALL_DEPS="brew" +INSTALL_DESC="Doppler CLI" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_doppler() { + if command -v doppler >/dev/null 2>&1; then + return 0 + fi + + if _using_brew; then + brew install doppler + else + echo "Doppler install requires Homebrew on macOS or manual install." >&2 + return 1 + fi +} diff --git a/installers/fd.sh b/installers/fd.sh new file mode 100755 index 0000000..e8ddc0c --- /dev/null +++ b/installers/fd.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# @file fd +# @brief Installer: fd +# @description +# Installer script for get-bashed. + +INSTALL_ID="fd" +INSTALL_DEPS="" +INSTALL_DESC="fd" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_fd() { + if command -v fd >/dev/null 2>&1; then + return 0 + fi + pkg_install fd +} diff --git a/installers/fzf.sh b/installers/fzf.sh new file mode 100755 index 0000000..6279efa --- /dev/null +++ b/installers/fzf.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# @file fzf +# @brief Installer: fzf +# @description +# Installer script for get-bashed. + +INSTALL_ID="fzf" +INSTALL_DEPS="" +INSTALL_DESC="fzf" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_fzf() { + if command -v fzf >/dev/null 2>&1; then + return 0 + fi + pkg_install fzf +} diff --git a/installers/gh.sh b/installers/gh.sh new file mode 100755 index 0000000..ef2cc15 --- /dev/null +++ b/installers/gh.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# @file gh +# @brief Installer: gh +# @description +# Installer script for get-bashed. + +INSTALL_ID="gh" +INSTALL_DEPS="" +INSTALL_DESC="GitHub CLI" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_gh() { + if command -v gh >/dev/null 2>&1; then + return 0 + fi + pkg_install gh +} diff --git a/installers/git-lfs.sh b/installers/git-lfs.sh new file mode 100755 index 0000000..7b3cc5e --- /dev/null +++ b/installers/git-lfs.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# @file git-lfs +# @brief Installer: git-lfs +# @description +# Installer script for get-bashed. + +INSTALL_ID="git_lfs" +INSTALL_DEPS="" +INSTALL_DESC="Git LFS" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_git_lfs() { + if command -v git-lfs >/dev/null 2>&1; then + return 0 + fi + pkg_install git-lfs git-lfs +} diff --git a/installers/git.sh b/installers/git.sh new file mode 100755 index 0000000..0e0d4b8 --- /dev/null +++ b/installers/git.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# @file git +# @brief Installer: git +# @description +# Installer script for get-bashed. + +INSTALL_ID="git" +INSTALL_DEPS="" +INSTALL_DESC="git" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_git() { + if _using_git; then + return 0 + fi + pkg_install git +} diff --git a/installers/gnu-tools.sh b/installers/gnu-tools.sh new file mode 100755 index 0000000..a2b388c --- /dev/null +++ b/installers/gnu-tools.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# @file gnu-tools +# @brief Installer: gnu-tools +# @description +# Installer script for get-bashed. + +INSTALL_ID="gnu_tools" +INSTALL_DEPS="brew" +INSTALL_DESC="GNU coreutils/findutils/sed/tar" +INSTALL_PLATFORMS="macos" + +# @description Run installer. +# @noargs +install_gnu_tools() { + if _using_brew; then + brew install coreutils findutils gnu-sed gnu-tar + else + echo "GNU tools install requires Homebrew." >&2 + return 1 + fi +} diff --git a/installers/gnupg.sh b/installers/gnupg.sh new file mode 100755 index 0000000..71cb8fe --- /dev/null +++ b/installers/gnupg.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# @file gnupg +# @brief Installer: gnupg +# @description +# Installer script for get-bashed. + +INSTALL_ID="gnupg" +INSTALL_DEPS="" +INSTALL_DESC="gnupg" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_gnupg() { + if command -v gpg >/dev/null 2>&1; then + return 0 + fi + pkg_install gnupg gnupg gnupg gnupg +} diff --git a/installers/helm.sh b/installers/helm.sh new file mode 100755 index 0000000..eb3f8b8 --- /dev/null +++ b/installers/helm.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# @file helm +# @brief Installer: helm +# @description +# Installer script for get-bashed. + +INSTALL_ID="helm" +INSTALL_DEPS="" +INSTALL_DESC="Helm" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_helm() { + if command -v helm >/dev/null 2>&1; then + return 0 + fi + pkg_install helm +} diff --git a/installers/java.sh b/installers/java.sh new file mode 100755 index 0000000..cc803d6 --- /dev/null +++ b/installers/java.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# @file java +# @brief Installer: java +# @description +# Installer script for get-bashed. + +INSTALL_ID="java" +INSTALL_DEPS="asdf" +INSTALL_DESC="Java (asdf preferred)" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_java() { + if command -v java >/dev/null 2>&1; then + return 0 + fi + + if _using_asdf; then + asdf_install_plugin java https://github.com/halcyon/asdf-java.git || true + latest_version="$(asdf latest java 2>/dev/null || true)" + if [[ -n "$latest_version" ]]; then + asdf install java "$latest_version" + asdf set --home java "$latest_version" + return 0 + fi + echo "Failed to resolve latest Java version via asdf." >&2 + return 1 + fi + + pkg_install openjdk +} diff --git a/installers/jq.sh b/installers/jq.sh new file mode 100755 index 0000000..71e1ef8 --- /dev/null +++ b/installers/jq.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# @file jq +# @brief Installer: jq +# @description +# Installer script for get-bashed. + +INSTALL_ID="jq" +INSTALL_DEPS="" +INSTALL_DESC="jq" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_jq() { + if command -v jq >/dev/null 2>&1; then + return 0 + fi + pkg_install jq +} diff --git a/installers/kubectl.sh b/installers/kubectl.sh new file mode 100755 index 0000000..615d4bb --- /dev/null +++ b/installers/kubectl.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# @file kubectl +# @brief Installer: kubectl +# @description +# Installer script for get-bashed. + +INSTALL_ID="kubectl" +INSTALL_DEPS="" +INSTALL_DESC="kubectl" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_kubectl() { + if command -v kubectl >/dev/null 2>&1; then + return 0 + fi + pkg_install kubectl kubectl +} diff --git a/installers/nodejs.sh b/installers/nodejs.sh new file mode 100755 index 0000000..9f12d90 --- /dev/null +++ b/installers/nodejs.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# @file nodejs +# @brief Installer: nodejs +# @description +# Installer script for get-bashed. + +INSTALL_ID="nodejs" +INSTALL_DEPS="asdf" +INSTALL_DESC="Node.js (asdf preferred)" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_nodejs() { + if command -v node >/dev/null 2>&1; then + return 0 + fi + + if _using_asdf; then + asdf_install_plugin nodejs || true + latest_version="$(asdf latest nodejs 2>/dev/null || true)" + if [[ -n "$latest_version" ]]; then + asdf install nodejs "$latest_version" + asdf set --home nodejs "$latest_version" + return 0 + fi + echo "Failed to resolve latest Node.js version via asdf." >&2 + return 1 + fi + + pkg_install node +} diff --git a/installers/pre-commit.sh b/installers/pre-commit.sh new file mode 100755 index 0000000..6180afc --- /dev/null +++ b/installers/pre-commit.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# @file pre-commit +# @brief Installer: pre-commit +# @description +# Installer script for get-bashed. + +INSTALL_ID="pre_commit" +INSTALL_DEPS="" +INSTALL_DESC="pre-commit" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_pre_commit() { + if command -v pre-commit >/dev/null 2>&1; then + return 0 + fi + pipx_install pre-commit +} diff --git a/installers/python.sh b/installers/python.sh new file mode 100755 index 0000000..e9d05ff --- /dev/null +++ b/installers/python.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# @file python +# @brief Installer: python +# @description +# Installer script for get-bashed. + +INSTALL_ID="python" +INSTALL_DEPS="asdf" +INSTALL_DESC="Python (asdf preferred)" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_python() { + if command -v python3 >/dev/null 2>&1; then + return 0 + fi + + if _using_asdf; then + asdf_install_plugin python || true + latest_version="$(asdf latest python 2>/dev/null || true)" + if [[ -n "$latest_version" ]]; then + asdf install python "$latest_version" + asdf set --home python "$latest_version" + return 0 + fi + echo "Failed to resolve latest Python version via asdf." >&2 + return 1 + fi + + pkg_install python +} diff --git a/installers/rg.sh b/installers/rg.sh new file mode 100755 index 0000000..5c93f9b --- /dev/null +++ b/installers/rg.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# @file rg +# @brief Installer: rg +# @description +# Installer script for get-bashed. + +INSTALL_ID="rg" +INSTALL_DEPS="" +INSTALL_DESC="ripgrep" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_rg() { + if command -v rg >/dev/null 2>&1; then + return 0 + fi + pkg_install ripgrep rg +} diff --git a/installers/shdoc.sh b/installers/shdoc.sh new file mode 100755 index 0000000..c1f32ef --- /dev/null +++ b/installers/shdoc.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# @file shdoc +# @brief Installer: shdoc +# @description +# Installer script for get-bashed. + +INSTALL_ID="shdoc" +INSTALL_DEPS="" +INSTALL_DESC="shdoc (shell script doc generator)" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_shdoc() { + if command -v shdoc >/dev/null 2>&1; then + return 0 + fi + if pkg_install shdoc; then + return 0 + fi + + if command -v yay >/dev/null 2>&1; then + yay -S --noconfirm shdoc-git && return 0 + elif command -v paru >/dev/null 2>&1; then + paru -S --noconfirm shdoc-git && return 0 + fi + + echo "shdoc is not available via the detected package manager." >&2 + echo "Attempting local install to GET_BASHED_HOME/bin without sudo." >&2 + + _using_git || { echo "git is required to build shdoc." >&2; return 1; } + pkg_install gawk gawk gawk gawk || true + pkg_install make make make make || true + + local prefix="${GET_BASHED_HOME:-$HOME/.get-bashed}" + local bindir="$prefix/bin" + mkdir -p "$bindir" + + # shdoc requires bash 4+ for ;;& case labels + local bash_bin + bash_bin="$(command -v bash)" + local bash_major + bash_major="$("$bash_bin" -c 'echo ${BASH_VERSINFO[0]:-0}' 2>/dev/null || echo 0)" + if [[ "$bash_major" -lt 4 ]] && _using_brew; then + brew install bash || true + if [[ -x "/opt/homebrew/bin/bash" ]]; then + bash_bin="/opt/homebrew/bin/bash" + elif [[ -x "/usr/local/bin/bash" ]]; then + bash_bin="/usr/local/bin/bash" + fi + fi + + tmp_dir="$(mktemp -d)" + git clone --recursive https://github.com/reconquest/shdoc "$tmp_dir/shdoc" + "$bash_bin" -lc "cd \"$tmp_dir/shdoc\" && make install PREFIX=\"$prefix\" BINDIR=\"$bindir\"" || { + echo "Failed to install shdoc locally. See https://github.com/reconquest/shdoc" >&2 + rm -rf "$tmp_dir" + return 1 + } + rm -rf "$tmp_dir" +} diff --git a/installers/shellcheck.sh b/installers/shellcheck.sh new file mode 100755 index 0000000..c9cd181 --- /dev/null +++ b/installers/shellcheck.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# @file shellcheck +# @brief Installer: shellcheck +# @description +# Installer script for get-bashed. + +INSTALL_ID="shellcheck" +INSTALL_DEPS="" +INSTALL_DESC="ShellCheck" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_shellcheck() { + if command -v shellcheck >/dev/null 2>&1; then + return 0 + fi + pkg_install shellcheck +} diff --git a/installers/starship.sh b/installers/starship.sh new file mode 100755 index 0000000..529f954 --- /dev/null +++ b/installers/starship.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# @file starship +# @brief Installer: starship +# @description +# Installer script for get-bashed. + +INSTALL_ID="starship" +INSTALL_DEPS="brew" +INSTALL_DESC="Starship prompt" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_starship() { + if command -v starship >/dev/null 2>&1; then + return 0 + fi + + if _using_brew; then + brew install starship + else + echo "Starship install requires Homebrew or manual install." >&2 + return 1 + fi +} diff --git a/installers/stern.sh b/installers/stern.sh new file mode 100755 index 0000000..cfe008a --- /dev/null +++ b/installers/stern.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# @file stern +# @brief Installer: stern +# @description +# Installer script for get-bashed. + +INSTALL_ID="stern" +INSTALL_DEPS="" +INSTALL_DESC="stern" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_stern() { + if command -v stern >/dev/null 2>&1; then + return 0 + fi + pkg_install stern +} diff --git a/installers/terraform.sh b/installers/terraform.sh new file mode 100755 index 0000000..d0af2e1 --- /dev/null +++ b/installers/terraform.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# @file terraform +# @brief Installer: terraform +# @description +# Installer script for get-bashed. + +INSTALL_ID="terraform" +INSTALL_DEPS="" +INSTALL_DESC="Terraform" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_terraform() { + if command -v terraform >/dev/null 2>&1; then + return 0 + fi + pkg_install terraform +} diff --git a/installers/tree.sh b/installers/tree.sh new file mode 100755 index 0000000..da1fd44 --- /dev/null +++ b/installers/tree.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# @file tree +# @brief Installer: tree +# @description +# Installer script for get-bashed. + +INSTALL_ID="tree" +INSTALL_DEPS="" +INSTALL_DESC="tree" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_tree() { + if command -v tree >/dev/null 2>&1; then + return 0 + fi + pkg_install tree +} diff --git a/installers/wget.sh b/installers/wget.sh new file mode 100755 index 0000000..c9c5c01 --- /dev/null +++ b/installers/wget.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# @file wget +# @brief Installer: wget +# @description +# Installer script for get-bashed. + +INSTALL_ID="wget" +INSTALL_DEPS="" +INSTALL_DESC="wget" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_wget() { + if command -v wget >/dev/null 2>&1; then + return 0 + fi + pkg_install wget +} diff --git a/installers/yq.sh b/installers/yq.sh new file mode 100755 index 0000000..8dba5d4 --- /dev/null +++ b/installers/yq.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# @file yq +# @brief Installer: yq +# @description +# Installer script for get-bashed. + +INSTALL_ID="yq" +INSTALL_DEPS="" +INSTALL_DESC="yq" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_yq() { + if command -v yq >/dev/null 2>&1; then + return 0 + fi + pkg_install yq +} diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md new file mode 100644 index 0000000..4db54dc --- /dev/null +++ b/memory-bank/activeContext.md @@ -0,0 +1,14 @@ +# Active Context + +- Focus: finalize repo hardening, docs, CI/CD, and initial release PR. +- Recent changes: + - installer refactor (profiles/features/installers, list/dry-run) + - shdoc pipeline + docs generation + - CI setup with get-bashed bootstrapping + - actions pinned to latest stable SHAs + - dependabot config + auto-merge workflow + - PR title conventional commit enforcement +- Next steps: + - regenerate docs after shdoc is installed (done) + - open release branch + PR for initial release + - verify CI docs job uses local shdoc install diff --git a/memory-bank/productContext.md b/memory-bank/productContext.md new file mode 100644 index 0000000..82636c2 --- /dev/null +++ b/memory-bank/productContext.md @@ -0,0 +1,6 @@ +# Product Context + +- Why: Provide a clean, modular Bash environment that is easy to audit and share. +- Problems solved: Ad-hoc dotfiles sprawl, poor reproducibility, unclear onboarding. +- How it should work: Installer generates config, modules load in order, optional features/installer bundles. +- UX goals: Simple install, safe defaults, clear opt-ins, cross-platform compatibility. diff --git a/memory-bank/progress.md b/memory-bank/progress.md new file mode 100644 index 0000000..8ed3b47 --- /dev/null +++ b/memory-bank/progress.md @@ -0,0 +1,16 @@ +# Progress + +## What works +- Installer framework with profiles/features/installers +- Memory bank + docs pipeline +- CI workflows and release automation +- Pre-commit policy and security docs + +## What's left +- Run `scripts/gen-docs.sh` after shdoc install (done) +- Create release branch and open PR for initial release +- Review CI for shdoc availability on all platforms + +## Known issues +- shdoc not available via Homebrew on macOS; uses local prefix install. +- Ensure docs generation uses `GET_BASHED_HOME/bin` shdoc in CI. diff --git a/memory-bank/projectbrief.md b/memory-bank/projectbrief.md new file mode 100644 index 0000000..1cd6eb0 --- /dev/null +++ b/memory-bank/projectbrief.md @@ -0,0 +1,6 @@ +# Project Brief + +- Project: get-bashed +- Goal: Portable, modular Bash setup with reproducible installs, profiles, and docs. +- Scope: Installer, modules, docs, CI/CD, and curated tool catalog. +- Non-goals: Hardcoding user-specific paths or secrets; auto-sourcing secret providers on shell startup. diff --git a/memory-bank/systemPatterns.md b/memory-bank/systemPatterns.md new file mode 100644 index 0000000..5a268a6 --- /dev/null +++ b/memory-bank/systemPatterns.md @@ -0,0 +1,7 @@ +# System Patterns + +- Modular runtime: `bashrc` loads ordered `bashrc.d/*` modules. +- Config generation: installer writes `get-bashedrc.sh` and reads it at startup. +- Installers: dependency-aware scripts with metadata and helper functions. +- Profiles/features: profiles set defaults; features override; bundles expand installers. +- Secrets: sourced from `~/.get-bashed/secrets.d/*.sh` only. diff --git a/memory-bank/techContext.md b/memory-bank/techContext.md new file mode 100644 index 0000000..33814e9 --- /dev/null +++ b/memory-bank/techContext.md @@ -0,0 +1,9 @@ +# Tech Context + +- Language: Bash +- Docs: shdoc +- Tests: BATS +- CI: GitHub Actions (pinned to action SHAs) +- Release: release-please +- Dependency updates: Dependabot + automerge workflow +- Linting: shellcheck, bashate, actionlint, gitleaks (pre-commit) diff --git a/profiles/README.md b/profiles/README.md new file mode 100644 index 0000000..d81c1e4 --- /dev/null +++ b/profiles/README.md @@ -0,0 +1,14 @@ +# profiles + +Profiles define default `FEATURES` and `INSTALLS` for the installer. + +Each profile is an `.env` file with variables: + +- `FEATURES` (comma list) +- `INSTALLS` (comma list) + +Example: +``` +FEATURES=gnu_over_bsd,build_flags,auto_tools +INSTALLS=brew,asdf,gnu_tools,rg,fd,bat,fzf,jq,yq,tree +``` diff --git a/profiles/dev.env b/profiles/dev.env new file mode 100644 index 0000000..bb6618c --- /dev/null +++ b/profiles/dev.env @@ -0,0 +1,2 @@ +FEATURES=gnu_over_bsd,build_flags,auto_tools +INSTALLS=brew,asdf,gnu_tools,rg,fd,bat,fzf,jq,yq,tree,direnv,starship,nodejs,python,bashate,shellcheck,actionlint,bats,shdoc diff --git a/profiles/minimal.env b/profiles/minimal.env new file mode 100644 index 0000000..661c935 --- /dev/null +++ b/profiles/minimal.env @@ -0,0 +1,2 @@ +FEATURES= +INSTALLS= diff --git a/profiles/ops.env b/profiles/ops.env new file mode 100644 index 0000000..1f9e847 --- /dev/null +++ b/profiles/ops.env @@ -0,0 +1,2 @@ +FEATURES=gnu_over_bsd,build_flags,auto_tools,ssh_agent,doppler_env +INSTALLS=brew,asdf,gnu_tools,rg,fd,bat,fzf,jq,yq,tree,direnv,starship,gh,git_lfs,terraform,awscli,kubectl,helm,stern,doppler,nodejs,python,java,bashate,shellcheck,actionlint,bats,shdoc diff --git a/scripts/ci-setup.sh b/scripts/ci-setup.sh new file mode 100755 index 0000000..6c197d1 --- /dev/null +++ b/scripts/ci-setup.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# @file ci-setup +# @brief CI setup using get-bashed installers. +# @description +# Detects GitHub Actions runner environment and installs tools into +# a writable prefix via get-bashed. + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +# Prefer RUNNER_TEMP, then RUNNER_TOOL_CACHE, then /tmp +PREFIX="${GET_BASHED_HOME:-${RUNNER_TEMP:-${RUNNER_TOOL_CACHE:-/tmp}}/get-bashed}" +export GET_BASHED_HOME="$PREFIX" +export PATH="$GET_BASHED_HOME/bin:$PATH" + +INSTALLS="${1:-shdoc,actionlint,shellcheck,bashate}" + +"$ROOT_DIR/install.sh" --auto --install "$INSTALLS" + +echo "CI tools installed to $GET_BASHED_HOME" diff --git a/scripts/gen-docs.sh b/scripts/gen-docs.sh new file mode 100755 index 0000000..fdb5465 --- /dev/null +++ b/scripts/gen-docs.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# @file gen-docs +# @brief Generate documentation for get-bashed. +# @description +# Uses shdoc to generate markdown docs from shell scripts. + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +command -v shdoc >/dev/null 2>&1 || { + echo "shdoc is required. Install with: ./install.sh --install shdoc" >&2 + exit 1 +} + +shdoc < "$ROOT_DIR/install.sh" > "$ROOT_DIR/docs/INSTALLER.md" +shdoc < "$ROOT_DIR/installers/_helpers.sh" > "$ROOT_DIR/docs/INSTALLERS_HELPERS.md" + +# Combine all installers into one doc +TMP_FILE="$(mktemp)" +for f in "$ROOT_DIR/installers"/*.sh; do + [[ "$f" == "$ROOT_DIR/installers/_helpers.sh" ]] && continue + echo "" >> "$TMP_FILE" + cat "$f" >> "$TMP_FILE" + echo "" >> "$TMP_FILE" + echo "# ----" >> "$TMP_FILE" + echo "" >> "$TMP_FILE" + done +shdoc < "$TMP_FILE" > "$ROOT_DIR/docs/INSTALLERS.md" +rm -f "$TMP_FILE" + +# Combine all runtime modules +TMP_MODULES="$(mktemp)" +for f in "$ROOT_DIR/bashrc.d"/*.sh; do + echo "" >> "$TMP_MODULES" + cat "$f" >> "$TMP_MODULES" + echo "" >> "$TMP_MODULES" + echo "# ----" >> "$TMP_MODULES" + echo "" >> "$TMP_MODULES" + done +shdoc < "$TMP_MODULES" > "$ROOT_DIR/docs/MODULES.md" +rm -f "$TMP_MODULES" + +# Generate index +{ + echo "# get-bashed Docs" + echo "" + echo "Generated docs:" + for f in "$ROOT_DIR/docs"/*.md; do + base="$(basename "$f")" + [[ "$base" == "index.md" ]] && continue + echo "- [$base]($base)" + done +} > "$ROOT_DIR/docs/INDEX.md" + +echo "Docs generated under docs/" diff --git a/scripts/package.sh b/scripts/package.sh new file mode 100755 index 0000000..5e79eb1 --- /dev/null +++ b/scripts/package.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# @file package +# @brief Package get-bashed into a tarball. +# @description +# Produces a versioned tarball for releases. + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +OUT_DIR="${1:-$ROOT_DIR/dist}" +VERSION="${2:-$(git describe --tags --always --dirty)}" + +mkdir -p "$OUT_DIR" +TARBALL="$OUT_DIR/get-bashed-${VERSION}.tar.gz" + +tar -czf "$TARBALL" \ + --exclude-vcs \ + --exclude='./dist' \ + --exclude='./tests' \ + --exclude='./.github' \ + -C "$ROOT_DIR" . + +echo "$TARBALL" diff --git a/scripts/pre-commit-ci.sh b/scripts/pre-commit-ci.sh new file mode 100755 index 0000000..c4c3048 --- /dev/null +++ b/scripts/pre-commit-ci.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# @file pre-commit-ci +# @brief Pre-commit runner using get-bashed installers. +# @description +# Bootstraps tools via get-bashed then runs pre-commit. + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +. "$ROOT_DIR/scripts/ci-setup.sh" "pre_commit,actionlint,shellcheck,bashate,shdoc" + +if command -v pre-commit >/dev/null 2>&1; then + pre-commit run --all-files +else + echo "pre-commit not found after install" >&2 + exit 1 +fi diff --git a/secrets.d/README.md b/secrets.d/README.md new file mode 100644 index 0000000..527dbfa --- /dev/null +++ b/secrets.d/README.md @@ -0,0 +1,9 @@ +# secrets.d + +Place local, non-versioned secret snippets here. Files ending in `.sh` are sourced by +`bashrc.d/99-secrets.sh` in lexicographic order. + +Example: +``` +~/.get-bashed/secrets.d/00-local.sh +``` diff --git a/tests/install.bats b/tests/install.bats new file mode 100755 index 0000000..9ef6dc5 --- /dev/null +++ b/tests/install.bats @@ -0,0 +1,15 @@ +#!/usr/bin/env bats + +@test "installer writes to prefix and wires bashrc" { + TMPDIR="$(mktemp -d)" + HOME="$TMPDIR" bash ./install.sh --prefix "$TMPDIR/.get-bashed" --force + + [ -f "$TMPDIR/.get-bashed/bashrc" ] + [ -d "$TMPDIR/.get-bashed/bashrc.d" ] + + run grep -F "# get-bashed: source modular bashrc" "$TMPDIR/.bashrc" + [ "$status" -eq 0 ] + + run grep -F "# get-bashed: source login bash_profile" "$TMPDIR/.bash_profile" + [ "$status" -eq 0 ] +} From 26bd94785dee55c65cec9fb4d06e73c2c040ba86 Mon Sep 17 00:00:00 2001 From: Jon B Date: Tue, 3 Feb 2026 13:45:31 -0600 Subject: [PATCH 02/33] fix: harden installer and tooling --- .pre-commit-config.yaml | 13 +++--- README.md | 6 ++- TOOLS.md | 11 ++++- bashrc | 4 ++ bashrc.d/10-helpers.sh | 1 - bashrc.d/60-asdf.sh | 2 + bin/ram_usage | 12 ++--- docs/INSTALLERS.md | 14 ++++++ install.sh | 97 +++++++++++++++++++++++++++++++++++----- installers/actionlint.sh | 51 ++++++++++++++++++++- installers/asdf.sh | 20 +++++++-- installers/bash.sh | 23 ++++++++++ installers/bashate.sh | 2 +- installers/brew.sh | 11 +++-- installers/pipx.sh | 49 ++++++++++++++++++++ installers/pre-commit.sh | 2 +- installers/shdoc.sh | 13 ++---- profiles/dev.env | 2 +- profiles/ops.env | 2 +- 19 files changed, 285 insertions(+), 50 deletions(-) create mode 100644 installers/bash.sh create mode 100755 installers/pipx.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c7d984d..02e752c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,28 +1,27 @@ repos: - repo: https://github.com/gitleaks/gitleaks - rev: v8.16.3 + rev: v8.30.0 hooks: - id: gitleaks - - repo: https://github.com/jumanjihouse/pre-commit-hooks - rev: 3.0.0 + - repo: https://github.com/gruntwork-io/pre-commit + rev: v0.1.30 hooks: - id: shellcheck - repo: https://github.com/sirosen/check-jsonschema - rev: b035497fb64e3f9faa91e833331688cc185891e6 + rev: 0.36.1 hooks: - id: check-github-workflows - repo: https://github.com/pre-commit/pre-commit-hooks - rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c + rev: v6.0.0 hooks: - id: detect-private-key - id: trailing-whitespace - id: end-of-file-fixer - repo: https://github.com/rhysd/actionlint - rev: v1.7.4 + rev: v1.7.10 hooks: - id: actionlint - repo: https://github.com/openstack/bashate rev: 2.1.1 hooks: - id: bashate - diff --git a/README.md b/README.md index b950a04..919f4da 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,9 @@ A modern, modular Bash environment you can install anywhere. get-bashed is desig ## Quick Start ```bash -curl -fsSL https://raw.githubusercontent.com/jbdevprimary/get-bashed/main/install.sh | bash +curl -fsSL -o install.sh https://raw.githubusercontent.com/jbdevprimary/get-bashed/main/install.sh +# Review install.sh before running +sh install.sh ``` ## Learn More @@ -43,7 +45,7 @@ Available installers (comma list): - `rg`, `fd`, `bat`, `fzf`, `jq`, `yq`, `tree` - `gh`, `git_lfs`, `terraform`, `awscli`, `kubectl`, `helm`, `stern` - `nodejs`, `python`, `java` -- `pre_commit`, `bashate`, `shellcheck`, `actionlint`, `bats`, `shdoc` +- `pipx`, `pre_commit`, `bashate`, `shellcheck`, `actionlint`, `bats`, `shdoc`, `bash` ## Support diff --git a/TOOLS.md b/TOOLS.md index fe39a28..2233293 100644 --- a/TOOLS.md +++ b/TOOLS.md @@ -2,6 +2,13 @@ This project favors **asdf** for multi-version language management and keeps shell behavior deterministic. +## Bash + +It is recommended to install the latest GNU Bash for full compatibility: +```bash +./install.sh --install bash +``` + ## asdf - Recommended for: Node.js, Python, Java, and other multi-version runtimes. @@ -10,8 +17,8 @@ This project favors **asdf** for multi-version language management and keeps she Example: ```bash asdf plugin add nodejs -asdf install nodejs 22.22.0 -asdf set --home nodejs 22.22.0 +asdf install nodejs lts +asdf set --home nodejs lts ``` ## Build Flags (macOS) diff --git a/bashrc b/bashrc index da30781..263e919 100644 --- a/bashrc +++ b/bashrc @@ -3,6 +3,10 @@ # @description # Loads modular runtime files in order. +# Include guard to prevent multiple sourcing +[[ -n "${GET_BASHED_SOURCED:-}" ]] && return +GET_BASHED_SOURCED=1 + # Return early if not interactive [[ $- != *i* ]] && return diff --git a/bashrc.d/10-helpers.sh b/bashrc.d/10-helpers.sh index 88ecd44..39d06e4 100644 --- a/bashrc.d/10-helpers.sh +++ b/bashrc.d/10-helpers.sh @@ -12,7 +12,6 @@ _path_dedupe() { declare -f path_add >/dev/null 2>&1 || path_add() { _path_add_front "$@" - PATH="$(_path_dedupe)" } _maybe_source() { [[ -r "$1" ]] && source "$1"; } diff --git a/bashrc.d/60-asdf.sh b/bashrc.d/60-asdf.sh index f4603e1..ab2106f 100644 --- a/bashrc.d/60-asdf.sh +++ b/bashrc.d/60-asdf.sh @@ -9,5 +9,7 @@ if command -v asdf >/dev/null 2>&1; then . "$HOME/.asdf/asdf.sh" elif [[ -r "/opt/homebrew/opt/asdf/libexec/asdf.sh" ]]; then . "/opt/homebrew/opt/asdf/libexec/asdf.sh" + elif [[ -r "/usr/local/opt/asdf/libexec/asdf.sh" ]]; then + . "/usr/local/opt/asdf/libexec/asdf.sh" fi fi diff --git a/bin/ram_usage b/bin/ram_usage index 61accef..6d5b013 100755 --- a/bin/ram_usage +++ b/bin/ram_usage @@ -22,9 +22,9 @@ class Colors: RESET = '\033[0m' def get_command_output(command): - """Run a shell command and return its output as a string.""" + """Run a command and return its output as a string.""" try: - result = subprocess.run(command, shell=True, check=True, capture_output=True, text=True) + result = subprocess.run(command, check=True, capture_output=True, text=True) return result.stdout except subprocess.CalledProcessError as e: print(f"Error running command: {command}") @@ -33,7 +33,7 @@ def get_command_output(command): def get_total_ram(): """Get the total physical RAM in the system.""" - output = get_command_output("sysctl hw.memsize") + output = get_command_output(["sysctl", "hw.memsize"]) if output: match = re.search(r'hw.memsize: (\d+)', output) if match: @@ -54,7 +54,7 @@ def format_bytes(bytes, precision=2): def parse_vm_stat(): """Parse vm_stat output to get memory statistics.""" - output = get_command_output("vm_stat") + output = get_command_output(["vm_stat"]) if not output: return {} @@ -106,7 +106,7 @@ def parse_vm_stat(): def get_process_memory(): """Get memory usage per process.""" - output = get_command_output("ps -eo pid,rss,vsz,user,comm") + output = get_command_output(["ps", "-eo", "pid,rss,vsz,user,comm"]) processes = [] for line in output.strip().split('\n')[1:]: # Skip header @@ -308,4 +308,4 @@ def main(): return 0 if __name__ == "__main__": - sys.exit(main()) \ No newline at end of file + sys.exit(main()) diff --git a/docs/INSTALLERS.md b/docs/INSTALLERS.md index 5129a6e..cc925ae 100644 --- a/docs/INSTALLERS.md +++ b/docs/INSTALLERS.md @@ -11,6 +11,7 @@ Installer script for get-bashed. * [install_actionlint](#installactionlint) * [install_asdf](#installasdf) * [install_awscli](#installawscli) +* [install_bash](#installbash) * [install_bashate](#installbashate) * [install_bat](#installbat) * [install_bats](#installbats) @@ -31,6 +32,7 @@ Installer script for get-bashed. * [install_jq](#installjq) * [install_kubectl](#installkubectl) * [install_nodejs](#installnodejs) +* [install_pipx](#installpipx) * [install_pre_commit](#installprecommit) * [install_python](#installpython) * [install_rg](#installrg) @@ -61,6 +63,12 @@ Run installer. _Function has no arguments._ +### install_bash + +Run installer. + +_Function has no arguments._ + ### install_bashate Run installer. @@ -181,6 +189,12 @@ Run installer. _Function has no arguments._ +### install_pipx + +Run installer. + +_Function has no arguments._ + ### install_pre_commit Run installer. diff --git a/install.sh b/install.sh index 4be8d7c..190bda0 100755 --- a/install.sh +++ b/install.sh @@ -1,4 +1,15 @@ -#!/usr/bin/env bash +#!/bin/sh +# POSIX shell bootstrap that re-execs with bash for full functionality. +if [ -z "${GET_BASHED_BOOTSTRAPPED:-}" ]; then + if command -v bash >/dev/null 2>&1; then + GET_BASHED_BOOTSTRAPPED=1 exec bash "$0" "$@" + fi + echo "Bash is required to run this installer." >&2 + echo "Install bash (recommended latest) and re-run: sh install.sh" >&2 + exit 1 +fi + +# shellcheck shell=bash # @file install # @name get-bashed-installer # @brief Installer and configurator for get-bashed. @@ -53,6 +64,11 @@ GET_BASHED_USE_DOPPLER=0 while [[ $# -gt 0 ]]; do case "$1" in --prefix) + if [[ $# -lt 2 ]]; then + echo "Error: --prefix requires a value" >&2 + usage + exit 1 + fi PREFIX="$2"; shift 2 ;; --force) FORCE=1; shift ;; @@ -63,10 +79,25 @@ while [[ $# -gt 0 ]]; do --yes|-y) YES=1; shift ;; --profiles|-w) + if [[ $# -lt 2 ]]; then + echo "Error: --profiles requires a value" >&2 + usage + exit 1 + fi PROFILES="$2"; shift 2 ;; --features) + if [[ $# -lt 2 ]]; then + echo "Error: --features requires a value" >&2 + usage + exit 1 + fi FEATURES="$2"; shift 2 ;; --install|-i) + if [[ $# -lt 2 ]]; then + echo "Error: --install requires a value" >&2 + usage + exit 1 + fi INSTALLS="$2"; shift 2 ;; --list) LIST=1; shift ;; @@ -135,8 +166,8 @@ apply_feature() { auto_tools) GET_BASHED_AUTO_TOOLS=$v ;; ssh_agent) GET_BASHED_SSH_AGENT=$v ;; doppler_env) GET_BASHED_USE_DOPPLER=$v ;; - dev_tools) GROUP_INSTALLS="${GROUP_INSTALLS},rg,fd,bat,fzf,jq,yq,tree,direnv,starship,nodejs,python" ;; - ops_tools) GROUP_INSTALLS="${GROUP_INSTALLS},gh,git_lfs,terraform,awscli,kubectl,helm,stern,doppler,nodejs,python,java" ;; + dev_tools) GROUP_INSTALLS="${GROUP_INSTALLS},rg,fd,bat,fzf,jq,yq,tree,direnv,starship,nodejs,python,bash" ;; + ops_tools) GROUP_INSTALLS="${GROUP_INSTALLS},gh,git_lfs,terraform,awscli,kubectl,helm,stern,doppler,nodejs,python,java,bash" ;; *) return 1 ;; esac } @@ -148,6 +179,25 @@ split_csv() { local s="$1"; IFS=',' read -r -a _parts <<<"$s"; echo "${_parts[@]}"; } +# @internal +is_valid_id() { + [[ "$1" =~ ^[a-z0-9_]+$ ]] +} + +# @internal +is_valid_profile() { + [[ "$1" =~ ^[a-z0-9_-]+$ ]] +} + +# @internal +installer_exists() { + local needle="$1" id + for id in $INSTALLERS; do + [[ "$id" == "$needle" ]] && return 0 + done + return 1 +} + # @internal install_dialog() { if command -v dialog >/dev/null 2>&1; then @@ -178,9 +228,16 @@ if [[ "$WITH_UI" -eq 1 ]] && [[ "$AUTO" -eq 0 ]]; then install_dialog || true fi +# Load installer registry early for interactive UI. +load_installers + # Apply profiles first if [[ -n "$PROFILES" ]]; then for p in $(split_csv "$PROFILES"); do + if ! is_valid_profile "$p"; then + echo "Invalid profile name: $p" >&2 + exit 1 + fi # Load profile file if present PROFILE_FILE="$REPO_DIR/profiles/${p}.env" if [[ -r "$PROFILE_FILE" ]]; then @@ -307,6 +364,16 @@ if [[ -n "$INSTALLS" ]]; then INSTALLS="$(echo "$INSTALLS" | tr ',' '\n' | awk 'NF && !seen[$0]++' | paste -sd, -)" fi +# Validate installer ids (after dedupe) +if [[ -n "$INSTALLS" ]]; then + for id in $(split_csv "$INSTALLS"); do + if ! installer_exists "$id"; then + echo "Unknown installer: $id" >&2 + exit 1 + fi + done +fi + # Installer registry INSTALLERS="" # @internal @@ -318,34 +385,44 @@ load_installers() { [[ "$f" == "$REPO_DIR/installers/_helpers.sh" ]] && continue # shellcheck disable=SC1090 source "$f" + if ! is_valid_id "$INSTALL_ID"; then + echo "Invalid installer id: $INSTALL_ID (from $f)" >&2 + exit 1 + fi INSTALLERS="$INSTALLERS $INSTALL_ID" - eval "INSTALL_DEPS_${INSTALL_ID}=\"${INSTALL_DEPS}\"" - eval "INSTALL_DESC_${INSTALL_ID}=\"${INSTALL_DESC:-}\"" - eval "INSTALL_PLATFORMS_${INSTALL_ID}=\"${INSTALL_PLATFORMS:-}\"" + printf -v "INSTALL_DEPS_${INSTALL_ID}" "%s" "${INSTALL_DEPS}" + printf -v "INSTALL_DESC_${INSTALL_ID}" "%s" "${INSTALL_DESC:-}" + printf -v "INSTALL_PLATFORMS_${INSTALL_ID}" "%s" "${INSTALL_PLATFORMS:-}" done } # @internal get_deps() { local id="$1" - eval "echo \"\${INSTALL_DEPS_${id}:-}\"" + local var="INSTALL_DEPS_${id}" + echo "${!var:-}" } # @internal is_done() { local id="$1" - eval "[[ \"\${INSTALLED_${id}:-0}\" == 1 ]]" + local var="INSTALLED_${id}" + [[ "${!var:-0}" == 1 ]] } # @internal mark_done() { local id="$1" - eval "INSTALLED_${id}=1" + printf -v "INSTALLED_${id}" "%s" 1 } # @internal run_install() { local id="$1" dep + if ! is_valid_id "$id"; then + echo "Invalid installer id: $id" >&2 + return 1 + fi if is_done "$id"; then return 0 fi @@ -365,8 +442,6 @@ run_install() { mark_done "$id" } -load_installers - if [[ "$LIST_FEATURES" -eq 1 ]]; then echo "Features:" echo " gnu_over_bsd" diff --git a/installers/actionlint.sh b/installers/actionlint.sh index 1b6a39a..d8da05d 100755 --- a/installers/actionlint.sh +++ b/installers/actionlint.sh @@ -15,5 +15,54 @@ install_actionlint() { if command -v actionlint >/dev/null 2>&1; then return 0 fi - pkg_install actionlint + + if _using_brew; then + brew install actionlint && return 0 + fi + if command -v apt-get >/dev/null 2>&1; then + if sudo apt-get update && sudo apt-get install -y actionlint; then + return 0 + fi + fi + + if ! _using_curl; then + echo "curl is required to install actionlint" >&2 + return 1 + fi + + # Fallback: download latest release binary + local tag version os arch url tmp_dir + tag="$(python3 - <<'PY' +import json +import urllib.request +u = 'https://api.github.com/repos/rhysd/actionlint/releases/latest' +print(json.load(urllib.request.urlopen(u))['tag_name']) +PY +)" + version="${tag#v}" + os="$(uname -s | tr '[:upper:]' '[:lower:]')" + arch="$(uname -m)" + case "$arch" in + x86_64) arch="amd64" ;; + arm64|aarch64) arch="arm64" ;; + esac + + if [[ "$os" == "darwin" ]]; then + os="darwin" + elif [[ "$os" == "linux" ]]; then + os="linux" + else + echo "Unsupported OS for actionlint: $os" >&2 + return 1 + fi + + url="https://github.com/rhysd/actionlint/releases/download/${tag}/actionlint_${version}_${os}_${arch}.tar.gz" + tmp_dir="$(mktemp -d)" + curl -fsSL "$url" -o "$tmp_dir/actionlint.tgz" + tar -xzf "$tmp_dir/actionlint.tgz" -C "$tmp_dir" + local prefix="${GET_BASHED_HOME:-$HOME/.get-bashed}" + mkdir -p "$prefix/bin" + mv "$tmp_dir/actionlint" "$prefix/bin/actionlint" + chmod +x "$prefix/bin/actionlint" + rm -rf "$tmp_dir" } diff --git a/installers/asdf.sh b/installers/asdf.sh index fea5133..127c918 100755 --- a/installers/asdf.sh +++ b/installers/asdf.sh @@ -18,8 +18,22 @@ install_asdf() { if _using_brew; then brew install asdf - else - echo "asdf install requires Homebrew on macOS or a supported package manager." >&2 - return 1 + return 0 fi + + if _using_git; then + if [[ -d "$HOME/.asdf" ]]; then + return 0 + fi + git clone https://github.com/asdf-vm/asdf.git "$HOME/.asdf" + if git -C "$HOME/.asdf" describe --tags --abbrev=0 >/dev/null 2>&1; then + local tag + tag="$(git -C "$HOME/.asdf" describe --tags --abbrev=0)" + git -C "$HOME/.asdf" checkout "$tag" || true + fi + return 0 + fi + + echo "asdf install requires Homebrew or git." >&2 + return 1 } diff --git a/installers/bash.sh b/installers/bash.sh new file mode 100644 index 0000000..885de5c --- /dev/null +++ b/installers/bash.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# @file bash +# @brief Installer: bash +# @description +# Installer script for get-bashed. + +INSTALL_ID="bash" +INSTALL_DEPS="" +INSTALL_DESC="Latest GNU Bash" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_bash() { + if command -v bash >/dev/null 2>&1; then + local major + major="$(bash -c 'echo ${BASH_VERSINFO[0]:-0}' 2>/dev/null || echo 0)" + if [[ "$major" -ge 4 ]]; then + return 0 + fi + fi + pkg_install bash bash bash bash +} diff --git a/installers/bashate.sh b/installers/bashate.sh index 4027fa7..8d204ce 100755 --- a/installers/bashate.sh +++ b/installers/bashate.sh @@ -5,7 +5,7 @@ # Installer script for get-bashed. INSTALL_ID="bashate" -INSTALL_DEPS="" +INSTALL_DEPS="pipx" INSTALL_DESC="bashate" INSTALL_PLATFORMS="macos,linux,wsl" diff --git a/installers/brew.sh b/installers/brew.sh index 3009a03..ba09572 100755 --- a/installers/brew.sh +++ b/installers/brew.sh @@ -16,10 +16,15 @@ install_brew() { return 0 fi - if _using_curl; then - /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" - else + if ! _using_curl; then echo "curl is required to install Homebrew." >&2 return 1 fi + + local tmp_dir + tmp_dir="$(mktemp -d)" + curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh -o "$tmp_dir/install.sh" + echo "Running Homebrew installer from $tmp_dir/install.sh" + /bin/bash "$tmp_dir/install.sh" + rm -rf "$tmp_dir" } diff --git a/installers/pipx.sh b/installers/pipx.sh new file mode 100755 index 0000000..156182b --- /dev/null +++ b/installers/pipx.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# @file pipx +# @brief Installer: pipx +# @description +# Installer script for get-bashed. + +INSTALL_ID="pipx" +INSTALL_DEPS="" +INSTALL_DESC="pipx" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_pipx() { + if _using_pipx; then + return 0 + fi + + if _using_brew; then + brew install pipx + return 0 + fi + + if command -v apt-get >/dev/null 2>&1; then + sudo apt-get update && sudo apt-get install -y pipx + return 0 + fi + if command -v dnf >/dev/null 2>&1; then + sudo dnf install -y pipx + return 0 + fi + if command -v yum >/dev/null 2>&1; then + sudo yum install -y pipx + return 0 + fi + if command -v pacman >/dev/null 2>&1; then + sudo pacman -Sy --noconfirm python-pipx + return 0 + fi + + if _using_pip; then + python3 -m pip install --user pipx + python3 -m pipx ensurepath || true + return 0 + fi + + echo "pipx install failed: no supported method" >&2 + return 1 +} diff --git a/installers/pre-commit.sh b/installers/pre-commit.sh index 6180afc..a18000b 100755 --- a/installers/pre-commit.sh +++ b/installers/pre-commit.sh @@ -5,7 +5,7 @@ # Installer script for get-bashed. INSTALL_ID="pre_commit" -INSTALL_DEPS="" +INSTALL_DEPS="pipx" INSTALL_DESC="pre-commit" INSTALL_PLATFORMS="macos,linux,wsl" diff --git a/installers/shdoc.sh b/installers/shdoc.sh index c1f32ef..97414da 100755 --- a/installers/shdoc.sh +++ b/installers/shdoc.sh @@ -37,25 +37,18 @@ install_shdoc() { mkdir -p "$bindir" # shdoc requires bash 4+ for ;;& case labels - local bash_bin - bash_bin="$(command -v bash)" local bash_major - bash_major="$("$bash_bin" -c 'echo ${BASH_VERSINFO[0]:-0}' 2>/dev/null || echo 0)" + bash_major="$(bash -c 'echo ${BASH_VERSINFO[0]:-0}' 2>/dev/null || echo 0)" if [[ "$bash_major" -lt 4 ]] && _using_brew; then brew install bash || true - if [[ -x "/opt/homebrew/bin/bash" ]]; then - bash_bin="/opt/homebrew/bin/bash" - elif [[ -x "/usr/local/bin/bash" ]]; then - bash_bin="/usr/local/bin/bash" - fi fi tmp_dir="$(mktemp -d)" git clone --recursive https://github.com/reconquest/shdoc "$tmp_dir/shdoc" - "$bash_bin" -lc "cd \"$tmp_dir/shdoc\" && make install PREFIX=\"$prefix\" BINDIR=\"$bindir\"" || { + if ! make -C "$tmp_dir/shdoc" install PREFIX="$prefix" BINDIR="$bindir"; then echo "Failed to install shdoc locally. See https://github.com/reconquest/shdoc" >&2 rm -rf "$tmp_dir" return 1 - } + fi rm -rf "$tmp_dir" } diff --git a/profiles/dev.env b/profiles/dev.env index bb6618c..f64f03c 100644 --- a/profiles/dev.env +++ b/profiles/dev.env @@ -1,2 +1,2 @@ FEATURES=gnu_over_bsd,build_flags,auto_tools -INSTALLS=brew,asdf,gnu_tools,rg,fd,bat,fzf,jq,yq,tree,direnv,starship,nodejs,python,bashate,shellcheck,actionlint,bats,shdoc +INSTALLS=brew,asdf,gnu_tools,rg,fd,bat,fzf,jq,yq,tree,direnv,starship,nodejs,python,pipx,bashate,shellcheck,actionlint,bats,shdoc,bash diff --git a/profiles/ops.env b/profiles/ops.env index 1f9e847..8ba4b03 100644 --- a/profiles/ops.env +++ b/profiles/ops.env @@ -1,2 +1,2 @@ FEATURES=gnu_over_bsd,build_flags,auto_tools,ssh_agent,doppler_env -INSTALLS=brew,asdf,gnu_tools,rg,fd,bat,fzf,jq,yq,tree,direnv,starship,gh,git_lfs,terraform,awscli,kubectl,helm,stern,doppler,nodejs,python,java,bashate,shellcheck,actionlint,bats,shdoc +INSTALLS=brew,asdf,gnu_tools,rg,fd,bat,fzf,jq,yq,tree,direnv,starship,gh,git_lfs,terraform,awscli,kubectl,helm,stern,doppler,nodejs,python,java,pipx,bashate,shellcheck,actionlint,bats,shdoc,bash From 401584c2274aaf4616ebae7b9d175050c2e7cb97 Mon Sep 17 00:00:00 2001 From: Jon B Date: Tue, 3 Feb 2026 13:49:43 -0600 Subject: [PATCH 03/33] feat: add bash-it and vimrc installers --- README.md | 2 ++ TOOLS.md | 16 ++++++++++++++++ bashrc.d/70-bash-it.sh | 13 +++++++++++++ docs/INSTALLERS.md | 14 ++++++++++++++ install.sh | 11 +++++++++-- installers/bash-it.sh | 22 ++++++++++++++++++++++ installers/vimrc.sh | 23 +++++++++++++++++++++++ 7 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 bashrc.d/70-bash-it.sh create mode 100644 installers/bash-it.sh create mode 100644 installers/vimrc.sh diff --git a/README.md b/README.md index 919f4da..a7611dc 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,8 @@ Available installers (comma list): - `gh`, `git_lfs`, `terraform`, `awscli`, `kubectl`, `helm`, `stern` - `nodejs`, `python`, `java` - `pipx`, `pre_commit`, `bashate`, `shellcheck`, `actionlint`, `bats`, `shdoc`, `bash` +- `bash_it` (bash-it framework) +- `vimrc` (amix/vimrc) ## Support diff --git a/TOOLS.md b/TOOLS.md index 2233293..507d58a 100644 --- a/TOOLS.md +++ b/TOOLS.md @@ -52,6 +52,21 @@ This adds `coreutils`, `findutils`, `gnu-sed`, and `gnu-tar` gnubin paths ahead - `@google/gemini-cli` - `@sonar/scan` +## bash-it + +Install bash-it and enable it: +```bash +./install.sh --install bash_it +./install.sh --features bash_it +``` + +## Vim (amix/vimrc) + +Install the Awesome vimrc: +```bash +./install.sh --install vimrc +``` + ## Doppler (Optional) If you use Doppler for secrets, install the CLI and enable: @@ -102,6 +117,7 @@ Use `--features` with comma-separated values: - `auto_tools` - `ssh_agent` - `doppler_env` +- `bash_it` - `dev_tools` - `ops_tools` diff --git a/bashrc.d/70-bash-it.sh b/bashrc.d/70-bash-it.sh new file mode 100644 index 0000000..b3c42a5 --- /dev/null +++ b/bashrc.d/70-bash-it.sh @@ -0,0 +1,13 @@ +# @file 70-bash-it +# @brief get-bashed module: 70-bash-it +# @description +# Optional bash-it integration. + +if [[ "${GET_BASHED_USE_BASH_IT:-0}" == "1" ]]; then + GET_BASHED_HOME="${GET_BASHED_HOME:-$HOME/.get-bashed}" + BASH_IT="$GET_BASHED_HOME/vendor/bash-it" + if [[ -r "$BASH_IT/bash_it.sh" ]]; then + # shellcheck disable=SC1090 + source "$BASH_IT/bash_it.sh" + fi +fi diff --git a/docs/INSTALLERS.md b/docs/INSTALLERS.md index cc925ae..0969708 100644 --- a/docs/INSTALLERS.md +++ b/docs/INSTALLERS.md @@ -11,6 +11,7 @@ Installer script for get-bashed. * [install_actionlint](#installactionlint) * [install_asdf](#installasdf) * [install_awscli](#installawscli) +* [install_bash_it](#installbashit) * [install_bash](#installbash) * [install_bashate](#installbashate) * [install_bat](#installbat) @@ -42,6 +43,7 @@ Installer script for get-bashed. * [install_stern](#installstern) * [install_terraform](#installterraform) * [install_tree](#installtree) +* [install_vimrc](#installvimrc) * [install_wget](#installwget) * [install_yq](#installyq) @@ -63,6 +65,12 @@ Run installer. _Function has no arguments._ +### install_bash_it + +Run installer. + +_Function has no arguments._ + ### install_bash Run installer. @@ -249,6 +257,12 @@ Run installer. _Function has no arguments._ +### install_vimrc + +Run installer. + +_Function has no arguments._ + ### install_wget Run installer. diff --git a/install.sh b/install.sh index 190bda0..b23b422 100755 --- a/install.sh +++ b/install.sh @@ -60,6 +60,7 @@ GET_BASHED_BUILD_FLAGS=0 GET_BASHED_AUTO_TOOLS=0 GET_BASHED_SSH_AGENT=0 GET_BASHED_USE_DOPPLER=0 +GET_BASHED_USE_BASH_IT=0 while [[ $# -gt 0 ]]; do case "$1" in @@ -166,6 +167,7 @@ apply_feature() { auto_tools) GET_BASHED_AUTO_TOOLS=$v ;; ssh_agent) GET_BASHED_SSH_AGENT=$v ;; doppler_env) GET_BASHED_USE_DOPPLER=$v ;; + bash_it) GET_BASHED_USE_BASH_IT=$v ;; dev_tools) GROUP_INSTALLS="${GROUP_INSTALLS},rg,fd,bat,fzf,jq,yq,tree,direnv,starship,nodejs,python,bash" ;; ops_tools) GROUP_INSTALLS="${GROUP_INSTALLS},gh,git_lfs,terraform,awscli,kubectl,helm,stern,doppler,nodejs,python,java,bash" ;; *) return 1 ;; @@ -283,6 +285,7 @@ if [[ "$AUTO" -eq 0 ]]; then auto_tools "Auto-install optional tools" "$( [[ "$GET_BASHED_AUTO_TOOLS" -eq 1 ]] && echo on || echo off )" \ ssh_agent "Auto-start ssh-agent" "$( [[ "$GET_BASHED_SSH_AGENT" -eq 1 ]] && echo on || echo off )" \ doppler_env "Enable Doppler env usage" "$( [[ "$GET_BASHED_USE_DOPPLER" -eq 1 ]] && echo on || echo off )" \ + bash_it "Enable bash-it (if installed)" "$( [[ "$GET_BASHED_USE_BASH_IT" -eq 1 ]] && echo on || echo off )" \ dev_tools "Developer tool bundle" off \ ops_tools "Ops tool bundle" off \ 3>&1 1>&2 2>&3) || true @@ -292,6 +295,7 @@ if [[ "$AUTO" -eq 0 ]]; then GET_BASHED_AUTO_TOOLS=0 GET_BASHED_SSH_AGENT=0 GET_BASHED_USE_DOPPLER=0 + GET_BASHED_USE_BASH_IT=0 for choice in $CHOICES; do apply_feature "${choice//\"/}" || true @@ -318,7 +322,7 @@ if [[ "$AUTO" -eq 0 ]]; then fi dialog --clear --title "get-bashed" --yesno \ - "Proceed with installation?\n\nFeatures: gnu_over_bsd=${GET_BASHED_GNU} build_flags=${GET_BASHED_BUILD_FLAGS} auto_tools=${GET_BASHED_AUTO_TOOLS} ssh_agent=${GET_BASHED_SSH_AGENT} doppler_env=${GET_BASHED_USE_DOPPLER}\nInstallers: ${INSTALLS}" \ + "Proceed with installation?\n\nFeatures: gnu_over_bsd=${GET_BASHED_GNU} build_flags=${GET_BASHED_BUILD_FLAGS} auto_tools=${GET_BASHED_AUTO_TOOLS} ssh_agent=${GET_BASHED_SSH_AGENT} doppler_env=${GET_BASHED_USE_DOPPLER} bash_it=${GET_BASHED_USE_BASH_IT}\nInstallers: ${INSTALLS}" \ 12 70 || exit 0 fi else @@ -334,6 +338,7 @@ if [[ "$AUTO" -eq 0 ]]; then prompt_yes_no "Auto-install optional tools (auto_tools)?" && GET_BASHED_AUTO_TOOLS=1 prompt_yes_no "Start ssh-agent automatically (ssh_agent)?" && GET_BASHED_SSH_AGENT=1 prompt_yes_no "Enable Doppler env support (doppler_env)?" && GET_BASHED_USE_DOPPLER=1 + prompt_yes_no "Enable bash-it (bash_it)?" && GET_BASHED_USE_BASH_IT=1 prompt_yes_no "Include developer tool bundle (dev_tools)?" && apply_feature "dev_tools" prompt_yes_no "Include ops tool bundle (ops_tools)?" && apply_feature "ops_tools" @@ -343,7 +348,7 @@ if [[ "$AUTO" -eq 0 ]]; then fi echo "Proceeding with:" - echo " Features: gnu_over_bsd=${GET_BASHED_GNU} build_flags=${GET_BASHED_BUILD_FLAGS} auto_tools=${GET_BASHED_AUTO_TOOLS} ssh_agent=${GET_BASHED_SSH_AGENT} doppler_env=${GET_BASHED_USE_DOPPLER}" + echo " Features: gnu_over_bsd=${GET_BASHED_GNU} build_flags=${GET_BASHED_BUILD_FLAGS} auto_tools=${GET_BASHED_AUTO_TOOLS} ssh_agent=${GET_BASHED_SSH_AGENT} doppler_env=${GET_BASHED_USE_DOPPLER} bash_it=${GET_BASHED_USE_BASH_IT}" echo " Installers: ${INSTALLS}" prompt_yes_no "Continue?" || exit 0 fi @@ -449,6 +454,7 @@ if [[ "$LIST_FEATURES" -eq 1 ]]; then echo " auto_tools" echo " ssh_agent" echo " doppler_env" + echo " bash_it" echo " dev_tools (bundle)" echo " ops_tools (bundle)" exit 0 @@ -519,6 +525,7 @@ export GET_BASHED_BUILD_FLAGS=${GET_BASHED_BUILD_FLAGS} export GET_BASHED_AUTO_TOOLS=${GET_BASHED_AUTO_TOOLS} export GET_BASHED_SSH_AGENT=${GET_BASHED_SSH_AGENT} export GET_BASHED_USE_DOPPLER=${GET_BASHED_USE_DOPPLER} +export GET_BASHED_USE_BASH_IT=${GET_BASHED_USE_BASH_IT} __CFG__ # @internal diff --git a/installers/bash-it.sh b/installers/bash-it.sh new file mode 100644 index 0000000..54e6bac --- /dev/null +++ b/installers/bash-it.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# @file bash-it +# @brief Installer: bash-it +# @description +# Installer script for get-bashed. + +INSTALL_ID="bash_it" +INSTALL_DEPS="git" +INSTALL_DESC="bash-it framework" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_bash_it() { + local prefix="${GET_BASHED_HOME:-$HOME/.get-bashed}" + local target="$prefix/vendor/bash-it" + if [[ -d "$target/.git" ]]; then + return 0 + fi + mkdir -p "$prefix/vendor" + git clone --depth=1 https://github.com/Bash-it/bash-it.git "$target" +} diff --git a/installers/vimrc.sh b/installers/vimrc.sh new file mode 100644 index 0000000..9d6c1bc --- /dev/null +++ b/installers/vimrc.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# @file vimrc +# @brief Installer: vimrc (amix) +# @description +# Installer script for get-bashed. + +INSTALL_ID="vimrc" +INSTALL_DEPS="git" +INSTALL_DESC="amix/vimrc (awesome vimrc)" +INSTALL_PLATFORMS="macos,linux,wsl" + +# @description Run installer. +# @noargs +install_vimrc() { + local prefix="${GET_BASHED_HOME:-$HOME/.get-bashed}" + local target="$prefix/vendor/vimrc" + if [[ -d "$target/.git" ]]; then + return 0 + fi + mkdir -p "$prefix/vendor" + git clone --depth=1 https://github.com/amix/vimrc.git "$target" + sh "$target/install_awesome_vimrc.sh" +} From fbfbfbdcf63b9bdef99fae49ec1da4080015be69 Mon Sep 17 00:00:00 2001 From: Jon B Date: Tue, 3 Feb 2026 13:50:25 -0600 Subject: [PATCH 04/33] fix: harden PR title check and repo refs --- .github/workflows/pr-title.yml | 6 ++++-- CODEOWNERS | 2 +- README.md | 8 ++++---- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index a0d67cf..a5230ce 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -9,9 +9,11 @@ jobs: steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Validate PR title + env: + PR_TITLE: ${{ github.event.pull_request.title }} run: | - title="${{ github.event.pull_request.title }}" - pattern='^(feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert)(\([a-z0-9._-]+\))?: .+' + title="$PR_TITLE" + pattern='^(feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert)(\([a-z0-9._-]+\))?: .+' if [[ ! "$title" =~ $pattern ]]; then echo "Invalid PR title: $title" >&2 echo "Expected Conventional Commit format, e.g., 'feat: add installer'." >&2 diff --git a/CODEOWNERS b/CODEOWNERS index 84222e3..476b1c1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1 +1 @@ -* @jbdevprimary +* @jbcom diff --git a/README.md b/README.md index a7611dc..3006e6f 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # get-bashed -[![CI](https://github.com/jbdevprimary/get-bashed/actions/workflows/ci.yml/badge.svg)](https://github.com/jbdevprimary/get-bashed/actions/workflows/ci.yml) -[![Docs](https://github.com/jbdevprimary/get-bashed/actions/workflows/docs.yml/badge.svg)](https://github.com/jbdevprimary/get-bashed/actions/workflows/docs.yml) -[![Release](https://github.com/jbdevprimary/get-bashed/actions/workflows/release-please.yml/badge.svg)](https://github.com/jbdevprimary/get-bashed/actions/workflows/release-please.yml) +[![CI](https://github.com/jbcom/get-bashed/actions/workflows/ci.yml/badge.svg)](https://github.com/jbcom/get-bashed/actions/workflows/ci.yml) +[![Docs](https://github.com/jbcom/get-bashed/actions/workflows/docs.yml/badge.svg)](https://github.com/jbcom/get-bashed/actions/workflows/docs.yml) +[![Release](https://github.com/jbcom/get-bashed/actions/workflows/release-please.yml/badge.svg)](https://github.com/jbcom/get-bashed/actions/workflows/release-please.yml) A modern, modular Bash environment you can install anywhere. get-bashed is designed to be readable, portable, and safe to extend, with a clean installer that supports interactive and non-interactive setups. @@ -17,7 +17,7 @@ A modern, modular Bash environment you can install anywhere. get-bashed is desig ## Quick Start ```bash -curl -fsSL -o install.sh https://raw.githubusercontent.com/jbdevprimary/get-bashed/main/install.sh +curl -fsSL -o install.sh https://raw.githubusercontent.com/jbcom/get-bashed/main/install.sh # Review install.sh before running sh install.sh ``` From 0b2335bbb6d94f2602627ab3a931cb81283d8df4 Mon Sep 17 00:00:00 2001 From: Jon B Date: Tue, 3 Feb 2026 13:54:09 -0600 Subject: [PATCH 05/33] fix: load installer registry before UI --- install.sh | 136 ++++++++++++++++++++++++++--------------------------- 1 file changed, 68 insertions(+), 68 deletions(-) diff --git a/install.sh b/install.sh index b23b422..43b5369 100755 --- a/install.sh +++ b/install.sh @@ -200,6 +200,74 @@ installer_exists() { return 1 } +# Installer registry +INSTALLERS="" +# @internal +load_installers() { + local f + # shellcheck disable=SC1090 + source "$REPO_DIR/installers/_helpers.sh" + for f in "$REPO_DIR/installers"/*.sh; do + [[ "$f" == "$REPO_DIR/installers/_helpers.sh" ]] && continue + # shellcheck disable=SC1090 + source "$f" + if ! is_valid_id "$INSTALL_ID"; then + echo "Invalid installer id: $INSTALL_ID (from $f)" >&2 + exit 1 + fi + INSTALLERS="$INSTALLERS $INSTALL_ID" + printf -v "INSTALL_DEPS_${INSTALL_ID}" "%s" "${INSTALL_DEPS}" + printf -v "INSTALL_DESC_${INSTALL_ID}" "%s" "${INSTALL_DESC:-}" + printf -v "INSTALL_PLATFORMS_${INSTALL_ID}" "%s" "${INSTALL_PLATFORMS:-}" + done +} + +# @internal +get_deps() { + local id="$1" + local var="INSTALL_DEPS_${id}" + echo "${!var:-}" +} + +# @internal +is_done() { + local id="$1" + local var="INSTALLED_${id}" + [[ "${!var:-0}" == 1 ]] +} + +# @internal +mark_done() { + local id="$1" + printf -v "INSTALLED_${id}" "%s" 1 +} + +# @internal +run_install() { + local id="$1" dep + if ! is_valid_id "$id"; then + echo "Invalid installer id: $id" >&2 + return 1 + fi + if is_done "$id"; then + return 0 + fi + for dep in $(get_deps "$id"); do + run_install "$dep" + done + if [[ "$DRY_RUN" -eq 1 ]]; then + echo "would install: $id" + else + if declare -f "install_${id}" >/dev/null 2>&1; then + "install_${id}" + else + echo "Installer not found: $id" >&2 + return 1 + fi + fi + mark_done "$id" +} + # @internal install_dialog() { if command -v dialog >/dev/null 2>&1; then @@ -379,74 +447,6 @@ if [[ -n "$INSTALLS" ]]; then done fi -# Installer registry -INSTALLERS="" -# @internal -load_installers() { - local f - # shellcheck disable=SC1090 - source "$REPO_DIR/installers/_helpers.sh" - for f in "$REPO_DIR/installers"/*.sh; do - [[ "$f" == "$REPO_DIR/installers/_helpers.sh" ]] && continue - # shellcheck disable=SC1090 - source "$f" - if ! is_valid_id "$INSTALL_ID"; then - echo "Invalid installer id: $INSTALL_ID (from $f)" >&2 - exit 1 - fi - INSTALLERS="$INSTALLERS $INSTALL_ID" - printf -v "INSTALL_DEPS_${INSTALL_ID}" "%s" "${INSTALL_DEPS}" - printf -v "INSTALL_DESC_${INSTALL_ID}" "%s" "${INSTALL_DESC:-}" - printf -v "INSTALL_PLATFORMS_${INSTALL_ID}" "%s" "${INSTALL_PLATFORMS:-}" - done -} - -# @internal -get_deps() { - local id="$1" - local var="INSTALL_DEPS_${id}" - echo "${!var:-}" -} - -# @internal -is_done() { - local id="$1" - local var="INSTALLED_${id}" - [[ "${!var:-0}" == 1 ]] -} - -# @internal -mark_done() { - local id="$1" - printf -v "INSTALLED_${id}" "%s" 1 -} - -# @internal -run_install() { - local id="$1" dep - if ! is_valid_id "$id"; then - echo "Invalid installer id: $id" >&2 - return 1 - fi - if is_done "$id"; then - return 0 - fi - for dep in $(get_deps "$id"); do - run_install "$dep" - done - if [[ "$DRY_RUN" -eq 1 ]]; then - echo "would install: $id" - else - if declare -f "install_${id}" >/dev/null 2>&1; then - "install_${id}" - else - echo "Installer not found: $id" >&2 - return 1 - fi - fi - mark_done "$id" -} - if [[ "$LIST_FEATURES" -eq 1 ]]; then echo "Features:" echo " gnu_over_bsd" From 805fc5e2bcb40d1299a4c0fa1c8556528e2ca74c Mon Sep 17 00:00:00 2001 From: Jon B Date: Tue, 3 Feb 2026 13:56:52 -0600 Subject: [PATCH 06/33] feat: add vimrc mode flag --- README.md | 5 +++++ TOOLS.md | 5 +++++ install.sh | 25 +++++++++++++++++++++++++ installers/vimrc.sh | 9 ++++++++- 4 files changed, 43 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3006e6f..eb282ed 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,11 @@ Available installers (comma list): - `bash_it` (bash-it framework) - `vimrc` (amix/vimrc) +Vim mode (default `awesome`): +```bash +./install.sh --install vimrc --vimrc-mode basic +``` + ## Support - Issues and feature requests: use GitHub Issues for this repository. diff --git a/TOOLS.md b/TOOLS.md index 507d58a..706d7f4 100644 --- a/TOOLS.md +++ b/TOOLS.md @@ -67,6 +67,11 @@ Install the Awesome vimrc: ./install.sh --install vimrc ``` +Install the Basic vimrc: +```bash +./install.sh --install vimrc --vimrc-mode basic +``` + ## Doppler (Optional) If you use Doppler for secrets, install the CLI and enable: diff --git a/install.sh b/install.sh index 43b5369..87c8703 100755 --- a/install.sh +++ b/install.sh @@ -28,6 +28,7 @@ Usage: install.sh [--prefix PATH] [--force] [--with-ui] [--profiles minimal|dev|ops[,..]] [--features gnu_over_bsd,build_flags,...] [--install brew,asdf,doppler,...] + [--vimrc-mode awesome|basic] [--list] [--list-profiles] [--list-features] [--list-installers] [--dry-run] @@ -53,6 +54,7 @@ LIST_PROFILES=0 LIST_FEATURES=0 LIST_INSTALLERS=0 GROUP_INSTALLS="" +VIMRC_MODE="awesome" # Feature flags (defaults) GET_BASHED_GNU=0 @@ -100,6 +102,13 @@ while [[ $# -gt 0 ]]; do exit 1 fi INSTALLS="$2"; shift 2 ;; + --vimrc-mode) + if [[ $# -lt 2 ]]; then + echo "Error: --vimrc-mode requires a value" >&2 + usage + exit 1 + fi + VIMRC_MODE="$2"; shift 2 ;; --list) LIST=1; shift ;; --list-profiles) @@ -298,6 +307,11 @@ if [[ "$WITH_UI" -eq 1 ]] && [[ "$AUTO" -eq 0 ]]; then install_dialog || true fi +# If stdin isn't a TTY, default to non-interactive. +if [[ ! -t 0 ]] && [[ "$AUTO" -eq 0 ]]; then + AUTO=1 +fi + # Load installer registry early for interactive UI. load_installers @@ -432,6 +446,15 @@ if [[ -n "${GROUP_INSTALLS:-}" ]]; then fi fi +# Validate vimrc mode +case "$VIMRC_MODE" in + awesome|basic) ;; + *) + echo "Invalid --vimrc-mode: $VIMRC_MODE (expected awesome|basic)" >&2 + exit 1 + ;; +esac + # Deduplicate installers if [[ -n "$INSTALLS" ]]; then INSTALLS="$(echo "$INSTALLS" | tr ',' '\n' | awk 'NF && !seen[$0]++' | paste -sd, -)" @@ -485,6 +508,7 @@ fi if [[ -n "$INSTALLS" ]]; then export GET_BASHED_HOME="$PREFIX" + export GET_BASHED_VIMRC_MODE="$VIMRC_MODE" for id in $(split_csv "$INSTALLS"); do run_install "$id" done @@ -526,6 +550,7 @@ export GET_BASHED_AUTO_TOOLS=${GET_BASHED_AUTO_TOOLS} export GET_BASHED_SSH_AGENT=${GET_BASHED_SSH_AGENT} export GET_BASHED_USE_DOPPLER=${GET_BASHED_USE_DOPPLER} export GET_BASHED_USE_BASH_IT=${GET_BASHED_USE_BASH_IT} +export GET_BASHED_VIMRC_MODE=${VIMRC_MODE} __CFG__ # @internal diff --git a/installers/vimrc.sh b/installers/vimrc.sh index 9d6c1bc..5c166d8 100644 --- a/installers/vimrc.sh +++ b/installers/vimrc.sh @@ -19,5 +19,12 @@ install_vimrc() { fi mkdir -p "$prefix/vendor" git clone --depth=1 https://github.com/amix/vimrc.git "$target" - sh "$target/install_awesome_vimrc.sh" + case "${GET_BASHED_VIMRC_MODE:-awesome}" in + basic) + sh "$target/install_basic_vimrc.sh" + ;; + *) + sh "$target/install_awesome_vimrc.sh" + ;; + esac } From 5b09997fe823edf8e7c58ff0c5233a773de1431f Mon Sep 17 00:00:00 2001 From: Jon B Date: Tue, 3 Feb 2026 14:03:22 -0600 Subject: [PATCH 07/33] feat: add get_bashed_component helper --- TOOLS.md | 5 +++++ bin/get_bashed_component | 45 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100755 bin/get_bashed_component diff --git a/TOOLS.md b/TOOLS.md index 706d7f4..d87779a 100644 --- a/TOOLS.md +++ b/TOOLS.md @@ -60,6 +60,11 @@ Install bash-it and enable it: ./install.sh --features bash_it ``` +Enable components via search: +```bash +get_bashed_component enable git docker +``` + ## Vim (amix/vimrc) Install the Awesome vimrc: diff --git a/bin/get_bashed_component b/bin/get_bashed_component new file mode 100755 index 0000000..58b7262 --- /dev/null +++ b/bin/get_bashed_component @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# @file get_bashed_component +# @brief Enable/disable bash-it components via search. +# @description +# Wrapper for bash-it search enable/disable. + +set -euo pipefail + +usage() { + cat <<'USAGE' +Usage: get_bashed_component [enable|disable] term1 [term2 ...] + +Examples: + get_bashed_component enable git docker + get_bashed_component disable ruby -chruby +USAGE +} + +action="${1:-}" +shift || true + +case "$action" in + enable|disable) ;; + *) + usage + exit 1 + ;; +esac + +if [[ $# -eq 0 ]]; then + usage + exit 1 +fi + +GET_BASHED_HOME="${GET_BASHED_HOME:-$HOME/.get-bashed}" +BASH_IT="$GET_BASHED_HOME/vendor/bash-it" +if [[ -r "$BASH_IT/bash_it.sh" ]]; then + # shellcheck disable=SC1090 + source "$BASH_IT/bash_it.sh" +else + echo "bash-it not found at $BASH_IT. Install with: ./install.sh --install bash_it" >&2 + exit 1 +fi + +NO_COLOR=1 bash-it search "$@" "--${action}" From ddbf7525299add18983d31cd87332e68383efe0a Mon Sep 17 00:00:00 2001 From: Jon B Date: Tue, 3 Feb 2026 14:05:21 -0600 Subject: [PATCH 08/33] feat: add graceful component resolver --- TOOLS.md | 2 + bin/get_bashed_component | 10 ++-- installers/_helpers.sh | 112 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 5 deletions(-) diff --git a/TOOLS.md b/TOOLS.md index d87779a..dc911d6 100644 --- a/TOOLS.md +++ b/TOOLS.md @@ -64,6 +64,8 @@ Enable components via search: ```bash get_bashed_component enable git docker ``` +If bash-it isn't available, it will try asdf, brew, or system package managers, +then fall back to known git/curl sources. ## Vim (amix/vimrc) diff --git a/bin/get_bashed_component b/bin/get_bashed_component index 58b7262..fca27fc 100755 --- a/bin/get_bashed_component +++ b/bin/get_bashed_component @@ -33,13 +33,13 @@ if [[ $# -eq 0 ]]; then fi GET_BASHED_HOME="${GET_BASHED_HOME:-$HOME/.get-bashed}" -BASH_IT="$GET_BASHED_HOME/vendor/bash-it" -if [[ -r "$BASH_IT/bash_it.sh" ]]; then +HELPERS="$GET_BASHED_HOME/installers/_helpers.sh" +if [[ -r "$HELPERS" ]]; then # shellcheck disable=SC1090 - source "$BASH_IT/bash_it.sh" + source "$HELPERS" else - echo "bash-it not found at $BASH_IT. Install with: ./install.sh --install bash_it" >&2 + echo "helpers not found at $HELPERS" >&2 exit 1 fi -NO_COLOR=1 bash-it search "$@" "--${action}" +component_install "$action" "$@" diff --git a/installers/_helpers.sh b/installers/_helpers.sh index 9b56a49..ed37b7a 100755 --- a/installers/_helpers.sh +++ b/installers/_helpers.sh @@ -31,6 +31,118 @@ _using_pipx() { command -v pipx >/dev/null 2>&1; } # @internal _using_pip() { command -v python3 >/dev/null 2>&1 && python3 -m pip --version >/dev/null 2>&1; } +# Known install sources (git/curl) +declare -A GET_BASHED_GIT_SOURCES=( + ["bash_it"]="https://github.com/Bash-it/bash-it.git" + ["vimrc"]="https://github.com/amix/vimrc.git" + ["shdoc"]="https://github.com/reconquest/shdoc" +) + +declare -A GET_BASHED_GIT_POST=( + ["vimrc"]="install_awesome_vimrc.sh" +) + +declare -A GET_BASHED_CURL_SOURCES=( + ["brew"]="https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh" +) + +declare -A GET_BASHED_CURL_CMD=( + ["brew"]="/bin/bash" +) + +# @internal +_bash_it_available() { + [[ "${GET_BASHED_USE_BASH_IT:-0}" == "1" ]] || return 1 + local prefix="${GET_BASHED_HOME:-$HOME/.get-bashed}" + [[ -r "$prefix/vendor/bash-it/bash_it.sh" ]] +} + +# @internal +_bash_it_search() { + local action="$1"; shift + local prefix="${GET_BASHED_HOME:-$HOME/.get-bashed}" + local bash_it="$prefix/vendor/bash-it" + # shellcheck disable=SC1090 + source "$bash_it/bash_it.sh" + NO_COLOR=1 bash-it search "$@" "--${action}" +} + +# @description Install a component using available methods. +# @arg $1 string Action (enable|disable|install). +# @arg $2 string Term to resolve/install. +component_install() { + local action="$1" term="$2" + shift 2 || true + + if [[ "$action" == "enable" || "$action" == "disable" ]]; then + if _bash_it_available; then + _bash_it_search "$action" "$term" "$@" + return 0 + fi + action="install" + fi + + if [[ "$action" != "install" ]]; then + echo "Unknown action: $action" >&2 + return 1 + fi + + if _using_asdf; then + if asdf plugin list all 2>/dev/null | awk '{print $1}' | grep -qx "$term"; then + asdf plugin add "$term" >/dev/null 2>&1 || true + asdf install "$term" latest + return $? + fi + fi + + if _using_brew; then + if brew install "$term"; then + return 0 + fi + fi + + if command -v apt-get >/dev/null 2>&1; then + if sudo apt-get update && sudo apt-get install -y "$term"; then + return 0 + fi + elif command -v dnf >/dev/null 2>&1; then + if sudo dnf install -y "$term"; then + return 0 + fi + elif command -v yum >/dev/null 2>&1; then + if sudo yum install -y "$term"; then + return 0 + fi + elif command -v pacman >/dev/null 2>&1; then + if sudo pacman -Sy --noconfirm "$term"; then + return 0 + fi + fi + + if [[ -n "${GET_BASHED_GIT_SOURCES[$term]:-}" ]] && _using_git; then + local prefix="${GET_BASHED_HOME:-$HOME/.get-bashed}" + local target="$prefix/vendor/$term" + mkdir -p "$prefix/vendor" + git clone --depth=1 "${GET_BASHED_GIT_SOURCES[$term]}" "$target" + if [[ -n "${GET_BASHED_GIT_POST[$term]:-}" ]]; then + (cd "$target" && sh "${GET_BASHED_GIT_POST[$term]}") + fi + return 0 + fi + + if [[ -n "${GET_BASHED_CURL_SOURCES[$term]:-}" ]] && _using_curl; then + local tmp_dir + tmp_dir="$(mktemp -d)" + curl -fsSL "${GET_BASHED_CURL_SOURCES[$term]}" -o "$tmp_dir/install.sh" + local cmd="${GET_BASHED_CURL_CMD[$term]:-bash}" + $cmd "$tmp_dir/install.sh" + rm -rf "$tmp_dir" + return 0 + fi + + echo "No installation method found for: $term" >&2 + return 1 +} # @description Install a package via available system package manager. # @arg $1 string Brew package name. # @arg $2 string Apt package name (optional). From 3a862b0b1342909a2b1b5a19fe48732d17dad96e Mon Sep 17 00:00:00 2001 From: Jon B Date: Tue, 3 Feb 2026 14:10:13 -0600 Subject: [PATCH 09/33] feat: unify auto-approve installs --- bashrc.d/70-bash-it.sh | 27 ++++++++++++++ install.sh | 11 ++++-- installers/_helpers.sh | 77 +++++++++++++++++++++++++++++++++++----- installers/actionlint.sh | 2 +- installers/dialog.sh | 6 ++-- installers/pipx.sh | 8 ++--- installers/shdoc.sh | 12 +++++-- 7 files changed, 122 insertions(+), 21 deletions(-) diff --git a/bashrc.d/70-bash-it.sh b/bashrc.d/70-bash-it.sh index b3c42a5..74c8de8 100644 --- a/bashrc.d/70-bash-it.sh +++ b/bashrc.d/70-bash-it.sh @@ -9,5 +9,32 @@ if [[ "${GET_BASHED_USE_BASH_IT:-0}" == "1" ]]; then if [[ -r "$BASH_IT/bash_it.sh" ]]; then # shellcheck disable=SC1090 source "$BASH_IT/bash_it.sh" + + get_bashed_component() { + local action="${1:-enable}" + shift || true + case "$action" in + enable|disable) ;; + *) action="enable" ;; + esac + NO_COLOR=1 bash-it search "$@" "--${action}" + } + + if [[ -n "${GET_BASHED_BASH_IT_SEARCH:-}" ]]; then + if [[ -z "${GET_BASHED_BASH_IT_APPLIED:-}" ]]; then + GET_BASHED_BASH_IT_APPLIED=1 + local action="${GET_BASHED_BASH_IT_ACTION:-enable}" + case "$action" in + enable|disable) ;; + *) action="enable" ;; + esac + local refresh="" + if [[ "${GET_BASHED_BASH_IT_REFRESH:-0}" == "1" ]]; then + refresh="--refresh" + fi + IFS=',' read -r -a terms <<<"${GET_BASHED_BASH_IT_SEARCH}" + NO_COLOR=1 bash-it search "${terms[@]}" "--${action}" $refresh + fi + fi fi fi diff --git a/install.sh b/install.sh index 87c8703..e1a3c98 100755 --- a/install.sh +++ b/install.sh @@ -40,6 +40,7 @@ USAGE } REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$REPO_DIR/installers/_helpers.sh" PREFIX="${GET_BASHED_HOME:-$HOME/.get-bashed}" FORCE=0 WITH_UI=0 @@ -126,6 +127,10 @@ while [[ $# -gt 0 ]]; do esac done +if [[ "$YES" -eq 1 || "$AUTO" -eq 1 ]]; then + export GET_BASHED_AUTO_APPROVE=1 +fi + # @description Apply a built-in profile. # @arg $1 string Profile name. # @exitcode 0 If applied. @@ -285,11 +290,11 @@ install_dialog() { if command -v brew >/dev/null 2>&1; then brew install dialog elif command -v apt-get >/dev/null 2>&1; then - sudo apt-get update && sudo apt-get install -y dialog + apt_install dialog elif command -v dnf >/dev/null 2>&1; then - sudo dnf install -y dialog + dnf_install dialog elif command -v yum >/dev/null 2>&1; then - sudo yum install -y dialog + yum_install dialog fi } diff --git a/installers/_helpers.sh b/installers/_helpers.sh index ed37b7a..93d9988 100755 --- a/installers/_helpers.sh +++ b/installers/_helpers.sh @@ -31,6 +31,67 @@ _using_pipx() { command -v pipx >/dev/null 2>&1; } # @internal _using_pip() { command -v python3 >/dev/null 2>&1 && python3 -m pip --version >/dev/null 2>&1; } +# @internal +_auto_approved() { [[ "${GET_BASHED_AUTO_APPROVE:-0}" == "1" ]]; } + +# @description Run a command with auto-approval when configured. +# @arg $1 string Command name. +# @arg $2 string Optional flag to auto-approve (e.g., -y, --noconfirm). +# @arg $3 string Optional extra flag (e.g., --assume-yes). +# @arg $4 string Optional extra flag (e.g., --yes). +# @arg $5 string Optional extra flag (e.g., --confirm). +# @arg $6 string Optional extra flag (e.g., --no-confirm). +auto_exec() { + local cmd="$1"; shift + local -a flags=() + if _auto_approved; then + while [[ $# -gt 0 ]]; do + [[ -n "$1" ]] && flags+=("$1") + shift + done + else + while [[ $# -gt 0 ]]; do shift; done + fi + "$cmd" "${flags[@]}" +} + +# @internal +apt_install() { + sudo apt-get update + if _auto_approved; then + sudo apt-get install -y "$@" + else + sudo apt-get install "$@" + fi +} + +# @internal +dnf_install() { + if _auto_approved; then + sudo dnf install -y "$@" + else + sudo dnf install "$@" + fi +} + +# @internal +yum_install() { + if _auto_approved; then + sudo yum install -y "$@" + else + sudo yum install "$@" + fi +} + +# @internal +pacman_install() { + if _auto_approved; then + sudo pacman -Sy --noconfirm "$@" + else + sudo pacman -Sy "$@" + fi +} + # Known install sources (git/curl) declare -A GET_BASHED_GIT_SOURCES=( ["bash_it"]="https://github.com/Bash-it/bash-it.git" @@ -102,19 +163,19 @@ component_install() { fi if command -v apt-get >/dev/null 2>&1; then - if sudo apt-get update && sudo apt-get install -y "$term"; then + if apt_install "$term"; then return 0 fi elif command -v dnf >/dev/null 2>&1; then - if sudo dnf install -y "$term"; then + if dnf_install "$term"; then return 0 fi elif command -v yum >/dev/null 2>&1; then - if sudo yum install -y "$term"; then + if yum_install "$term"; then return 0 fi elif command -v pacman >/dev/null 2>&1; then - if sudo pacman -Sy --noconfirm "$term"; then + if pacman_install "$term"; then return 0 fi fi @@ -155,13 +216,13 @@ pkg_install() { if _using_brew; then brew install "$brew_pkg" elif command -v apt-get >/dev/null 2>&1; then - sudo apt-get update && sudo apt-get install -y "$apt_pkg" + apt_install "$apt_pkg" elif command -v dnf >/dev/null 2>&1; then - sudo dnf install -y "$dnf_pkg" + dnf_install "$dnf_pkg" elif command -v yum >/dev/null 2>&1; then - sudo yum install -y "$yum_pkg" + yum_install "$yum_pkg" elif command -v pacman >/dev/null 2>&1; then - sudo pacman -Sy --noconfirm "$brew_pkg" + pacman_install "$brew_pkg" else echo "No supported package manager found for $brew_pkg" >&2 return 1 diff --git a/installers/actionlint.sh b/installers/actionlint.sh index d8da05d..9407b2f 100755 --- a/installers/actionlint.sh +++ b/installers/actionlint.sh @@ -20,7 +20,7 @@ install_actionlint() { brew install actionlint && return 0 fi if command -v apt-get >/dev/null 2>&1; then - if sudo apt-get update && sudo apt-get install -y actionlint; then + if apt_install actionlint; then return 0 fi fi diff --git a/installers/dialog.sh b/installers/dialog.sh index f025877..3fb5d76 100755 --- a/installers/dialog.sh +++ b/installers/dialog.sh @@ -19,10 +19,10 @@ install_dialog() { if _using_brew; then brew install dialog elif command -v apt-get >/dev/null 2>&1; then - sudo apt-get update && sudo apt-get install -y dialog + apt_install dialog elif command -v dnf >/dev/null 2>&1; then - sudo dnf install -y dialog + dnf_install dialog elif command -v yum >/dev/null 2>&1; then - sudo yum install -y dialog + yum_install dialog fi } diff --git a/installers/pipx.sh b/installers/pipx.sh index 156182b..554014b 100755 --- a/installers/pipx.sh +++ b/installers/pipx.sh @@ -22,19 +22,19 @@ install_pipx() { fi if command -v apt-get >/dev/null 2>&1; then - sudo apt-get update && sudo apt-get install -y pipx + apt_install pipx return 0 fi if command -v dnf >/dev/null 2>&1; then - sudo dnf install -y pipx + dnf_install pipx return 0 fi if command -v yum >/dev/null 2>&1; then - sudo yum install -y pipx + yum_install pipx return 0 fi if command -v pacman >/dev/null 2>&1; then - sudo pacman -Sy --noconfirm python-pipx + pacman_install python-pipx return 0 fi diff --git a/installers/shdoc.sh b/installers/shdoc.sh index 97414da..f61aa3e 100755 --- a/installers/shdoc.sh +++ b/installers/shdoc.sh @@ -20,9 +20,17 @@ install_shdoc() { fi if command -v yay >/dev/null 2>&1; then - yay -S --noconfirm shdoc-git && return 0 + if _auto_approved; then + yay -S --noconfirm shdoc-git && return 0 + else + yay -S shdoc-git && return 0 + fi elif command -v paru >/dev/null 2>&1; then - paru -S --noconfirm shdoc-git && return 0 + if _auto_approved; then + paru -S --noconfirm shdoc-git && return 0 + else + paru -S shdoc-git && return 0 + fi fi echo "shdoc is not available via the detected package manager." >&2 From f79f0a10c7fd098e4592b8b76e0f590cf2caff93 Mon Sep 17 00:00:00 2001 From: Jon B Date: Tue, 3 Feb 2026 14:15:46 -0600 Subject: [PATCH 10/33] refactor: centralize tool registry --- docs/INSTALLERS.md | 274 +------------------------------ docs/INSTALLERS_HELPERS.md | 66 ++++++++ docs/MODULES.md | 5 + install.sh | 25 ++- installers/README.md | 13 +- installers/_helpers.sh | 324 +++++++++++++++++++++++++++++++++++++ installers/actionlint.sh | 68 -------- installers/asdf.sh | 39 ----- installers/awscli.sh | 19 --- installers/bash-it.sh | 22 --- installers/bash.sh | 23 --- installers/bashate.sh | 19 --- installers/bat.sh | 19 --- installers/bats.sh | 19 --- installers/brew.sh | 30 ---- installers/curl.sh | 19 --- installers/dialog.sh | 28 ---- installers/direnv.sh | 25 --- installers/doppler.sh | 25 --- installers/fd.sh | 19 --- installers/fzf.sh | 19 --- installers/gh.sh | 19 --- installers/git-lfs.sh | 19 --- installers/git.sh | 19 --- installers/gnu-tools.sh | 21 --- installers/gnupg.sh | 19 --- installers/helm.sh | 19 --- installers/java.sh | 32 ---- installers/jq.sh | 19 --- installers/kubectl.sh | 19 --- installers/nodejs.sh | 32 ---- installers/pipx.sh | 49 ------ installers/pre-commit.sh | 19 --- installers/python.sh | 32 ---- installers/rg.sh | 19 --- installers/shdoc.sh | 62 ------- installers/shellcheck.sh | 19 --- installers/starship.sh | 25 --- installers/stern.sh | 19 --- installers/terraform.sh | 19 --- installers/tools.sh | 167 +++++++++++++++++++ installers/tree.sh | 19 --- installers/vimrc.sh | 30 ---- installers/wget.sh | 19 --- installers/yq.sh | 19 --- scripts/gen-docs.sh | 14 +- 46 files changed, 582 insertions(+), 1267 deletions(-) delete mode 100755 installers/actionlint.sh delete mode 100755 installers/asdf.sh delete mode 100755 installers/awscli.sh delete mode 100644 installers/bash-it.sh delete mode 100644 installers/bash.sh delete mode 100755 installers/bashate.sh delete mode 100755 installers/bat.sh delete mode 100755 installers/bats.sh delete mode 100755 installers/brew.sh delete mode 100755 installers/curl.sh delete mode 100755 installers/dialog.sh delete mode 100755 installers/direnv.sh delete mode 100755 installers/doppler.sh delete mode 100755 installers/fd.sh delete mode 100755 installers/fzf.sh delete mode 100755 installers/gh.sh delete mode 100755 installers/git-lfs.sh delete mode 100755 installers/git.sh delete mode 100755 installers/gnu-tools.sh delete mode 100755 installers/gnupg.sh delete mode 100755 installers/helm.sh delete mode 100755 installers/java.sh delete mode 100755 installers/jq.sh delete mode 100755 installers/kubectl.sh delete mode 100755 installers/nodejs.sh delete mode 100755 installers/pipx.sh delete mode 100755 installers/pre-commit.sh delete mode 100755 installers/python.sh delete mode 100755 installers/rg.sh delete mode 100755 installers/shdoc.sh delete mode 100755 installers/shellcheck.sh delete mode 100755 installers/starship.sh delete mode 100755 installers/stern.sh delete mode 100755 installers/terraform.sh create mode 100644 installers/tools.sh delete mode 100755 installers/tree.sh delete mode 100644 installers/vimrc.sh delete mode 100755 installers/wget.sh delete mode 100755 installers/yq.sh diff --git a/docs/INSTALLERS.md b/docs/INSTALLERS.md index 0969708..511691b 100644 --- a/docs/INSTALLERS.md +++ b/docs/INSTALLERS.md @@ -1,277 +1,9 @@ -# yq +# tools -Installer: yq +Tool registry for get-bashed installers. ## Overview -Installer script for get-bashed. +Defines tool metadata, dependencies, and installation methods. -## Index - -* [install_actionlint](#installactionlint) -* [install_asdf](#installasdf) -* [install_awscli](#installawscli) -* [install_bash_it](#installbashit) -* [install_bash](#installbash) -* [install_bashate](#installbashate) -* [install_bat](#installbat) -* [install_bats](#installbats) -* [install_brew](#installbrew) -* [install_curl](#installcurl) -* [install_dialog](#installdialog) -* [install_direnv](#installdirenv) -* [install_doppler](#installdoppler) -* [install_fd](#installfd) -* [install_fzf](#installfzf) -* [install_gh](#installgh) -* [install_git_lfs](#installgitlfs) -* [install_git](#installgit) -* [install_gnu_tools](#installgnutools) -* [install_gnupg](#installgnupg) -* [install_helm](#installhelm) -* [install_java](#installjava) -* [install_jq](#installjq) -* [install_kubectl](#installkubectl) -* [install_nodejs](#installnodejs) -* [install_pipx](#installpipx) -* [install_pre_commit](#installprecommit) -* [install_python](#installpython) -* [install_rg](#installrg) -* [install_shdoc](#installshdoc) -* [install_shellcheck](#installshellcheck) -* [install_starship](#installstarship) -* [install_stern](#installstern) -* [install_terraform](#installterraform) -* [install_tree](#installtree) -* [install_vimrc](#installvimrc) -* [install_wget](#installwget) -* [install_yq](#installyq) - -### install_actionlint - -Run installer. - -_Function has no arguments._ - -### install_asdf - -Run installer. - -_Function has no arguments._ - -### install_awscli - -Run installer. - -_Function has no arguments._ - -### install_bash_it - -Run installer. - -_Function has no arguments._ - -### install_bash - -Run installer. - -_Function has no arguments._ - -### install_bashate - -Run installer. - -_Function has no arguments._ - -### install_bat - -Run installer. - -_Function has no arguments._ - -### install_bats - -Run installer. - -_Function has no arguments._ - -### install_brew - -Run installer. - -_Function has no arguments._ - -### install_curl - -Run installer. - -_Function has no arguments._ - -### install_dialog - -Run installer. - -_Function has no arguments._ - -### install_direnv - -Run installer. - -_Function has no arguments._ - -### install_doppler - -Run installer. - -_Function has no arguments._ - -### install_fd - -Run installer. - -_Function has no arguments._ - -### install_fzf - -Run installer. - -_Function has no arguments._ - -### install_gh - -Run installer. - -_Function has no arguments._ - -### install_git_lfs - -Run installer. - -_Function has no arguments._ - -### install_git - -Run installer. - -_Function has no arguments._ - -### install_gnu_tools - -Run installer. - -_Function has no arguments._ - -### install_gnupg - -Run installer. - -_Function has no arguments._ - -### install_helm - -Run installer. - -_Function has no arguments._ - -### install_java - -Run installer. - -_Function has no arguments._ - -### install_jq - -Run installer. - -_Function has no arguments._ - -### install_kubectl - -Run installer. - -_Function has no arguments._ - -### install_nodejs - -Run installer. - -_Function has no arguments._ - -### install_pipx - -Run installer. - -_Function has no arguments._ - -### install_pre_commit - -Run installer. - -_Function has no arguments._ - -### install_python - -Run installer. - -_Function has no arguments._ - -### install_rg - -Run installer. - -_Function has no arguments._ - -### install_shdoc - -Run installer. - -_Function has no arguments._ - -### install_shellcheck - -Run installer. - -_Function has no arguments._ - -### install_starship - -Run installer. - -_Function has no arguments._ - -### install_stern - -Run installer. - -_Function has no arguments._ - -### install_terraform - -Run installer. - -_Function has no arguments._ - -### install_tree - -Run installer. - -_Function has no arguments._ - -### install_vimrc - -Run installer. - -_Function has no arguments._ - -### install_wget - -Run installer. - -_Function has no arguments._ - -### install_yq - -Run installer. - -_Function has no arguments._ diff --git a/docs/INSTALLERS_HELPERS.md b/docs/INSTALLERS_HELPERS.md index 58c66d8..5b3ca63 100644 --- a/docs/INSTALLERS_HELPERS.md +++ b/docs/INSTALLERS_HELPERS.md @@ -9,10 +9,76 @@ installer scripts. ## Index +* [install_tool](#installtool) +* [install_asdf](#installasdf) +* [install_gnu_tools](#installgnutools) +* [install_java](#installjava) +* [install_nodejs](#installnodejs) +* [install_python](#installpython) +* [install_shdoc](#installshdoc) +* [install_vimrc](#installvimrc) +* [install_actionlint](#installactionlint) +* [pkg_install](#pkginstall) * [asdf_has_plugin](#asdfhasplugin) * [asdf_install_plugin](#asdfinstallplugin) * [pipx_install](#pipxinstall) +### install_tool + +Install a tool from the tools registry. + +#### Arguments + +* **$1** (string): Tool id. + +### install_asdf + +Install asdf (handler). + +### install_gnu_tools + +Install GNU tools (handler). + +### install_java + +Install Java (handler). + +### install_nodejs + +Install Node.js (handler). + +### install_python + +Install Python (handler). + +### install_shdoc + +Install shdoc (handler). + +### install_vimrc + +Install vimrc (handler). + +### install_actionlint + +Install actionlint (handler). + +### pkg_install + +Install a package via available system package manager. + +#### Arguments + +* **$1** (string): Brew package name. +* **$2** (string): Apt package name (optional). +* **$3** (string): Dnf package name (optional). +* **$4** (string): Yum package name (optional). + +#### Exit codes + +* **0**: If installed. +* **1**: If no supported package manager. + ### asdf_has_plugin Check if an asdf plugin is installed. diff --git a/docs/MODULES.md b/docs/MODULES.md index 84019b3..87fd6e6 100644 --- a/docs/MODULES.md +++ b/docs/MODULES.md @@ -11,6 +11,7 @@ Runtime module loaded by get-bashed in lexicographic order. * [_path_add_front](#pathaddfront) * [install_cli_tools](#installclitools) * [doppler_shell](#dopplershell) +* [get_bashed_component](#getbashedcomponent) * [ex](#ex) ### _path_add_front @@ -29,6 +30,10 @@ Runtime module loaded by get-bashed in lexicographic order. doppler_shell ``` +### get_bashed_component + +Optional bash-it integration. + ### ex Runtime module loaded by get-bashed in lexicographic order. diff --git a/install.sh b/install.sh index e1a3c98..fafd54f 100755 --- a/install.sh +++ b/install.sh @@ -41,6 +41,7 @@ USAGE REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$REPO_DIR/installers/_helpers.sh" +source "$REPO_DIR/installers/tools.sh" PREFIX="${GET_BASHED_HOME:-$HOME/.get-bashed}" FORCE=0 WITH_UI=0 @@ -218,21 +219,16 @@ installer_exists() { INSTALLERS="" # @internal load_installers() { - local f - # shellcheck disable=SC1090 - source "$REPO_DIR/installers/_helpers.sh" - for f in "$REPO_DIR/installers"/*.sh; do - [[ "$f" == "$REPO_DIR/installers/_helpers.sh" ]] && continue - # shellcheck disable=SC1090 - source "$f" - if ! is_valid_id "$INSTALL_ID"; then - echo "Invalid installer id: $INSTALL_ID (from $f)" >&2 + local id + for id in "${TOOL_IDS[@]}"; do + if ! is_valid_id "$id"; then + echo "Invalid installer id: $id (from tools.sh)" >&2 exit 1 fi - INSTALLERS="$INSTALLERS $INSTALL_ID" - printf -v "INSTALL_DEPS_${INSTALL_ID}" "%s" "${INSTALL_DEPS}" - printf -v "INSTALL_DESC_${INSTALL_ID}" "%s" "${INSTALL_DESC:-}" - printf -v "INSTALL_PLATFORMS_${INSTALL_ID}" "%s" "${INSTALL_PLATFORMS:-}" + INSTALLERS="$INSTALLERS $id" + printf -v "INSTALL_DEPS_${id}" "%s" "${TOOL_DEPS[$id]:-}" + printf -v "INSTALL_DESC_${id}" "%s" "${TOOL_DESC[$id]:-}" + printf -v "INSTALL_PLATFORMS_${id}" "%s" "${TOOL_PLATFORMS[$id]:-}" done } @@ -275,8 +271,7 @@ run_install() { if declare -f "install_${id}" >/dev/null 2>&1; then "install_${id}" else - echo "Installer not found: $id" >&2 - return 1 + install_tool "$id" fi fi mark_done "$id" diff --git a/installers/README.md b/installers/README.md index a911a5e..2405623 100644 --- a/installers/README.md +++ b/installers/README.md @@ -1,11 +1,10 @@ # installers -Each installer is a Bash script that declares: +Installers are defined in `installers/tools.sh` as a registry. -- `INSTALL_ID`: unique id -- `INSTALL_DEPS`: space-delimited list of other installers -- `INSTALL_DESC`: short description -- `INSTALL_PLATFORMS`: supported platforms -- `install_()`: function that performs installation +Each tool declares: +- ID, description, deps, platforms +- supported install methods (brew/apt/dnf/yum/pacman/pipx/git/curl/handler) +- optional package name overrides -The main installer resolves dependencies and executes installers idempotently. +Handlers live in `installers/_helpers.sh` for tools that need custom logic. diff --git a/installers/_helpers.sh b/installers/_helpers.sh index 93d9988..a660b7f 100755 --- a/installers/_helpers.sh +++ b/installers/_helpers.sh @@ -31,6 +31,18 @@ _using_pipx() { command -v pipx >/dev/null 2>&1; } # @internal _using_pip() { command -v python3 >/dev/null 2>&1 && python3 -m pip --version >/dev/null 2>&1; } +# @internal +_tools_loaded() { [[ -n "${TOOL_IDS[*]:-}" ]]; } + +# @internal +_ensure_tools_loaded() { + _tools_loaded && return 0 + local repo_dir + repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + # shellcheck disable=SC1090 + source "$repo_dir/installers/tools.sh" +} + # @internal _auto_approved() { [[ "${GET_BASHED_AUTO_APPROVE:-0}" == "1" ]]; } @@ -204,6 +216,318 @@ component_install() { echo "No installation method found for: $term" >&2 return 1 } + +# @description Install a tool from the tools registry. +# @arg $1 string Tool id. +install_tool() { + local id="$1" + _ensure_tools_loaded + + local handler="${TOOL_HANDLER[$id]:-}" + if [[ -n "$handler" ]]; then + "$handler" "$id" + return $? + fi + + local methods="${TOOL_METHODS[$id]:-}" + if [[ -z "$methods" ]]; then + echo "No install methods defined for $id" >&2 + return 1 + fi + + local method + IFS=',' read -r -a _methods <<<"$methods" + for method in "${_methods[@]}"; do + case "$method" in + brew) + _using_brew || continue + brew install "${TOOL_BREW[$id]:-$id}" && return 0 + ;; + apt) + command -v apt-get >/dev/null 2>&1 || continue + apt_install "${TOOL_APT[$id]:-$id}" && return 0 + ;; + dnf) + command -v dnf >/dev/null 2>&1 || continue + dnf_install "${TOOL_DNF[$id]:-$id}" && return 0 + ;; + yum) + command -v yum >/dev/null 2>&1 || continue + yum_install "${TOOL_YUM[$id]:-$id}" && return 0 + ;; + pacman) + command -v pacman >/dev/null 2>&1 || continue + pacman_install "${TOOL_PACMAN[$id]:-$id}" && return 0 + ;; + pip) + _using_pip || continue + python3 -m pip install --user "${id}" && return 0 + ;; + pipx) + pipx_install "${id}" && return 0 + ;; + git) + _using_git || continue + local url="${TOOL_GIT_URL[$id]:-}" + [[ -n "$url" ]] || continue + local prefix="${GET_BASHED_HOME:-$HOME/.get-bashed}" + local target="$prefix/vendor/$id" + mkdir -p "$prefix/vendor" + git clone --depth=1 "$url" "$target" + return 0 + ;; + curl) + _using_curl || continue + local url="${TOOL_CURL_URL[$id]:-}" + [[ -n "$url" ]] || continue + local tmp_dir + tmp_dir="$(mktemp -d)" + curl -fsSL "$url" -o "$tmp_dir/install.sh" + local cmd="${TOOL_CURL_CMD[$id]:-bash}" + $cmd "$tmp_dir/install.sh" + rm -rf "$tmp_dir" + return 0 + ;; + *) + ;; + esac + done + + echo "Failed to install $id via methods: $methods" >&2 + return 1 +} + +# @description Install asdf (handler). +install_asdf() { + if _using_asdf; then + return 0 + fi + + if _using_brew; then + brew install asdf + return 0 + fi + + if _using_git; then + if [[ -d "$HOME/.asdf" ]]; then + return 0 + fi + git clone https://github.com/asdf-vm/asdf.git "$HOME/.asdf" + if git -C "$HOME/.asdf" describe --tags --abbrev=0 >/dev/null 2>&1; then + local tag + tag="$(git -C "$HOME/.asdf" describe --tags --abbrev=0)" + git -C "$HOME/.asdf" checkout "$tag" || true + fi + return 0 + fi + + echo "asdf install requires Homebrew or git." >&2 + return 1 +} + +# @description Install GNU tools (handler). +install_gnu_tools() { + if _using_brew; then + brew install coreutils findutils gnu-sed gnu-tar + return 0 + fi + echo "GNU tools install requires Homebrew." >&2 + return 1 +} + +# @description Install Java (handler). +install_java() { + if command -v java >/dev/null 2>&1; then + return 0 + fi + + if _using_asdf; then + asdf_install_plugin java https://github.com/halcyon/asdf-java.git || true + local latest_version + latest_version="$(asdf latest java 2>/dev/null || true)" + if [[ -n "$latest_version" ]]; then + asdf install java "$latest_version" + asdf set --home java "$latest_version" + return 0 + fi + echo "Failed to resolve latest Java version via asdf." >&2 + return 1 + fi + + pkg_install openjdk +} + +# @description Install Node.js (handler). +install_nodejs() { + if command -v node >/dev/null 2>&1; then + return 0 + fi + + if _using_asdf; then + asdf_install_plugin nodejs || true + local latest_version + latest_version="$(asdf latest nodejs 2>/dev/null || true)" + if [[ -n "$latest_version" ]]; then + asdf install nodejs "$latest_version" + asdf set --home nodejs "$latest_version" + return 0 + fi + echo "Failed to resolve latest Node.js version via asdf." >&2 + return 1 + fi + + pkg_install node +} + +# @description Install Python (handler). +install_python() { + if command -v python3 >/dev/null 2>&1; then + return 0 + fi + + if _using_asdf; then + asdf_install_plugin python || true + local latest_version + latest_version="$(asdf latest python 2>/dev/null || true)" + if [[ -n "$latest_version" ]]; then + asdf install python "$latest_version" + asdf set --home python "$latest_version" + return 0 + fi + echo "Failed to resolve latest Python version via asdf." >&2 + return 1 + fi + + pkg_install python3 python3 python3 python3 +} + +# @description Install shdoc (handler). +install_shdoc() { + if command -v shdoc >/dev/null 2>&1; then + return 0 + fi + if pkg_install shdoc; then + return 0 + fi + + if command -v yay >/dev/null 2>&1; then + if _auto_approved; then + yay -S --noconfirm shdoc-git && return 0 + else + yay -S shdoc-git && return 0 + fi + elif command -v paru >/dev/null 2>&1; then + if _auto_approved; then + paru -S --noconfirm shdoc-git && return 0 + else + paru -S shdoc-git && return 0 + fi + fi + + echo "shdoc is not available via the detected package manager." >&2 + echo "Attempting local install to GET_BASHED_HOME/bin without sudo." >&2 + + _using_git || { echo "git is required to build shdoc." >&2; return 1; } + pkg_install gawk gawk gawk gawk || true + pkg_install make make make make || true + + local prefix="${GET_BASHED_HOME:-$HOME/.get-bashed}" + local bindir="$prefix/bin" + mkdir -p "$bindir" + + # shdoc requires bash 4+ for ;;& case labels + local bash_major + bash_major="$(bash -c 'echo ${BASH_VERSINFO[0]:-0}' 2>/dev/null || echo 0)" + if [[ "$bash_major" -lt 4 ]] && _using_brew; then + brew install bash || true + fi + + local tmp_dir + tmp_dir="$(mktemp -d)" + git clone --recursive https://github.com/reconquest/shdoc "$tmp_dir/shdoc" + if ! make -C "$tmp_dir/shdoc" install PREFIX="$prefix" BINDIR="$bindir"; then + echo "Failed to install shdoc locally. See https://github.com/reconquest/shdoc" >&2 + rm -rf "$tmp_dir" + return 1 + fi + rm -rf "$tmp_dir" +} + +# @description Install vimrc (handler). +install_vimrc() { + local prefix="${GET_BASHED_HOME:-$HOME/.get-bashed}" + local target="$prefix/vendor/vimrc" + if [[ -d "$target/.git" ]]; then + return 0 + fi + mkdir -p "$prefix/vendor" + git clone --depth=1 https://github.com/amix/vimrc.git "$target" + case "${GET_BASHED_VIMRC_MODE:-awesome}" in + basic) + sh "$target/install_basic_vimrc.sh" + ;; + *) + sh "$target/install_awesome_vimrc.sh" + ;; + esac +} + +# @description Install actionlint (handler). +install_actionlint() { + if command -v actionlint >/dev/null 2>&1; then + return 0 + fi + + if _using_brew; then + brew install actionlint && return 0 + fi + if command -v apt-get >/dev/null 2>&1; then + if apt_install actionlint; then + return 0 + fi + fi + + if ! _using_curl; then + echo "curl is required to install actionlint" >&2 + return 1 + fi + + # Fallback: download latest release binary + local tag version os arch url tmp_dir + tag="$(python3 - <<'PY' +import json +import urllib.request +u = 'https://api.github.com/repos/rhysd/actionlint/releases/latest' +print(json.load(urllib.request.urlopen(u))['tag_name']) +PY +)" + version="${tag#v}" + os="$(uname -s | tr '[:upper:]' '[:lower:]')" + arch="$(uname -m)" + case "$arch" in + x86_64) arch="amd64" ;; + arm64|aarch64) arch="arm64" ;; + esac + + if [[ "$os" == "darwin" ]]; then + os="darwin" + elif [[ "$os" == "linux" ]]; then + os="linux" + else + echo "Unsupported OS for actionlint: $os" >&2 + return 1 + fi + + url="https://github.com/rhysd/actionlint/releases/download/${tag}/actionlint_${version}_${os}_${arch}.tar.gz" + tmp_dir="$(mktemp -d)" + curl -fsSL "$url" -o "$tmp_dir/actionlint.tgz" + tar -xzf "$tmp_dir/actionlint.tgz" -C "$tmp_dir" + local prefix="${GET_BASHED_HOME:-$HOME/.get-bashed}" + mkdir -p "$prefix/bin" + mv "$tmp_dir/actionlint" "$prefix/bin/actionlint" + chmod +x "$prefix/bin/actionlint" + rm -rf "$tmp_dir" +} # @description Install a package via available system package manager. # @arg $1 string Brew package name. # @arg $2 string Apt package name (optional). diff --git a/installers/actionlint.sh b/installers/actionlint.sh deleted file mode 100755 index 9407b2f..0000000 --- a/installers/actionlint.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env bash -# @file actionlint -# @brief Installer: actionlint -# @description -# Installer script for get-bashed. - -INSTALL_ID="actionlint" -INSTALL_DEPS="" -INSTALL_DESC="actionlint" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_actionlint() { - if command -v actionlint >/dev/null 2>&1; then - return 0 - fi - - if _using_brew; then - brew install actionlint && return 0 - fi - if command -v apt-get >/dev/null 2>&1; then - if apt_install actionlint; then - return 0 - fi - fi - - if ! _using_curl; then - echo "curl is required to install actionlint" >&2 - return 1 - fi - - # Fallback: download latest release binary - local tag version os arch url tmp_dir - tag="$(python3 - <<'PY' -import json -import urllib.request -u = 'https://api.github.com/repos/rhysd/actionlint/releases/latest' -print(json.load(urllib.request.urlopen(u))['tag_name']) -PY -)" - version="${tag#v}" - os="$(uname -s | tr '[:upper:]' '[:lower:]')" - arch="$(uname -m)" - case "$arch" in - x86_64) arch="amd64" ;; - arm64|aarch64) arch="arm64" ;; - esac - - if [[ "$os" == "darwin" ]]; then - os="darwin" - elif [[ "$os" == "linux" ]]; then - os="linux" - else - echo "Unsupported OS for actionlint: $os" >&2 - return 1 - fi - - url="https://github.com/rhysd/actionlint/releases/download/${tag}/actionlint_${version}_${os}_${arch}.tar.gz" - tmp_dir="$(mktemp -d)" - curl -fsSL "$url" -o "$tmp_dir/actionlint.tgz" - tar -xzf "$tmp_dir/actionlint.tgz" -C "$tmp_dir" - local prefix="${GET_BASHED_HOME:-$HOME/.get-bashed}" - mkdir -p "$prefix/bin" - mv "$tmp_dir/actionlint" "$prefix/bin/actionlint" - chmod +x "$prefix/bin/actionlint" - rm -rf "$tmp_dir" -} diff --git a/installers/asdf.sh b/installers/asdf.sh deleted file mode 100755 index 127c918..0000000 --- a/installers/asdf.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env bash -# @file asdf -# @brief Installer: asdf -# @description -# Installer script for get-bashed. - -INSTALL_ID="asdf" -INSTALL_DEPS="brew" -INSTALL_DESC="asdf version manager" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_asdf() { - if _using_asdf; then - return 0 - fi - - if _using_brew; then - brew install asdf - return 0 - fi - - if _using_git; then - if [[ -d "$HOME/.asdf" ]]; then - return 0 - fi - git clone https://github.com/asdf-vm/asdf.git "$HOME/.asdf" - if git -C "$HOME/.asdf" describe --tags --abbrev=0 >/dev/null 2>&1; then - local tag - tag="$(git -C "$HOME/.asdf" describe --tags --abbrev=0)" - git -C "$HOME/.asdf" checkout "$tag" || true - fi - return 0 - fi - - echo "asdf install requires Homebrew or git." >&2 - return 1 -} diff --git a/installers/awscli.sh b/installers/awscli.sh deleted file mode 100755 index 86b0382..0000000 --- a/installers/awscli.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# @file awscli -# @brief Installer: awscli -# @description -# Installer script for get-bashed. - -INSTALL_ID="awscli" -INSTALL_DEPS="" -INSTALL_DESC="AWS CLI" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_awscli() { - if command -v aws >/dev/null 2>&1; then - return 0 - fi - pkg_install awscli awscli -} diff --git a/installers/bash-it.sh b/installers/bash-it.sh deleted file mode 100644 index 54e6bac..0000000 --- a/installers/bash-it.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash -# @file bash-it -# @brief Installer: bash-it -# @description -# Installer script for get-bashed. - -INSTALL_ID="bash_it" -INSTALL_DEPS="git" -INSTALL_DESC="bash-it framework" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_bash_it() { - local prefix="${GET_BASHED_HOME:-$HOME/.get-bashed}" - local target="$prefix/vendor/bash-it" - if [[ -d "$target/.git" ]]; then - return 0 - fi - mkdir -p "$prefix/vendor" - git clone --depth=1 https://github.com/Bash-it/bash-it.git "$target" -} diff --git a/installers/bash.sh b/installers/bash.sh deleted file mode 100644 index 885de5c..0000000 --- a/installers/bash.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash -# @file bash -# @brief Installer: bash -# @description -# Installer script for get-bashed. - -INSTALL_ID="bash" -INSTALL_DEPS="" -INSTALL_DESC="Latest GNU Bash" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_bash() { - if command -v bash >/dev/null 2>&1; then - local major - major="$(bash -c 'echo ${BASH_VERSINFO[0]:-0}' 2>/dev/null || echo 0)" - if [[ "$major" -ge 4 ]]; then - return 0 - fi - fi - pkg_install bash bash bash bash -} diff --git a/installers/bashate.sh b/installers/bashate.sh deleted file mode 100755 index 8d204ce..0000000 --- a/installers/bashate.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# @file bashate -# @brief Installer: bashate -# @description -# Installer script for get-bashed. - -INSTALL_ID="bashate" -INSTALL_DEPS="pipx" -INSTALL_DESC="bashate" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_bashate() { - if command -v bashate >/dev/null 2>&1; then - return 0 - fi - pipx_install bashate -} diff --git a/installers/bat.sh b/installers/bat.sh deleted file mode 100755 index 26bebee..0000000 --- a/installers/bat.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# @file bat -# @brief Installer: bat -# @description -# Installer script for get-bashed. - -INSTALL_ID="bat" -INSTALL_DEPS="" -INSTALL_DESC="bat" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_bat() { - if command -v bat >/dev/null 2>&1; then - return 0 - fi - pkg_install bat -} diff --git a/installers/bats.sh b/installers/bats.sh deleted file mode 100755 index 76433c1..0000000 --- a/installers/bats.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# @file bats -# @brief Installer: bats -# @description -# Installer script for get-bashed. - -INSTALL_ID="bats" -INSTALL_DEPS="" -INSTALL_DESC="Bats (bash testing)" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_bats() { - if command -v bats >/dev/null 2>&1; then - return 0 - fi - pkg_install bats-core bats -} diff --git a/installers/brew.sh b/installers/brew.sh deleted file mode 100755 index ba09572..0000000 --- a/installers/brew.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash -# @file brew -# @brief Installer: brew -# @description -# Installer script for get-bashed. - -INSTALL_ID="brew" -INSTALL_DEPS="" -INSTALL_DESC="Homebrew/Linuxbrew installer" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_brew() { - if _using_brew; then - return 0 - fi - - if ! _using_curl; then - echo "curl is required to install Homebrew." >&2 - return 1 - fi - - local tmp_dir - tmp_dir="$(mktemp -d)" - curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh -o "$tmp_dir/install.sh" - echo "Running Homebrew installer from $tmp_dir/install.sh" - /bin/bash "$tmp_dir/install.sh" - rm -rf "$tmp_dir" -} diff --git a/installers/curl.sh b/installers/curl.sh deleted file mode 100755 index 5b658db..0000000 --- a/installers/curl.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# @file curl -# @brief Installer: curl -# @description -# Installer script for get-bashed. - -INSTALL_ID="curl" -INSTALL_DEPS="" -INSTALL_DESC="curl" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_curl() { - if command -v curl >/dev/null 2>&1; then - return 0 - fi - pkg_install curl -} diff --git a/installers/dialog.sh b/installers/dialog.sh deleted file mode 100755 index 3fb5d76..0000000 --- a/installers/dialog.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# @file dialog -# @brief Installer: dialog -# @description -# Installer script for get-bashed. - -INSTALL_ID="dialog" -INSTALL_DEPS="" -INSTALL_DESC="curses dialog UI" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_dialog() { - if command -v dialog >/dev/null 2>&1; then - return 0 - fi - - if _using_brew; then - brew install dialog - elif command -v apt-get >/dev/null 2>&1; then - apt_install dialog - elif command -v dnf >/dev/null 2>&1; then - dnf_install dialog - elif command -v yum >/dev/null 2>&1; then - yum_install dialog - fi -} diff --git a/installers/direnv.sh b/installers/direnv.sh deleted file mode 100755 index ea56b35..0000000 --- a/installers/direnv.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash -# @file direnv -# @brief Installer: direnv -# @description -# Installer script for get-bashed. - -INSTALL_ID="direnv" -INSTALL_DEPS="brew" -INSTALL_DESC="direnv" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_direnv() { - if command -v direnv >/dev/null 2>&1; then - return 0 - fi - - if _using_brew; then - brew install direnv - else - echo "direnv install requires Homebrew or manual install." >&2 - return 1 - fi -} diff --git a/installers/doppler.sh b/installers/doppler.sh deleted file mode 100755 index ba9e52c..0000000 --- a/installers/doppler.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash -# @file doppler -# @brief Installer: doppler -# @description -# Installer script for get-bashed. - -INSTALL_ID="doppler" -INSTALL_DEPS="brew" -INSTALL_DESC="Doppler CLI" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_doppler() { - if command -v doppler >/dev/null 2>&1; then - return 0 - fi - - if _using_brew; then - brew install doppler - else - echo "Doppler install requires Homebrew on macOS or manual install." >&2 - return 1 - fi -} diff --git a/installers/fd.sh b/installers/fd.sh deleted file mode 100755 index e8ddc0c..0000000 --- a/installers/fd.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# @file fd -# @brief Installer: fd -# @description -# Installer script for get-bashed. - -INSTALL_ID="fd" -INSTALL_DEPS="" -INSTALL_DESC="fd" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_fd() { - if command -v fd >/dev/null 2>&1; then - return 0 - fi - pkg_install fd -} diff --git a/installers/fzf.sh b/installers/fzf.sh deleted file mode 100755 index 6279efa..0000000 --- a/installers/fzf.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# @file fzf -# @brief Installer: fzf -# @description -# Installer script for get-bashed. - -INSTALL_ID="fzf" -INSTALL_DEPS="" -INSTALL_DESC="fzf" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_fzf() { - if command -v fzf >/dev/null 2>&1; then - return 0 - fi - pkg_install fzf -} diff --git a/installers/gh.sh b/installers/gh.sh deleted file mode 100755 index ef2cc15..0000000 --- a/installers/gh.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# @file gh -# @brief Installer: gh -# @description -# Installer script for get-bashed. - -INSTALL_ID="gh" -INSTALL_DEPS="" -INSTALL_DESC="GitHub CLI" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_gh() { - if command -v gh >/dev/null 2>&1; then - return 0 - fi - pkg_install gh -} diff --git a/installers/git-lfs.sh b/installers/git-lfs.sh deleted file mode 100755 index 7b3cc5e..0000000 --- a/installers/git-lfs.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# @file git-lfs -# @brief Installer: git-lfs -# @description -# Installer script for get-bashed. - -INSTALL_ID="git_lfs" -INSTALL_DEPS="" -INSTALL_DESC="Git LFS" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_git_lfs() { - if command -v git-lfs >/dev/null 2>&1; then - return 0 - fi - pkg_install git-lfs git-lfs -} diff --git a/installers/git.sh b/installers/git.sh deleted file mode 100755 index 0e0d4b8..0000000 --- a/installers/git.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# @file git -# @brief Installer: git -# @description -# Installer script for get-bashed. - -INSTALL_ID="git" -INSTALL_DEPS="" -INSTALL_DESC="git" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_git() { - if _using_git; then - return 0 - fi - pkg_install git -} diff --git a/installers/gnu-tools.sh b/installers/gnu-tools.sh deleted file mode 100755 index a2b388c..0000000 --- a/installers/gnu-tools.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash -# @file gnu-tools -# @brief Installer: gnu-tools -# @description -# Installer script for get-bashed. - -INSTALL_ID="gnu_tools" -INSTALL_DEPS="brew" -INSTALL_DESC="GNU coreutils/findutils/sed/tar" -INSTALL_PLATFORMS="macos" - -# @description Run installer. -# @noargs -install_gnu_tools() { - if _using_brew; then - brew install coreutils findutils gnu-sed gnu-tar - else - echo "GNU tools install requires Homebrew." >&2 - return 1 - fi -} diff --git a/installers/gnupg.sh b/installers/gnupg.sh deleted file mode 100755 index 71cb8fe..0000000 --- a/installers/gnupg.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# @file gnupg -# @brief Installer: gnupg -# @description -# Installer script for get-bashed. - -INSTALL_ID="gnupg" -INSTALL_DEPS="" -INSTALL_DESC="gnupg" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_gnupg() { - if command -v gpg >/dev/null 2>&1; then - return 0 - fi - pkg_install gnupg gnupg gnupg gnupg -} diff --git a/installers/helm.sh b/installers/helm.sh deleted file mode 100755 index eb3f8b8..0000000 --- a/installers/helm.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# @file helm -# @brief Installer: helm -# @description -# Installer script for get-bashed. - -INSTALL_ID="helm" -INSTALL_DEPS="" -INSTALL_DESC="Helm" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_helm() { - if command -v helm >/dev/null 2>&1; then - return 0 - fi - pkg_install helm -} diff --git a/installers/java.sh b/installers/java.sh deleted file mode 100755 index cc803d6..0000000 --- a/installers/java.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash -# @file java -# @brief Installer: java -# @description -# Installer script for get-bashed. - -INSTALL_ID="java" -INSTALL_DEPS="asdf" -INSTALL_DESC="Java (asdf preferred)" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_java() { - if command -v java >/dev/null 2>&1; then - return 0 - fi - - if _using_asdf; then - asdf_install_plugin java https://github.com/halcyon/asdf-java.git || true - latest_version="$(asdf latest java 2>/dev/null || true)" - if [[ -n "$latest_version" ]]; then - asdf install java "$latest_version" - asdf set --home java "$latest_version" - return 0 - fi - echo "Failed to resolve latest Java version via asdf." >&2 - return 1 - fi - - pkg_install openjdk -} diff --git a/installers/jq.sh b/installers/jq.sh deleted file mode 100755 index 71e1ef8..0000000 --- a/installers/jq.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# @file jq -# @brief Installer: jq -# @description -# Installer script for get-bashed. - -INSTALL_ID="jq" -INSTALL_DEPS="" -INSTALL_DESC="jq" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_jq() { - if command -v jq >/dev/null 2>&1; then - return 0 - fi - pkg_install jq -} diff --git a/installers/kubectl.sh b/installers/kubectl.sh deleted file mode 100755 index 615d4bb..0000000 --- a/installers/kubectl.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# @file kubectl -# @brief Installer: kubectl -# @description -# Installer script for get-bashed. - -INSTALL_ID="kubectl" -INSTALL_DEPS="" -INSTALL_DESC="kubectl" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_kubectl() { - if command -v kubectl >/dev/null 2>&1; then - return 0 - fi - pkg_install kubectl kubectl -} diff --git a/installers/nodejs.sh b/installers/nodejs.sh deleted file mode 100755 index 9f12d90..0000000 --- a/installers/nodejs.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash -# @file nodejs -# @brief Installer: nodejs -# @description -# Installer script for get-bashed. - -INSTALL_ID="nodejs" -INSTALL_DEPS="asdf" -INSTALL_DESC="Node.js (asdf preferred)" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_nodejs() { - if command -v node >/dev/null 2>&1; then - return 0 - fi - - if _using_asdf; then - asdf_install_plugin nodejs || true - latest_version="$(asdf latest nodejs 2>/dev/null || true)" - if [[ -n "$latest_version" ]]; then - asdf install nodejs "$latest_version" - asdf set --home nodejs "$latest_version" - return 0 - fi - echo "Failed to resolve latest Node.js version via asdf." >&2 - return 1 - fi - - pkg_install node -} diff --git a/installers/pipx.sh b/installers/pipx.sh deleted file mode 100755 index 554014b..0000000 --- a/installers/pipx.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env bash -# @file pipx -# @brief Installer: pipx -# @description -# Installer script for get-bashed. - -INSTALL_ID="pipx" -INSTALL_DEPS="" -INSTALL_DESC="pipx" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_pipx() { - if _using_pipx; then - return 0 - fi - - if _using_brew; then - brew install pipx - return 0 - fi - - if command -v apt-get >/dev/null 2>&1; then - apt_install pipx - return 0 - fi - if command -v dnf >/dev/null 2>&1; then - dnf_install pipx - return 0 - fi - if command -v yum >/dev/null 2>&1; then - yum_install pipx - return 0 - fi - if command -v pacman >/dev/null 2>&1; then - pacman_install python-pipx - return 0 - fi - - if _using_pip; then - python3 -m pip install --user pipx - python3 -m pipx ensurepath || true - return 0 - fi - - echo "pipx install failed: no supported method" >&2 - return 1 -} diff --git a/installers/pre-commit.sh b/installers/pre-commit.sh deleted file mode 100755 index a18000b..0000000 --- a/installers/pre-commit.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# @file pre-commit -# @brief Installer: pre-commit -# @description -# Installer script for get-bashed. - -INSTALL_ID="pre_commit" -INSTALL_DEPS="pipx" -INSTALL_DESC="pre-commit" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_pre_commit() { - if command -v pre-commit >/dev/null 2>&1; then - return 0 - fi - pipx_install pre-commit -} diff --git a/installers/python.sh b/installers/python.sh deleted file mode 100755 index e9d05ff..0000000 --- a/installers/python.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash -# @file python -# @brief Installer: python -# @description -# Installer script for get-bashed. - -INSTALL_ID="python" -INSTALL_DEPS="asdf" -INSTALL_DESC="Python (asdf preferred)" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_python() { - if command -v python3 >/dev/null 2>&1; then - return 0 - fi - - if _using_asdf; then - asdf_install_plugin python || true - latest_version="$(asdf latest python 2>/dev/null || true)" - if [[ -n "$latest_version" ]]; then - asdf install python "$latest_version" - asdf set --home python "$latest_version" - return 0 - fi - echo "Failed to resolve latest Python version via asdf." >&2 - return 1 - fi - - pkg_install python -} diff --git a/installers/rg.sh b/installers/rg.sh deleted file mode 100755 index 5c93f9b..0000000 --- a/installers/rg.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# @file rg -# @brief Installer: rg -# @description -# Installer script for get-bashed. - -INSTALL_ID="rg" -INSTALL_DEPS="" -INSTALL_DESC="ripgrep" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_rg() { - if command -v rg >/dev/null 2>&1; then - return 0 - fi - pkg_install ripgrep rg -} diff --git a/installers/shdoc.sh b/installers/shdoc.sh deleted file mode 100755 index f61aa3e..0000000 --- a/installers/shdoc.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash -# @file shdoc -# @brief Installer: shdoc -# @description -# Installer script for get-bashed. - -INSTALL_ID="shdoc" -INSTALL_DEPS="" -INSTALL_DESC="shdoc (shell script doc generator)" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_shdoc() { - if command -v shdoc >/dev/null 2>&1; then - return 0 - fi - if pkg_install shdoc; then - return 0 - fi - - if command -v yay >/dev/null 2>&1; then - if _auto_approved; then - yay -S --noconfirm shdoc-git && return 0 - else - yay -S shdoc-git && return 0 - fi - elif command -v paru >/dev/null 2>&1; then - if _auto_approved; then - paru -S --noconfirm shdoc-git && return 0 - else - paru -S shdoc-git && return 0 - fi - fi - - echo "shdoc is not available via the detected package manager." >&2 - echo "Attempting local install to GET_BASHED_HOME/bin without sudo." >&2 - - _using_git || { echo "git is required to build shdoc." >&2; return 1; } - pkg_install gawk gawk gawk gawk || true - pkg_install make make make make || true - - local prefix="${GET_BASHED_HOME:-$HOME/.get-bashed}" - local bindir="$prefix/bin" - mkdir -p "$bindir" - - # shdoc requires bash 4+ for ;;& case labels - local bash_major - bash_major="$(bash -c 'echo ${BASH_VERSINFO[0]:-0}' 2>/dev/null || echo 0)" - if [[ "$bash_major" -lt 4 ]] && _using_brew; then - brew install bash || true - fi - - tmp_dir="$(mktemp -d)" - git clone --recursive https://github.com/reconquest/shdoc "$tmp_dir/shdoc" - if ! make -C "$tmp_dir/shdoc" install PREFIX="$prefix" BINDIR="$bindir"; then - echo "Failed to install shdoc locally. See https://github.com/reconquest/shdoc" >&2 - rm -rf "$tmp_dir" - return 1 - fi - rm -rf "$tmp_dir" -} diff --git a/installers/shellcheck.sh b/installers/shellcheck.sh deleted file mode 100755 index c9cd181..0000000 --- a/installers/shellcheck.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# @file shellcheck -# @brief Installer: shellcheck -# @description -# Installer script for get-bashed. - -INSTALL_ID="shellcheck" -INSTALL_DEPS="" -INSTALL_DESC="ShellCheck" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_shellcheck() { - if command -v shellcheck >/dev/null 2>&1; then - return 0 - fi - pkg_install shellcheck -} diff --git a/installers/starship.sh b/installers/starship.sh deleted file mode 100755 index 529f954..0000000 --- a/installers/starship.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash -# @file starship -# @brief Installer: starship -# @description -# Installer script for get-bashed. - -INSTALL_ID="starship" -INSTALL_DEPS="brew" -INSTALL_DESC="Starship prompt" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_starship() { - if command -v starship >/dev/null 2>&1; then - return 0 - fi - - if _using_brew; then - brew install starship - else - echo "Starship install requires Homebrew or manual install." >&2 - return 1 - fi -} diff --git a/installers/stern.sh b/installers/stern.sh deleted file mode 100755 index cfe008a..0000000 --- a/installers/stern.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# @file stern -# @brief Installer: stern -# @description -# Installer script for get-bashed. - -INSTALL_ID="stern" -INSTALL_DEPS="" -INSTALL_DESC="stern" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_stern() { - if command -v stern >/dev/null 2>&1; then - return 0 - fi - pkg_install stern -} diff --git a/installers/terraform.sh b/installers/terraform.sh deleted file mode 100755 index d0af2e1..0000000 --- a/installers/terraform.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# @file terraform -# @brief Installer: terraform -# @description -# Installer script for get-bashed. - -INSTALL_ID="terraform" -INSTALL_DEPS="" -INSTALL_DESC="Terraform" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_terraform() { - if command -v terraform >/dev/null 2>&1; then - return 0 - fi - pkg_install terraform -} diff --git a/installers/tools.sh b/installers/tools.sh new file mode 100644 index 0000000..ade46d9 --- /dev/null +++ b/installers/tools.sh @@ -0,0 +1,167 @@ +#!/usr/bin/env bash +# @file tools +# @brief Tool registry for get-bashed installers. +# @description +# Defines tool metadata, dependencies, and installation methods. + +TOOL_IDS=() +declare -A TOOL_DESC +declare -A TOOL_DEPS +declare -A TOOL_PLATFORMS +declare -A TOOL_METHODS +declare -A TOOL_BREW +declare -A TOOL_APT +declare -A TOOL_DNF +declare -A TOOL_YUM +declare -A TOOL_PACMAN +declare -A TOOL_GIT_URL +declare -A TOOL_CURL_URL +declare -A TOOL_CURL_CMD +declare -A TOOL_HANDLER + +tool_register() { + local id="$1" desc="$2" deps="$3" platforms="$4" methods="$5" + TOOL_IDS+=("$id") + TOOL_DESC["$id"]="$desc" + TOOL_DEPS["$id"]="$deps" + TOOL_PLATFORMS["$id"]="$platforms" + TOOL_METHODS["$id"]="$methods" +} + +tool_pkgs() { + local id="$1" brew="$2" apt="$3" dnf="$4" yum="$5" pacman="$6" + TOOL_BREW["$id"]="$brew" + TOOL_APT["$id"]="$apt" + TOOL_DNF["$id"]="$dnf" + TOOL_YUM["$id"]="$yum" + TOOL_PACMAN["$id"]="$pacman" +} + +tool_git() { + local id="$1" url="$2" + TOOL_GIT_URL["$id"]="$url" +} + +tool_curl() { + local id="$1" url="$2" cmd="$3" + TOOL_CURL_URL["$id"]="$url" + TOOL_CURL_CMD["$id"]="$cmd" +} + +tool_handler() { + local id="$1" handler="$2" + TOOL_HANDLER["$id"]="$handler" +} + +# Core tools +tool_register brew "Homebrew/Linuxbrew installer" "" "macos,linux,wsl" "curl" +tool_curl brew "https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh" "/bin/bash" + +tool_register asdf "asdf version manager" "" "macos,linux,wsl" "handler" +tool_handler asdf "install_asdf" + +tool_register bash "Latest GNU Bash" "" "macos,linux,wsl" "brew,apt,dnf,yum,pacman" +tool_pkgs bash "bash" "bash" "bash" "bash" "bash" + +tool_register bash_it "bash-it framework" "git" "macos,linux,wsl" "git" +tool_git bash_it "https://github.com/Bash-it/bash-it.git" + +tool_register vimrc "amix/vimrc (awesome/basic)" "git" "macos,linux,wsl" "git" +tool_git vimrc "https://github.com/amix/vimrc.git" +tool_handler vimrc "install_vimrc" + +tool_register shdoc "shdoc (shell script doc generator)" "" "macos,linux,wsl" "handler" +tool_handler shdoc "install_shdoc" + +tool_register dialog "curses dialog UI" "" "macos,linux,wsl" "brew,apt,dnf,yum" +tool_pkgs dialog "dialog" "dialog" "dialog" "dialog" "" + +tool_register pipx "pipx" "" "macos,linux,wsl" "brew,apt,dnf,yum,pacman,pip" +tool_pkgs pipx "pipx" "pipx" "pipx" "pipx" "python-pipx" + +tool_register pre_commit "pre-commit" "pipx" "macos,linux,wsl" "pipx" +tool_register bashate "bashate" "pipx" "macos,linux,wsl" "pipx" + +tool_register shellcheck "shellcheck" "" "macos,linux,wsl" "brew,apt,dnf,yum,pacman" +tool_pkgs shellcheck "shellcheck" "shellcheck" "ShellCheck" "ShellCheck" "shellcheck" + +tool_register actionlint "actionlint" "" "macos,linux,wsl" "handler" +tool_handler actionlint "install_actionlint" + +tool_register bats "bats" "" "macos,linux,wsl" "brew,apt,dnf,yum,pacman" +tool_pkgs bats "bats-core" "bats" "bats" "bats" "bats" + +tool_register curl "curl" "" "macos,linux,wsl" "brew,apt,dnf,yum,pacman" +tool_pkgs curl "curl" "curl" "curl" "curl" "curl" + +tool_register wget "wget" "" "macos,linux,wsl" "brew,apt,dnf,yum,pacman" +tool_pkgs wget "wget" "wget" "wget" "wget" "wget" + +tool_register gnupg "gnupg" "" "macos,linux,wsl" "brew,apt,dnf,yum,pacman" +tool_pkgs gnupg "gnupg" "gnupg" "gnupg2" "gnupg2" "gnupg" + +tool_register gnu_tools "GNU coreutils/findutils/sed/tar" "brew" "macos" "handler" +tool_handler gnu_tools "install_gnu_tools" + +tool_register git "git" "" "macos,linux,wsl" "brew,apt,dnf,yum,pacman" +tool_pkgs git "git" "git" "git" "git" "git" + +tool_register git_lfs "git-lfs" "" "macos,linux,wsl" "brew,apt,dnf,yum" +tool_pkgs git_lfs "git-lfs" "git-lfs" "git-lfs" "git-lfs" "" + +tool_register gh "GitHub CLI" "" "macos,linux,wsl" "brew,apt" +tool_pkgs gh "gh" "gh" "" "" "" + +tool_register direnv "direnv" "" "macos,linux,wsl" "brew,apt" +tool_pkgs direnv "direnv" "direnv" "" "" "" + +tool_register starship "starship" "" "macos,linux,wsl" "brew" +tool_pkgs starship "starship" "" "" "" "" + +tool_register rg "ripgrep" "" "macos,linux,wsl" "brew,apt,dnf,yum,pacman" +tool_pkgs rg "ripgrep" "ripgrep" "ripgrep" "ripgrep" "ripgrep" + +tool_register fd "fd" "" "macos,linux,wsl" "brew,apt,dnf,yum,pacman" +tool_pkgs fd "fd" "fd-find" "fd-find" "fd-find" "fd" + +tool_register bat "bat" "" "macos,linux,wsl" "brew,apt,dnf,yum,pacman" +tool_pkgs bat "bat" "bat" "bat" "bat" "bat" + +tool_register fzf "fzf" "" "macos,linux,wsl" "brew,apt,dnf,yum,pacman" +tool_pkgs fzf "fzf" "fzf" "fzf" "fzf" "fzf" + +tool_register jq "jq" "" "macos,linux,wsl" "brew,apt,dnf,yum,pacman" +tool_pkgs jq "jq" "jq" "jq" "jq" "jq" + +tool_register yq "yq" "" "macos,linux,wsl" "brew,apt,dnf,yum,pacman" +tool_pkgs yq "yq" "yq" "yq" "yq" "yq" + +tool_register tree "tree" "" "macos,linux,wsl" "brew,apt,dnf,yum,pacman" +tool_pkgs tree "tree" "tree" "tree" "tree" "tree" + +tool_register nodejs "Node.js (asdf preferred)" "asdf" "macos,linux,wsl" "handler" +tool_handler nodejs "install_nodejs" + +tool_register python "Python (asdf preferred)" "asdf" "macos,linux,wsl" "handler" +tool_handler python "install_python" + +tool_register java "Java (asdf preferred)" "asdf" "macos,linux,wsl" "handler" +tool_handler java "install_java" + +tool_register terraform "terraform" "" "macos,linux,wsl" "brew" +tool_pkgs terraform "terraform" "" "" "" "" + +tool_register awscli "AWS CLI" "" "macos,linux,wsl" "brew,apt" +tool_pkgs awscli "awscli" "awscli" "" "" "" + +tool_register kubectl "kubectl" "" "macos,linux,wsl" "brew" +tool_pkgs kubectl "kubectl" "" "" "" "" + +tool_register helm "helm" "" "macos,linux,wsl" "brew" +tool_pkgs helm "helm" "" "" "" "" + +tool_register stern "stern" "" "macos,linux,wsl" "brew" +tool_pkgs stern "stern" "" "" "" "" + +tool_register doppler "Doppler CLI" "brew" "macos,linux,wsl" "brew" +tool_pkgs doppler "doppler" "" "" "" "" diff --git a/installers/tree.sh b/installers/tree.sh deleted file mode 100755 index da1fd44..0000000 --- a/installers/tree.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# @file tree -# @brief Installer: tree -# @description -# Installer script for get-bashed. - -INSTALL_ID="tree" -INSTALL_DEPS="" -INSTALL_DESC="tree" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_tree() { - if command -v tree >/dev/null 2>&1; then - return 0 - fi - pkg_install tree -} diff --git a/installers/vimrc.sh b/installers/vimrc.sh deleted file mode 100644 index 5c166d8..0000000 --- a/installers/vimrc.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash -# @file vimrc -# @brief Installer: vimrc (amix) -# @description -# Installer script for get-bashed. - -INSTALL_ID="vimrc" -INSTALL_DEPS="git" -INSTALL_DESC="amix/vimrc (awesome vimrc)" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_vimrc() { - local prefix="${GET_BASHED_HOME:-$HOME/.get-bashed}" - local target="$prefix/vendor/vimrc" - if [[ -d "$target/.git" ]]; then - return 0 - fi - mkdir -p "$prefix/vendor" - git clone --depth=1 https://github.com/amix/vimrc.git "$target" - case "${GET_BASHED_VIMRC_MODE:-awesome}" in - basic) - sh "$target/install_basic_vimrc.sh" - ;; - *) - sh "$target/install_awesome_vimrc.sh" - ;; - esac -} diff --git a/installers/wget.sh b/installers/wget.sh deleted file mode 100755 index c9c5c01..0000000 --- a/installers/wget.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# @file wget -# @brief Installer: wget -# @description -# Installer script for get-bashed. - -INSTALL_ID="wget" -INSTALL_DEPS="" -INSTALL_DESC="wget" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_wget() { - if command -v wget >/dev/null 2>&1; then - return 0 - fi - pkg_install wget -} diff --git a/installers/yq.sh b/installers/yq.sh deleted file mode 100755 index 8dba5d4..0000000 --- a/installers/yq.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# @file yq -# @brief Installer: yq -# @description -# Installer script for get-bashed. - -INSTALL_ID="yq" -INSTALL_DEPS="" -INSTALL_DESC="yq" -INSTALL_PLATFORMS="macos,linux,wsl" - -# @description Run installer. -# @noargs -install_yq() { - if command -v yq >/dev/null 2>&1; then - return 0 - fi - pkg_install yq -} diff --git a/scripts/gen-docs.sh b/scripts/gen-docs.sh index fdb5465..11d5f97 100755 --- a/scripts/gen-docs.sh +++ b/scripts/gen-docs.sh @@ -15,19 +15,7 @@ command -v shdoc >/dev/null 2>&1 || { shdoc < "$ROOT_DIR/install.sh" > "$ROOT_DIR/docs/INSTALLER.md" shdoc < "$ROOT_DIR/installers/_helpers.sh" > "$ROOT_DIR/docs/INSTALLERS_HELPERS.md" - -# Combine all installers into one doc -TMP_FILE="$(mktemp)" -for f in "$ROOT_DIR/installers"/*.sh; do - [[ "$f" == "$ROOT_DIR/installers/_helpers.sh" ]] && continue - echo "" >> "$TMP_FILE" - cat "$f" >> "$TMP_FILE" - echo "" >> "$TMP_FILE" - echo "# ----" >> "$TMP_FILE" - echo "" >> "$TMP_FILE" - done -shdoc < "$TMP_FILE" > "$ROOT_DIR/docs/INSTALLERS.md" -rm -f "$TMP_FILE" +shdoc < "$ROOT_DIR/installers/tools.sh" > "$ROOT_DIR/docs/INSTALLERS.md" # Combine all runtime modules TMP_MODULES="$(mktemp)" From 8fc738d6d382ac39a9384f6ab026d19880c764a5 Mon Sep 17 00:00:00 2001 From: Jon B Date: Tue, 3 Feb 2026 14:22:33 -0600 Subject: [PATCH 11/33] fix: satisfy shellcheck/bashate --- bashrc.d/20-path.sh | 1 + bashrc.d/40-completions.sh | 1 + bashrc.d/66-doppler.sh | 1 + bashrc.d/70-bash-it.sh | 1 + bashrc.d/70-env.sh | 1 + bashrc.d/90-functions.sh | 1 + bashrc.d/95-ssh-agent.sh | 1 + install.sh | 1 + installers/_helpers.sh | 1 + installers/tools.sh | 1 + scripts/gen-docs.sh | 14 ++++++++------ 11 files changed, 18 insertions(+), 6 deletions(-) diff --git a/bashrc.d/20-path.sh b/bashrc.d/20-path.sh index c0ab004..f279f48 100644 --- a/bashrc.d/20-path.sh +++ b/bashrc.d/20-path.sh @@ -1,3 +1,4 @@ +#!/usr/bin/env bash # @file 20-path # @brief get-bashed module: 20-path # @description diff --git a/bashrc.d/40-completions.sh b/bashrc.d/40-completions.sh index fc8bc85..5ae6589 100644 --- a/bashrc.d/40-completions.sh +++ b/bashrc.d/40-completions.sh @@ -1,3 +1,4 @@ +#!/usr/bin/env bash # @file 40-completions # @brief get-bashed module: 40-completions # @description diff --git a/bashrc.d/66-doppler.sh b/bashrc.d/66-doppler.sh index 370dae1..ba318f2 100644 --- a/bashrc.d/66-doppler.sh +++ b/bashrc.d/66-doppler.sh @@ -1,3 +1,4 @@ +#!/usr/bin/env bash # @file 66-doppler # @brief get-bashed module: 66-doppler # @description diff --git a/bashrc.d/70-bash-it.sh b/bashrc.d/70-bash-it.sh index 74c8de8..0d27cab 100644 --- a/bashrc.d/70-bash-it.sh +++ b/bashrc.d/70-bash-it.sh @@ -1,3 +1,4 @@ +#!/usr/bin/env bash # @file 70-bash-it # @brief get-bashed module: 70-bash-it # @description diff --git a/bashrc.d/70-env.sh b/bashrc.d/70-env.sh index 1f31d57..265b476 100644 --- a/bashrc.d/70-env.sh +++ b/bashrc.d/70-env.sh @@ -1,3 +1,4 @@ +#!/usr/bin/env bash # @file 70-env # @brief get-bashed module: 70-env # @description diff --git a/bashrc.d/90-functions.sh b/bashrc.d/90-functions.sh index 4732cb3..2fb5613 100644 --- a/bashrc.d/90-functions.sh +++ b/bashrc.d/90-functions.sh @@ -1,3 +1,4 @@ +#!/usr/bin/env bash # @file 90-functions # @brief get-bashed module: 90-functions # @description diff --git a/bashrc.d/95-ssh-agent.sh b/bashrc.d/95-ssh-agent.sh index 2908056..3ea8e4a 100644 --- a/bashrc.d/95-ssh-agent.sh +++ b/bashrc.d/95-ssh-agent.sh @@ -1,3 +1,4 @@ +#!/usr/bin/env bash # @file 95-ssh-agent # @brief get-bashed module: 95-ssh-agent # @description diff --git a/install.sh b/install.sh index fafd54f..f2f141a 100755 --- a/install.sh +++ b/install.sh @@ -17,6 +17,7 @@ fi # Supports non-interactive and interactive installation with profiles, # feature flags, and installer bundles. +# shellcheck disable=SC3040 set -euo pipefail # @description Print usage help. diff --git a/installers/_helpers.sh b/installers/_helpers.sh index a660b7f..cb7e558 100755 --- a/installers/_helpers.sh +++ b/installers/_helpers.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# bashate: ignore=E003,E006 # @file installers-helpers # @brief Shared helpers for installers. # @description diff --git a/installers/tools.sh b/installers/tools.sh index ade46d9..7ffd116 100644 --- a/installers/tools.sh +++ b/installers/tools.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# shellcheck disable=SC2034 # @file tools # @brief Tool registry for get-bashed installers. # @description diff --git a/scripts/gen-docs.sh b/scripts/gen-docs.sh index 11d5f97..97ced83 100755 --- a/scripts/gen-docs.sh +++ b/scripts/gen-docs.sh @@ -20,12 +20,14 @@ shdoc < "$ROOT_DIR/installers/tools.sh" > "$ROOT_DIR/docs/INSTALLERS.md" # Combine all runtime modules TMP_MODULES="$(mktemp)" for f in "$ROOT_DIR/bashrc.d"/*.sh; do - echo "" >> "$TMP_MODULES" - cat "$f" >> "$TMP_MODULES" - echo "" >> "$TMP_MODULES" - echo "# ----" >> "$TMP_MODULES" - echo "" >> "$TMP_MODULES" - done + { + echo "" + cat "$f" + echo "" + echo "# ----" + echo "" + } >> "$TMP_MODULES" +done shdoc < "$TMP_MODULES" > "$ROOT_DIR/docs/MODULES.md" rm -f "$TMP_MODULES" From 5340a20fc41b0f9ca2377cdfdf1eb7632daf2cfa Mon Sep 17 00:00:00 2001 From: Jon B Date: Tue, 3 Feb 2026 14:26:04 -0600 Subject: [PATCH 12/33] feat: add dotfile linking option --- README.md | 5 +++++ TOOLS.md | 1 + inputrc | 7 +++++++ install.sh | 37 +++++++++++++++++++++++++++++++++++-- installers/_helpers.sh | 3 +++ 5 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 inputrc diff --git a/README.md b/README.md index eb282ed..3ee8224 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,11 @@ curl -fsSL -o install.sh https://raw.githubusercontent.com/jbcom/get-bashed/main sh install.sh ``` +To fully manage shell dotfiles (backup + symlink into `~/.get-bashed`): +```bash +sh install.sh --link-dotfiles +``` + ## Learn More Docs: `https://jonbogaty.com/get-bashed` diff --git a/TOOLS.md b/TOOLS.md index dc911d6..3c6f7b5 100644 --- a/TOOLS.md +++ b/TOOLS.md @@ -113,6 +113,7 @@ manager (Homebrew, apt, dnf, yum). Otherwise it falls back to plain prompts. - `./install.sh --list-features` shows available features. - `./install.sh --list-installers` shows the installer catalog. - `./install.sh --dry-run --install ` shows what would be installed. +- `./install.sh --link-dotfiles` backs up and symlinks shell dotfiles to `~/.get-bashed`. ## Docs diff --git a/inputrc b/inputrc new file mode 100644 index 0000000..3b0caf7 --- /dev/null +++ b/inputrc @@ -0,0 +1,7 @@ +# @file inputrc +# @brief get-bashed readline defaults. +# @description +# Minimal readline configuration used when linking dotfiles. + +set completion-ignore-case on +set show-all-if-ambiguous on diff --git a/install.sh b/install.sh index f2f141a..97d957a 100755 --- a/install.sh +++ b/install.sh @@ -30,6 +30,7 @@ Usage: install.sh [--prefix PATH] [--force] [--with-ui] [--features gnu_over_bsd,build_flags,...] [--install brew,asdf,doppler,...] [--vimrc-mode awesome|basic] + [--link-dotfiles] [--list] [--list-profiles] [--list-features] [--list-installers] [--dry-run] @@ -58,6 +59,7 @@ LIST_FEATURES=0 LIST_INSTALLERS=0 GROUP_INSTALLS="" VIMRC_MODE="awesome" +LINK_DOTFILES=0 # Feature flags (defaults) GET_BASHED_GNU=0 @@ -112,6 +114,8 @@ while [[ $# -gt 0 ]]; do exit 1 fi VIMRC_MODE="$2"; shift 2 ;; + --link-dotfiles) + LINK_DOTFILES=1; shift ;; --list) LIST=1; shift ;; --list-profiles) @@ -552,6 +556,7 @@ export GET_BASHED_SSH_AGENT=${GET_BASHED_SSH_AGENT} export GET_BASHED_USE_DOPPLER=${GET_BASHED_USE_DOPPLER} export GET_BASHED_USE_BASH_IT=${GET_BASHED_USE_BASH_IT} export GET_BASHED_VIMRC_MODE=${VIMRC_MODE} +export GET_BASHED_LINK_DOTFILES=${LINK_DOTFILES} __CFG__ # @internal @@ -570,7 +575,35 @@ BASHRC_SNIP='if [[ -r "$HOME/.get-bashed/bashrc" ]]; then source "$HOME/.get-bas BASH_PROFILE_LINE="# get-bashed: source login bash_profile" BASH_PROFILE_SNIP='if [[ -r "$HOME/.get-bashed/bash_profile" ]]; then source "$HOME/.get-bashed/bash_profile"; fi' -ensure_block "$HOME/.bashrc" "$BASHRC_LINE" "$BASHRC_SNIP" -ensure_block "$HOME/.bash_profile" "$BASH_PROFILE_LINE" "$BASH_PROFILE_SNIP" +if [[ "$LINK_DOTFILES" -ne 1 ]]; then + ensure_block "$HOME/.bashrc" "$BASHRC_LINE" "$BASHRC_SNIP" + ensure_block "$HOME/.bash_profile" "$BASH_PROFILE_LINE" "$BASH_PROFILE_SNIP" +fi + +link_dotfile() { + local name="$1" + local src="$PREFIX/$name" + local dst="$HOME/.${name}" + if [[ ! -e "$src" ]]; then + return 0 + fi + if [[ -L "$dst" ]] && [[ "$(readlink "$dst")" == "$src" ]]; then + return 0 + fi + if [[ -e "$dst" || -L "$dst" ]]; then + local backup_dir="$PREFIX/backup" + local stamp + stamp="$(date +%Y%m%d%H%M%S)" + mkdir -p "$backup_dir" + mv "$dst" "$backup_dir/${name}.${stamp}" + fi + ln -s "$src" "$dst" +} + +if [[ "$LINK_DOTFILES" -eq 1 ]]; then + link_dotfile "bashrc" + link_dotfile "bash_profile" + link_dotfile "inputrc" +fi echo "Installed get-bashed to $PREFIX" diff --git a/installers/_helpers.sh b/installers/_helpers.sh index cb7e558..a418dba 100755 --- a/installers/_helpers.sh +++ b/installers/_helpers.sh @@ -273,6 +273,9 @@ install_tool() { [[ -n "$url" ]] || continue local prefix="${GET_BASHED_HOME:-$HOME/.get-bashed}" local target="$prefix/vendor/$id" + if [[ -d "$target/.git" ]]; then + return 0 + fi mkdir -p "$prefix/vendor" git clone --depth=1 "$url" "$target" return 0 From dcff864105abe2c543bb2c708ca37e35219210ea Mon Sep 17 00:00:00 2001 From: Jon B Date: Tue, 3 Feb 2026 14:28:49 -0600 Subject: [PATCH 13/33] feat: add default dotfiles --- README.md | 2 ++ bash_aliases | 8 ++++++++ bashrc | 4 ++++ gitconfig | 26 ++++++++++++++++++++++++++ install.sh | 3 +++ vimrc | 21 +++++++++++++++++++++ 6 files changed, 64 insertions(+) create mode 100644 bash_aliases create mode 100644 gitconfig create mode 100644 vimrc diff --git a/README.md b/README.md index 3ee8224..b9dedd1 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ To fully manage shell dotfiles (backup + symlink into `~/.get-bashed`): ```bash sh install.sh --link-dotfiles ``` +This will link `bashrc`, `bash_profile`, `inputrc`, `bash_aliases`, `vimrc`, +and `gitconfig` to the versions under `~/.get-bashed`. ## Learn More diff --git a/bash_aliases b/bash_aliases new file mode 100644 index 0000000..b4b68cd --- /dev/null +++ b/bash_aliases @@ -0,0 +1,8 @@ +# @file bash_aliases +# @brief get-bashed aliases. +# @description +# Default aliases sourced by bashrc when present. + +alias ll='ls -alF' +alias la='ls -A' +alias l='ls -CF' diff --git a/bashrc b/bashrc index 263e919..c0098c0 100644 --- a/bashrc +++ b/bashrc @@ -17,6 +17,10 @@ if [[ -r "$GET_BASHED_HOME/get-bashedrc.sh" ]]; then source "$GET_BASHED_HOME/get-bashedrc.sh" fi +if [[ -r "$GET_BASHED_HOME/bash_aliases" ]]; then + source "$GET_BASHED_HOME/bash_aliases" +fi + for f in "$GET_BASHED_RC_DIR"/[0-9][0-9]-*.sh; do [[ -r "$f" ]] && source "$f" done diff --git a/gitconfig b/gitconfig new file mode 100644 index 0000000..7f36fa6 --- /dev/null +++ b/gitconfig @@ -0,0 +1,26 @@ +[user] + name = Your Name + email = you@example.com + +[core] + editor = vim + pager = less -FRSX + +[init] + defaultBranch = main + +[pull] + rebase = false + +[fetch] + prune = true + +[color] + ui = auto + +[alias] + st = status -sb + co = checkout + br = branch + ci = commit + lg = log --oneline --decorate --graph --all diff --git a/install.sh b/install.sh index 97d957a..eb5d5b7 100755 --- a/install.sh +++ b/install.sh @@ -604,6 +604,9 @@ if [[ "$LINK_DOTFILES" -eq 1 ]]; then link_dotfile "bashrc" link_dotfile "bash_profile" link_dotfile "inputrc" + link_dotfile "bash_aliases" + link_dotfile "vimrc" + link_dotfile "gitconfig" fi echo "Installed get-bashed to $PREFIX" diff --git a/vimrc b/vimrc new file mode 100644 index 0000000..15fae61 --- /dev/null +++ b/vimrc @@ -0,0 +1,21 @@ +" @file vimrc +" @brief get-bashed minimal vimrc. +" @description +" Minimal defaults used when not installing amix/vimrc. + +set nocompatible +set number +set ruler +set showcmd +set hidden +set nowrap +set tabstop=4 +set shiftwidth=2 +set expandtab +set smartindent +set ignorecase +set smartcase +set incsearch +set hlsearch +set backspace=indent,eol,start +syntax on From ad0ea29aed4a4e4b9d5e22a1e0c263afdb4064a1 Mon Sep 17 00:00:00 2001 From: Jon B Date: Tue, 3 Feb 2026 14:30:49 -0600 Subject: [PATCH 14/33] feat: add git identity flags --- README.md | 2 +- TOOLS.md | 1 + install.sh | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b9dedd1..809c630 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ sh install.sh To fully manage shell dotfiles (backup + symlink into `~/.get-bashed`): ```bash -sh install.sh --link-dotfiles +sh install.sh --link-dotfiles --name "Jane Doe" --email "jane@example.com" ``` This will link `bashrc`, `bash_profile`, `inputrc`, `bash_aliases`, `vimrc`, and `gitconfig` to the versions under `~/.get-bashed`. diff --git a/TOOLS.md b/TOOLS.md index 3c6f7b5..275e355 100644 --- a/TOOLS.md +++ b/TOOLS.md @@ -114,6 +114,7 @@ manager (Homebrew, apt, dnf, yum). Otherwise it falls back to plain prompts. - `./install.sh --list-installers` shows the installer catalog. - `./install.sh --dry-run --install ` shows what would be installed. - `./install.sh --link-dotfiles` backs up and symlinks shell dotfiles to `~/.get-bashed`. +- `./install.sh --name "Full Name" --email "me@example.com"` sets git identity. ## Docs diff --git a/install.sh b/install.sh index eb5d5b7..2d94a2a 100755 --- a/install.sh +++ b/install.sh @@ -31,6 +31,7 @@ Usage: install.sh [--prefix PATH] [--force] [--with-ui] [--install brew,asdf,doppler,...] [--vimrc-mode awesome|basic] [--link-dotfiles] + [--name "Full Name"] [--email "me@example.com"] [--list] [--list-profiles] [--list-features] [--list-installers] [--dry-run] @@ -60,6 +61,8 @@ LIST_INSTALLERS=0 GROUP_INSTALLS="" VIMRC_MODE="awesome" LINK_DOTFILES=0 +USER_NAME="" +USER_EMAIL="" # Feature flags (defaults) GET_BASHED_GNU=0 @@ -116,6 +119,20 @@ while [[ $# -gt 0 ]]; do VIMRC_MODE="$2"; shift 2 ;; --link-dotfiles) LINK_DOTFILES=1; shift ;; + --name|-n) + if [[ $# -lt 2 ]]; then + echo "Error: --name requires a value" >&2 + usage + exit 1 + fi + USER_NAME="$2"; shift 2 ;; + --email|-e) + if [[ $# -lt 2 ]]; then + echo "Error: --email requires a value" >&2 + usage + exit 1 + fi + USER_EMAIL="$2"; shift 2 ;; --list) LIST=1; shift ;; --list-profiles) @@ -408,6 +425,13 @@ if [[ "$AUTO" -eq 0 ]]; then INSTALLS="${INSTALLS// /,}" fi + if [[ -z "$USER_NAME" ]]; then + USER_NAME=$(dialog --clear --title "get-bashed" --inputbox "Git user.name" 8 60 "${USER_NAME}" 3>&1 1>&2 2>&3) || true + fi + if [[ -z "$USER_EMAIL" ]]; then + USER_EMAIL=$(dialog --clear --title "get-bashed" --inputbox "Git user.email" 8 60 "${USER_EMAIL}" 3>&1 1>&2 2>&3) || true + fi + dialog --clear --title "get-bashed" --yesno \ "Proceed with installation?\n\nFeatures: gnu_over_bsd=${GET_BASHED_GNU} build_flags=${GET_BASHED_BUILD_FLAGS} auto_tools=${GET_BASHED_AUTO_TOOLS} ssh_agent=${GET_BASHED_SSH_AGENT} doppler_env=${GET_BASHED_USE_DOPPLER} bash_it=${GET_BASHED_USE_BASH_IT}\nInstallers: ${INSTALLS}" \ 12 70 || exit 0 @@ -434,9 +458,18 @@ if [[ "$AUTO" -eq 0 ]]; then INSTALLS="$INSTALLS_INPUT" fi + if [[ -z "$USER_NAME" ]]; then + read -r -p "Git user.name (enter to skip): " USER_NAME + fi + if [[ -z "$USER_EMAIL" ]]; then + read -r -p "Git user.email (enter to skip): " USER_EMAIL + fi + echo "Proceeding with:" echo " Features: gnu_over_bsd=${GET_BASHED_GNU} build_flags=${GET_BASHED_BUILD_FLAGS} auto_tools=${GET_BASHED_AUTO_TOOLS} ssh_agent=${GET_BASHED_SSH_AGENT} doppler_env=${GET_BASHED_USE_DOPPLER} bash_it=${GET_BASHED_USE_BASH_IT}" echo " Installers: ${INSTALLS}" + [[ -n "$USER_NAME" ]] && echo " Git user.name: ${USER_NAME}" + [[ -n "$USER_EMAIL" ]] && echo " Git user.email: ${USER_EMAIL}" prompt_yes_no "Continue?" || exit 0 fi fi @@ -557,6 +590,8 @@ export GET_BASHED_USE_DOPPLER=${GET_BASHED_USE_DOPPLER} export GET_BASHED_USE_BASH_IT=${GET_BASHED_USE_BASH_IT} export GET_BASHED_VIMRC_MODE=${VIMRC_MODE} export GET_BASHED_LINK_DOTFILES=${LINK_DOTFILES} +export GET_BASHED_USER_NAME="${USER_NAME}" +export GET_BASHED_USER_EMAIL="${USER_EMAIL}" __CFG__ # @internal @@ -600,7 +635,29 @@ link_dotfile() { ln -s "$src" "$dst" } +apply_gitconfig() { + local cfg="$PREFIX/gitconfig" + [[ -f "$cfg" ]] || return 0 + local name_esc email_esc + name_esc="${USER_NAME//\\/\\\\}" + name_esc="${name_esc//|/\\|}" + email_esc="${USER_EMAIL//\\/\\\\}" + email_esc="${email_esc//|/\\|}" + if [[ -n "$USER_NAME" ]]; then + if grep -q "^[[:space:]]*name[[:space:]]*=" "$cfg"; then + sed -i.bak -E "s|^[[:space:]]*name[[:space:]]*=.*| name = ${name_esc}|" "$cfg" + fi + fi + if [[ -n "$USER_EMAIL" ]]; then + if grep -q "^[[:space:]]*email[[:space:]]*=" "$cfg"; then + sed -i.bak -E "s|^[[:space:]]*email[[:space:]]*=.*| email = ${email_esc}|" "$cfg" + fi + fi + rm -f "$cfg.bak" +} + if [[ "$LINK_DOTFILES" -eq 1 ]]; then + apply_gitconfig link_dotfile "bashrc" link_dotfile "bash_profile" link_dotfile "inputrc" From c7dab2a4209b1da6b935746e680dff87a675177a Mon Sep 17 00:00:00 2001 From: Jon B Date: Tue, 3 Feb 2026 14:44:19 -0600 Subject: [PATCH 15/33] feat: add optional deps by config --- README.md | 5 +++++ TOOLS.md | 1 + install.sh | 33 ++++++++++++++++++++++++++++++--- installers/tools.sh | 6 ++++++ 4 files changed, 42 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 809c630..5acf570 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,11 @@ sh install.sh --link-dotfiles --name "Jane Doe" --email "jane@example.com" This will link `bashrc`, `bash_profile`, `inputrc`, `bash_aliases`, `vimrc`, and `gitconfig` to the versions under `~/.get-bashed`. +Optional git signing (installs `gnupg` when enabled): +```bash +sh install.sh --features git_signing --install git +``` + ## Learn More Docs: `https://jonbogaty.com/get-bashed` diff --git a/TOOLS.md b/TOOLS.md index 275e355..42976b2 100644 --- a/TOOLS.md +++ b/TOOLS.md @@ -132,6 +132,7 @@ Use `--features` with comma-separated values: - `ssh_agent` - `doppler_env` - `bash_it` +- `git_signing` - `dev_tools` - `ops_tools` diff --git a/install.sh b/install.sh index 2d94a2a..fc38f32 100755 --- a/install.sh +++ b/install.sh @@ -71,6 +71,7 @@ GET_BASHED_AUTO_TOOLS=0 GET_BASHED_SSH_AGENT=0 GET_BASHED_USE_DOPPLER=0 GET_BASHED_USE_BASH_IT=0 +GET_BASHED_GIT_SIGNING=0 while [[ $# -gt 0 ]]; do case "$1" in @@ -205,6 +206,7 @@ apply_feature() { ssh_agent) GET_BASHED_SSH_AGENT=$v ;; doppler_env) GET_BASHED_USE_DOPPLER=$v ;; bash_it) GET_BASHED_USE_BASH_IT=$v ;; + git_signing) GET_BASHED_GIT_SIGNING=$v ;; dev_tools) GROUP_INSTALLS="${GROUP_INSTALLS},rg,fd,bat,fzf,jq,yq,tree,direnv,starship,nodejs,python,bash" ;; ops_tools) GROUP_INSTALLS="${GROUP_INSTALLS},gh,git_lfs,terraform,awscli,kubectl,helm,stern,doppler,nodejs,python,java,bash" ;; *) return 1 ;; @@ -249,6 +251,7 @@ load_installers() { fi INSTALLERS="$INSTALLERS $id" printf -v "INSTALL_DEPS_${id}" "%s" "${TOOL_DEPS[$id]:-}" + printf -v "INSTALL_OPT_DEPS_${id}" "%s" "${TOOL_OPT_DEPS[$id]:-}" printf -v "INSTALL_DESC_${id}" "%s" "${TOOL_DESC[$id]:-}" printf -v "INSTALL_PLATFORMS_${id}" "%s" "${TOOL_PLATFORMS[$id]:-}" done @@ -258,7 +261,26 @@ load_installers() { get_deps() { local id="$1" local var="INSTALL_DEPS_${id}" - echo "${!var:-}" + local deps="${!var:-}" + local opt_var="INSTALL_OPT_DEPS_${id}" + local opt="${!opt_var:-}" + if [[ -n "$opt" ]]; then + IFS=';' read -r -a _parts <<<"$opt" + local part key list val + for part in "${_parts[@]}"; do + key="${part%%:*}" + list="${part#*:}" + val="${!key:-0}" + if [[ "$val" == "1" ]]; then + if [[ -n "$deps" ]]; then + deps="${deps} ${list}" + else + deps="${list}" + fi + fi + done + fi + echo "${deps}" } # @internal @@ -390,6 +412,7 @@ if [[ "$AUTO" -eq 0 ]]; then ssh_agent "Auto-start ssh-agent" "$( [[ "$GET_BASHED_SSH_AGENT" -eq 1 ]] && echo on || echo off )" \ doppler_env "Enable Doppler env usage" "$( [[ "$GET_BASHED_USE_DOPPLER" -eq 1 ]] && echo on || echo off )" \ bash_it "Enable bash-it (if installed)" "$( [[ "$GET_BASHED_USE_BASH_IT" -eq 1 ]] && echo on || echo off )" \ + git_signing "Enable git signing (gnupg)" "$( [[ "$GET_BASHED_GIT_SIGNING" -eq 1 ]] && echo on || echo off )" \ dev_tools "Developer tool bundle" off \ ops_tools "Ops tool bundle" off \ 3>&1 1>&2 2>&3) || true @@ -400,6 +423,7 @@ if [[ "$AUTO" -eq 0 ]]; then GET_BASHED_SSH_AGENT=0 GET_BASHED_USE_DOPPLER=0 GET_BASHED_USE_BASH_IT=0 + GET_BASHED_GIT_SIGNING=0 for choice in $CHOICES; do apply_feature "${choice//\"/}" || true @@ -433,7 +457,7 @@ if [[ "$AUTO" -eq 0 ]]; then fi dialog --clear --title "get-bashed" --yesno \ - "Proceed with installation?\n\nFeatures: gnu_over_bsd=${GET_BASHED_GNU} build_flags=${GET_BASHED_BUILD_FLAGS} auto_tools=${GET_BASHED_AUTO_TOOLS} ssh_agent=${GET_BASHED_SSH_AGENT} doppler_env=${GET_BASHED_USE_DOPPLER} bash_it=${GET_BASHED_USE_BASH_IT}\nInstallers: ${INSTALLS}" \ + "Proceed with installation?\n\nFeatures: gnu_over_bsd=${GET_BASHED_GNU} build_flags=${GET_BASHED_BUILD_FLAGS} auto_tools=${GET_BASHED_AUTO_TOOLS} ssh_agent=${GET_BASHED_SSH_AGENT} doppler_env=${GET_BASHED_USE_DOPPLER} bash_it=${GET_BASHED_USE_BASH_IT} git_signing=${GET_BASHED_GIT_SIGNING}\nInstallers: ${INSTALLS}" \ 12 70 || exit 0 fi else @@ -450,6 +474,7 @@ if [[ "$AUTO" -eq 0 ]]; then prompt_yes_no "Start ssh-agent automatically (ssh_agent)?" && GET_BASHED_SSH_AGENT=1 prompt_yes_no "Enable Doppler env support (doppler_env)?" && GET_BASHED_USE_DOPPLER=1 prompt_yes_no "Enable bash-it (bash_it)?" && GET_BASHED_USE_BASH_IT=1 + prompt_yes_no "Enable git signing (git_signing)?" && GET_BASHED_GIT_SIGNING=1 prompt_yes_no "Include developer tool bundle (dev_tools)?" && apply_feature "dev_tools" prompt_yes_no "Include ops tool bundle (ops_tools)?" && apply_feature "ops_tools" @@ -466,7 +491,7 @@ if [[ "$AUTO" -eq 0 ]]; then fi echo "Proceeding with:" - echo " Features: gnu_over_bsd=${GET_BASHED_GNU} build_flags=${GET_BASHED_BUILD_FLAGS} auto_tools=${GET_BASHED_AUTO_TOOLS} ssh_agent=${GET_BASHED_SSH_AGENT} doppler_env=${GET_BASHED_USE_DOPPLER} bash_it=${GET_BASHED_USE_BASH_IT}" + echo " Features: gnu_over_bsd=${GET_BASHED_GNU} build_flags=${GET_BASHED_BUILD_FLAGS} auto_tools=${GET_BASHED_AUTO_TOOLS} ssh_agent=${GET_BASHED_SSH_AGENT} doppler_env=${GET_BASHED_USE_DOPPLER} bash_it=${GET_BASHED_USE_BASH_IT} git_signing=${GET_BASHED_GIT_SIGNING}" echo " Installers: ${INSTALLS}" [[ -n "$USER_NAME" ]] && echo " Git user.name: ${USER_NAME}" [[ -n "$USER_EMAIL" ]] && echo " Git user.email: ${USER_EMAIL}" @@ -516,6 +541,7 @@ if [[ "$LIST_FEATURES" -eq 1 ]]; then echo " ssh_agent" echo " doppler_env" echo " bash_it" + echo " git_signing" echo " dev_tools (bundle)" echo " ops_tools (bundle)" exit 0 @@ -588,6 +614,7 @@ export GET_BASHED_AUTO_TOOLS=${GET_BASHED_AUTO_TOOLS} export GET_BASHED_SSH_AGENT=${GET_BASHED_SSH_AGENT} export GET_BASHED_USE_DOPPLER=${GET_BASHED_USE_DOPPLER} export GET_BASHED_USE_BASH_IT=${GET_BASHED_USE_BASH_IT} +export GET_BASHED_GIT_SIGNING=${GET_BASHED_GIT_SIGNING} export GET_BASHED_VIMRC_MODE=${VIMRC_MODE} export GET_BASHED_LINK_DOTFILES=${LINK_DOTFILES} export GET_BASHED_USER_NAME="${USER_NAME}" diff --git a/installers/tools.sh b/installers/tools.sh index 7ffd116..ee3656e 100644 --- a/installers/tools.sh +++ b/installers/tools.sh @@ -19,6 +19,7 @@ declare -A TOOL_GIT_URL declare -A TOOL_CURL_URL declare -A TOOL_CURL_CMD declare -A TOOL_HANDLER +declare -A TOOL_OPT_DEPS tool_register() { local id="$1" desc="$2" deps="$3" platforms="$4" methods="$5" @@ -54,6 +55,10 @@ tool_handler() { TOOL_HANDLER["$id"]="$handler" } +tool_opt_deps() { + local id="$1" spec="$2" + TOOL_OPT_DEPS["$id"]="$spec" +} # Core tools tool_register brew "Homebrew/Linuxbrew installer" "" "macos,linux,wsl" "curl" tool_curl brew "https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh" "/bin/bash" @@ -106,6 +111,7 @@ tool_handler gnu_tools "install_gnu_tools" tool_register git "git" "" "macos,linux,wsl" "brew,apt,dnf,yum,pacman" tool_pkgs git "git" "git" "git" "git" "git" +tool_opt_deps git "GET_BASHED_GIT_SIGNING:gnupg" tool_register git_lfs "git-lfs" "" "macos,linux,wsl" "brew,apt,dnf,yum" tool_pkgs git_lfs "git-lfs" "git-lfs" "git-lfs" "git-lfs" "" From ad2d55cac881eacd55774b8e77e08b7443cbaf65 Mon Sep 17 00:00:00 2001 From: Jon B Date: Tue, 3 Feb 2026 14:45:54 -0600 Subject: [PATCH 16/33] test: add link-dotfiles coverage --- .pre-commit-config.yaml | 1 + installers/_helpers.sh | 2 +- tests/link_dotfiles.bats | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 tests/link_dotfiles.bats diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 02e752c..6034cb1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,3 +25,4 @@ repos: rev: 2.1.1 hooks: - id: bashate + args: [--ignore=E003,E006] diff --git a/installers/_helpers.sh b/installers/_helpers.sh index a418dba..1ec05c2 100755 --- a/installers/_helpers.sh +++ b/installers/_helpers.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# bashate: ignore=E003,E006 +# bashate:ignore=E003,E006 # @file installers-helpers # @brief Shared helpers for installers. # @description diff --git a/tests/link_dotfiles.bats b/tests/link_dotfiles.bats new file mode 100644 index 0000000..b51d1ea --- /dev/null +++ b/tests/link_dotfiles.bats @@ -0,0 +1,38 @@ +#!/usr/bin/env bats + +@test "link-dotfiles creates symlinks and updates gitconfig" { + TMPDIR="$(mktemp -d)" + HOME="$TMPDIR/home" + mkdir -p "$HOME" + + USER_NAME="Jane Doe" + USER_EMAIL="jane@example.com" + + HOME="$HOME" bash ./install.sh --auto --link-dotfiles --name "$USER_NAME" --email "$USER_EMAIL" --prefix "$HOME/.get-bashed" --force + + [ -L "$HOME/.bashrc" ] + [ -L "$HOME/.bash_profile" ] + [ -L "$HOME/.inputrc" ] + [ -L "$HOME/.bash_aliases" ] + [ -L "$HOME/.vimrc" ] + [ -L "$HOME/.gitconfig" ] + + run grep -F "name = ${USER_NAME}" "$HOME/.get-bashed/gitconfig" + [ "$status" -eq 0 ] + run grep -F "email = ${USER_EMAIL}" "$HOME/.get-bashed/gitconfig" + [ "$status" -eq 0 ] +} + +@test "link-dotfiles backs up existing dotfiles" { + TMPDIR="$(mktemp -d)" + HOME="$TMPDIR/home" + mkdir -p "$HOME" + echo "legacy" > "$HOME/.bashrc" + + HOME="$HOME" bash ./install.sh --auto --link-dotfiles --prefix "$HOME/.get-bashed" --force + + [ -L "$HOME/.bashrc" ] + [ -d "$HOME/.get-bashed/backup" ] + run ls "$HOME/.get-bashed/backup" | grep -E '^bashrc\.[0-9]+' + [ "$status" -eq 0 ] +} From d6d802e4b862cece6ce96e4db6492bd6488bfbe8 Mon Sep 17 00:00:00 2001 From: Jon B Date: Tue, 3 Feb 2026 14:48:00 -0600 Subject: [PATCH 17/33] docs: update memory bank and README --- README.md | 9 +++++++++ memory-bank/activeContext.md | 21 +++++++++++---------- memory-bank/progress.md | 10 +++++++--- memory-bank/systemPatterns.md | 3 ++- tests/config_output.bats | 17 +++++++++++++++++ 5 files changed, 46 insertions(+), 14 deletions(-) create mode 100644 tests/config_output.bats diff --git a/README.md b/README.md index 5acf570..a94c932 100644 --- a/README.md +++ b/README.md @@ -46,9 +46,18 @@ Use `--features` with comma lists (supports `no-` prefix): - `auto_tools` - `ssh_agent` - `doppler_env` +- `bash_it` +- `git_signing` - `dev_tools` (bundle) - `ops_tools` (bundle) +## Tool Registry + +Installers are driven by a single registry in `installers/tools.sh`. Each tool +declares supported install methods (brew/apt/dnf/yum/pacman/pipx/git/curl) and +dependencies, and optional dependencies can be enabled via features (for +example, `git_signing` adds `gnupg` when installing `git`). + ## Installers Available installers (comma list): diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md index 4db54dc..edf3885 100644 --- a/memory-bank/activeContext.md +++ b/memory-bank/activeContext.md @@ -1,14 +1,15 @@ # Active Context -- Focus: finalize repo hardening, docs, CI/CD, and initial release PR. +- Focus: stabilize installer registry, idempotent dotfile linking, and CI. - Recent changes: - - installer refactor (profiles/features/installers, list/dry-run) - - shdoc pipeline + docs generation - - CI setup with get-bashed bootstrapping - - actions pinned to latest stable SHAs - - dependabot config + auto-merge workflow - - PR title conventional commit enforcement + - centralized tool registry in `installers/tools.sh` + - unified install handlers in `installers/_helpers.sh` + - optional dependencies keyed off config (e.g. git_signing -> gnupg) + - added dotfile linking (`--link-dotfiles`) with backups + - added default dotfiles (inputrc, vimrc, gitconfig, bash_aliases) + - added `--name/--email` prompts + config wiring + - added tests for dotfile linking and config output - Next steps: - - regenerate docs after shdoc is installed (done) - - open release branch + PR for initial release - - verify CI docs job uses local shdoc install + - finish CI green (bashate/shellcheck) + - expand BATS coverage beyond smoke tests + - refresh README to document registry + optional deps diff --git a/memory-bank/progress.md b/memory-bank/progress.md index 8ed3b47..150f21b 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -5,11 +5,15 @@ - Memory bank + docs pipeline - CI workflows and release automation - Pre-commit policy and security docs +- Centralized tools registry + handlers +- Dotfile linking with backups + defaults +- Git identity prompts + config output +- Optional dependency gating by feature flags ## What's left -- Run `scripts/gen-docs.sh` after shdoc install (done) -- Create release branch and open PR for initial release -- Review CI for shdoc availability on all platforms +- CI green after bashate/shellcheck changes +- Expand BATS coverage to core flows +- Document tool registry and optional deps in README ## Known issues - shdoc not available via Homebrew on macOS; uses local prefix install. diff --git a/memory-bank/systemPatterns.md b/memory-bank/systemPatterns.md index 5a268a6..f647600 100644 --- a/memory-bank/systemPatterns.md +++ b/memory-bank/systemPatterns.md @@ -2,6 +2,7 @@ - Modular runtime: `bashrc` loads ordered `bashrc.d/*` modules. - Config generation: installer writes `get-bashedrc.sh` and reads it at startup. -- Installers: dependency-aware scripts with metadata and helper functions. +- Installers: centralized tool registry in `installers/tools.sh` with handlers in `_helpers.sh`. - Profiles/features: profiles set defaults; features override; bundles expand installers. - Secrets: sourced from `~/.get-bashed/secrets.d/*.sh` only. +- Optional deps: tool optional dependencies can be gated by config flags. diff --git a/tests/config_output.bats b/tests/config_output.bats new file mode 100644 index 0000000..cd1a89a --- /dev/null +++ b/tests/config_output.bats @@ -0,0 +1,17 @@ +#!/usr/bin/env bats + +@test "installer writes get-bashedrc with git identity" { + TMPDIR="$(mktemp -d)" + HOME="$TMPDIR/home" + mkdir -p "$HOME" + + USER_NAME="Jane Doe" + USER_EMAIL="jane@example.com" + + HOME="$HOME" bash ./install.sh --auto --name "$USER_NAME" --email "$USER_EMAIL" --prefix "$HOME/.get-bashed" --force + + run grep -F "GET_BASHED_USER_NAME=\"${USER_NAME}\"" "$HOME/.get-bashed/get-bashedrc.sh" + [ "$status" -eq 0 ] + run grep -F "GET_BASHED_USER_EMAIL=\"${USER_EMAIL}\"" "$HOME/.get-bashed/get-bashedrc.sh" + [ "$status" -eq 0 ] +} From d1bde57166e4ac61071424899faf948af9d53599 Mon Sep 17 00:00:00 2001 From: Jon B Date: Tue, 3 Feb 2026 15:01:00 -0600 Subject: [PATCH 18/33] test: expand coverage and address CR --- bashrc.d/70-bash-it.sh | 4 ++-- install.sh | 31 +++++++++++++++++-------------- scripts/gen-docs.sh | 4 +++- tests/optional_deps.bats | 12 ++++++++++++ tests/registry_idempotent.bats | 14 ++++++++++++++ 5 files changed, 48 insertions(+), 17 deletions(-) create mode 100644 tests/optional_deps.bats create mode 100644 tests/registry_idempotent.bats diff --git a/bashrc.d/70-bash-it.sh b/bashrc.d/70-bash-it.sh index 0d27cab..e54f53e 100644 --- a/bashrc.d/70-bash-it.sh +++ b/bashrc.d/70-bash-it.sh @@ -24,12 +24,12 @@ if [[ "${GET_BASHED_USE_BASH_IT:-0}" == "1" ]]; then if [[ -n "${GET_BASHED_BASH_IT_SEARCH:-}" ]]; then if [[ -z "${GET_BASHED_BASH_IT_APPLIED:-}" ]]; then GET_BASHED_BASH_IT_APPLIED=1 - local action="${GET_BASHED_BASH_IT_ACTION:-enable}" + action="${GET_BASHED_BASH_IT_ACTION:-enable}" case "$action" in enable|disable) ;; *) action="enable" ;; esac - local refresh="" + refresh="" if [[ "${GET_BASHED_BASH_IT_REFRESH:-0}" == "1" ]]; then refresh="--refresh" fi diff --git a/install.sh b/install.sh index fc38f32..9e1d386 100755 --- a/install.sh +++ b/install.sh @@ -241,6 +241,7 @@ installer_exists() { # Installer registry INSTALLERS="" +declare -A INSTALL_IN_PROGRESS # @internal load_installers() { local id @@ -303,9 +304,14 @@ run_install() { echo "Invalid installer id: $id" >&2 return 1 fi + if [[ "${INSTALL_IN_PROGRESS[$id]:-0}" == "1" ]]; then + echo "Circular dependency detected at: $id" >&2 + return 1 + fi if is_done "$id"; then return 0 fi + INSTALL_IN_PROGRESS["$id"]=1 for dep in $(get_deps "$id"); do run_install "$dep" done @@ -318,6 +324,7 @@ run_install() { install_tool "$id" fi fi + unset INSTALL_IN_PROGRESS["$id"] mark_done "$id" } @@ -665,32 +672,28 @@ link_dotfile() { apply_gitconfig() { local cfg="$PREFIX/gitconfig" [[ -f "$cfg" ]] || return 0 - local name_esc email_esc - name_esc="${USER_NAME//\\/\\\\}" - name_esc="${name_esc//|/\\|}" - email_esc="${USER_EMAIL//\\/\\\\}" - email_esc="${email_esc//|/\\|}" if [[ -n "$USER_NAME" ]]; then - if grep -q "^[[:space:]]*name[[:space:]]*=" "$cfg"; then - sed -i.bak -E "s|^[[:space:]]*name[[:space:]]*=.*| name = ${name_esc}|" "$cfg" - fi + git config -f "$cfg" user.name "$USER_NAME" fi if [[ -n "$USER_EMAIL" ]]; then - if grep -q "^[[:space:]]*email[[:space:]]*=" "$cfg"; then - sed -i.bak -E "s|^[[:space:]]*email[[:space:]]*=.*| email = ${email_esc}|" "$cfg" - fi + git config -f "$cfg" user.email "$USER_EMAIL" fi - rm -f "$cfg.bak" } if [[ "$LINK_DOTFILES" -eq 1 ]]; then - apply_gitconfig + if [[ -z "$USER_NAME" || -z "$USER_EMAIL" ]]; then + echo "Skipping gitconfig link: missing --name/--email." >&2 + else + apply_gitconfig + fi link_dotfile "bashrc" link_dotfile "bash_profile" link_dotfile "inputrc" link_dotfile "bash_aliases" link_dotfile "vimrc" - link_dotfile "gitconfig" + if [[ -n "$USER_NAME" && -n "$USER_EMAIL" ]]; then + link_dotfile "gitconfig" + fi fi echo "Installed get-bashed to $PREFIX" diff --git a/scripts/gen-docs.sh b/scripts/gen-docs.sh index 97ced83..ec2786b 100755 --- a/scripts/gen-docs.sh +++ b/scripts/gen-docs.sh @@ -19,6 +19,7 @@ shdoc < "$ROOT_DIR/installers/tools.sh" > "$ROOT_DIR/docs/INSTALLERS.md" # Combine all runtime modules TMP_MODULES="$(mktemp)" +shopt -s nullglob for f in "$ROOT_DIR/bashrc.d"/*.sh; do { echo "" @@ -28,6 +29,7 @@ for f in "$ROOT_DIR/bashrc.d"/*.sh; do echo "" } >> "$TMP_MODULES" done +shopt -u nullglob shdoc < "$TMP_MODULES" > "$ROOT_DIR/docs/MODULES.md" rm -f "$TMP_MODULES" @@ -38,7 +40,7 @@ rm -f "$TMP_MODULES" echo "Generated docs:" for f in "$ROOT_DIR/docs"/*.md; do base="$(basename "$f")" - [[ "$base" == "index.md" ]] && continue + [[ "$base" == "INDEX.md" ]] && continue echo "- [$base]($base)" done } > "$ROOT_DIR/docs/INDEX.md" diff --git a/tests/optional_deps.bats b/tests/optional_deps.bats new file mode 100644 index 0000000..f383b3d --- /dev/null +++ b/tests/optional_deps.bats @@ -0,0 +1,12 @@ +#!/usr/bin/env bats + +@test "optional deps are added when feature enabled" { + TMPDIR="$(mktemp -d)" + HOME="$TMPDIR/home" + mkdir -p "$HOME" + + HOME="$HOME" bash ./install.sh --auto --prefix "$HOME/.get-bashed" --force --features git_signing --install git --dry-run > "$TMPDIR/out" + + run grep -F "would install: gnupg" "$TMPDIR/out" + [ "$status" -eq 0 ] +} diff --git a/tests/registry_idempotent.bats b/tests/registry_idempotent.bats new file mode 100644 index 0000000..71a1c59 --- /dev/null +++ b/tests/registry_idempotent.bats @@ -0,0 +1,14 @@ +#!/usr/bin/env bats + +@test "tool registry exposes expected installers" { + TMPDIR="$(mktemp -d)" + HOME="$TMPDIR/home" + mkdir -p "$HOME" + + HOME="$HOME" bash ./install.sh --auto --list-installers > "$TMPDIR/list" + + run grep -F " - git" "$TMPDIR/list" + [ "$status" -eq 0 ] + run grep -F " - bash_it" "$TMPDIR/list" + [ "$status" -eq 0 ] +} From f42f39dd255b3b2db476c36facf1f8c4dd9d39d8 Mon Sep 17 00:00:00 2001 From: Jon B Date: Tue, 3 Feb 2026 15:09:32 -0600 Subject: [PATCH 19/33] test: add bats helpers and split CI --- .github/workflows/ci.yml | 15 +++++++++++---- .gitignore | 1 + CONTRIBUTING.md | 1 + Makefile | 6 +++++- docs/INDEX.md | 1 - docs/INSTALLER.md | 6 +++--- docs/INSTALLERS_HELPERS.md | 26 +++++++++++++------------- docs/MODULES.md | 8 ++++---- install.sh | 16 ++++++++++++++-- scripts/gen-docs.sh | 31 +++++++++++++++++++++++++++++++ scripts/test-setup.sh | 28 ++++++++++++++++++++++++++++ tests/config_output.bats | 17 +++++++++++++++-- tests/install.bats | 10 ++++++---- tests/link_dotfiles.bats | 31 ++++++++++++++++++++----------- tests/optional_deps.bats | 4 +++- tests/registry_idempotent.bats | 6 ++++-- tests/test_helper.bash | 5 +++++ 17 files changed, 164 insertions(+), 48 deletions(-) create mode 100644 .gitignore create mode 100755 scripts/test-setup.sh create mode 100644 tests/test_helper.bash diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5eae8ca..721c993 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,13 +5,20 @@ on: pull_request: jobs: - test: + lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: CI setup (get-bashed) - run: ./scripts/ci-setup.sh "bats,shellcheck,actionlint,bashate,pre_commit" - - name: Run tests - run: bats tests + run: ./scripts/ci-setup.sh "shellcheck,actionlint,bashate,pre_commit,shdoc" - name: Pre-commit run: ./scripts/pre-commit-ci.sh + tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: bats-core/bats-action@e412797c46257a2dbf3775f6f6010b33ee6cb99f # v3.0.1 + - name: Fetch Bats helpers + run: ./scripts/test-setup.sh + - name: Run tests + run: bats tests diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3e4594f --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +tests/lib/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b7b51aa..7cb314b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,6 +32,7 @@ pre-commit run --all-files ## Tests ```bash +./scripts/test-setup.sh bats tests ``` diff --git a/Makefile b/Makefile index e0b7d10..ef7e033 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,12 @@ # Docs and lint targets for get-bashed. -.PHONY: docs lint +.PHONY: docs lint test docs: ./scripts/gen-docs.sh lint: pre-commit run --all-files + +test: + ./scripts/test-setup.sh + bats tests diff --git a/docs/INDEX.md b/docs/INDEX.md index 10e15e3..d124eec 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -2,7 +2,6 @@ Generated docs: - [CONFIG.md](CONFIG.md) -- [INDEX.md](INDEX.md) - [INSTALLER.md](INSTALLER.md) - [INSTALLERS.md](INSTALLERS.md) - [INSTALLERS_HELPERS.md](INSTALLERS_HELPERS.md) diff --git a/docs/INSTALLER.md b/docs/INSTALLER.md index 946f832..46e08ce 100644 --- a/docs/INSTALLER.md +++ b/docs/INSTALLER.md @@ -10,9 +10,9 @@ feature flags, and installer bundles. ## Index * [usage](#usage) -* [apply_profile](#applyprofile) -* [apply_feature](#applyfeature) -* [split_csv](#splitcsv) +* [apply_profile](#apply_profile) +* [apply_feature](#apply_feature) +* [split_csv](#split_csv) ### usage diff --git a/docs/INSTALLERS_HELPERS.md b/docs/INSTALLERS_HELPERS.md index 5b3ca63..872297c 100644 --- a/docs/INSTALLERS_HELPERS.md +++ b/docs/INSTALLERS_HELPERS.md @@ -9,19 +9,19 @@ installer scripts. ## Index -* [install_tool](#installtool) -* [install_asdf](#installasdf) -* [install_gnu_tools](#installgnutools) -* [install_java](#installjava) -* [install_nodejs](#installnodejs) -* [install_python](#installpython) -* [install_shdoc](#installshdoc) -* [install_vimrc](#installvimrc) -* [install_actionlint](#installactionlint) -* [pkg_install](#pkginstall) -* [asdf_has_plugin](#asdfhasplugin) -* [asdf_install_plugin](#asdfinstallplugin) -* [pipx_install](#pipxinstall) +* [install_tool](#install_tool) +* [install_asdf](#install_asdf) +* [install_gnu_tools](#install_gnu_tools) +* [install_java](#install_java) +* [install_nodejs](#install_nodejs) +* [install_python](#install_python) +* [install_shdoc](#install_shdoc) +* [install_vimrc](#install_vimrc) +* [install_actionlint](#install_actionlint) +* [pkg_install](#pkg_install) +* [asdf_has_plugin](#asdf_has_plugin) +* [asdf_install_plugin](#asdf_install_plugin) +* [pipx_install](#pipx_install) ### install_tool diff --git a/docs/MODULES.md b/docs/MODULES.md index 87fd6e6..0e023bb 100644 --- a/docs/MODULES.md +++ b/docs/MODULES.md @@ -8,10 +8,10 @@ Runtime module loaded by get-bashed in lexicographic order. ## Index -* [_path_add_front](#pathaddfront) -* [install_cli_tools](#installclitools) -* [doppler_shell](#dopplershell) -* [get_bashed_component](#getbashedcomponent) +* [_path_add_front](#_path_add_front) +* [install_cli_tools](#install_cli_tools) +* [doppler_shell](#doppler_shell) +* [get_bashed_component](#get_bashed_component) * [ex](#ex) ### _path_add_front diff --git a/install.sh b/install.sh index 9e1d386..2215cca 100755 --- a/install.sh +++ b/install.sh @@ -366,6 +366,12 @@ fi # Load installer registry early for interactive UI. load_installers +# Preserve CLI features/installers so profiles do not clobber them. +CLI_FEATURES="${FEATURES:-}" +CLI_INSTALLS="${INSTALLS:-}" +FEATURES="" +INSTALLS="" + # Apply profiles first if [[ -n "$PROFILES" ]]; then for p in $(split_csv "$PROFILES"); do @@ -378,20 +384,26 @@ if [[ -n "$PROFILES" ]]; then if [[ -r "$PROFILE_FILE" ]]; then # shellcheck disable=SC1090 source "$PROFILE_FILE" - if [[ -n "${FEATURES:-}" ]]; then - for f in $(split_csv "$FEATURES"); do + PROFILE_FEATURES="${FEATURES:-}" + if [[ -n "$PROFILE_FEATURES" ]]; then + for f in $(split_csv "$PROFILE_FEATURES"); do apply_feature "$f" || { echo "Unknown feature: $f"; exit 1; } done fi if [[ -n "${INSTALLS:-}" ]]; then GROUP_INSTALLS="${GROUP_INSTALLS},${INSTALLS}" fi + FEATURES="" + INSTALLS="" else apply_profile "$p" || { echo "Unknown profile: $p"; exit 1; } fi done fi +FEATURES="$CLI_FEATURES" +INSTALLS="$CLI_INSTALLS" + # Apply features overrides if [[ -n "$FEATURES" ]]; then for f in $(split_csv "$FEATURES"); do diff --git a/scripts/gen-docs.sh b/scripts/gen-docs.sh index ec2786b..537bbdd 100755 --- a/scripts/gen-docs.sh +++ b/scripts/gen-docs.sh @@ -17,6 +17,36 @@ shdoc < "$ROOT_DIR/install.sh" > "$ROOT_DIR/docs/INSTALLER.md" shdoc < "$ROOT_DIR/installers/_helpers.sh" > "$ROOT_DIR/docs/INSTALLERS_HELPERS.md" shdoc < "$ROOT_DIR/installers/tools.sh" > "$ROOT_DIR/docs/INSTALLERS.md" +fix_toc_anchors() { + local file="$1" tmp + tmp="$(mktemp)" + awk ' + function anchorize(text, t) { + t = tolower(text) + gsub(/ /, "-", t) + gsub(/[^a-z0-9_-]/, "", t) + return t + } + { + if (match($0, /^\* \[[^]]+\]\(#/)) { + line = $0 + sub(/^\* \[/, "", line) + text = line + sub(/\].*$/, "", text) + anchor = anchorize(text) + print "* [" text "](#" anchor ")" + } else { + print $0 + } + } + ' "$file" > "$tmp" + mv "$tmp" "$file" +} + +for doc in "$ROOT_DIR/docs/INSTALLER.md" "$ROOT_DIR/docs/INSTALLERS_HELPERS.md" "$ROOT_DIR/docs/INSTALLERS.md"; do + fix_toc_anchors "$doc" +done + # Combine all runtime modules TMP_MODULES="$(mktemp)" shopt -s nullglob @@ -32,6 +62,7 @@ done shopt -u nullglob shdoc < "$TMP_MODULES" > "$ROOT_DIR/docs/MODULES.md" rm -f "$TMP_MODULES" +fix_toc_anchors "$ROOT_DIR/docs/MODULES.md" # Generate index { diff --git a/scripts/test-setup.sh b/scripts/test-setup.sh new file mode 100755 index 0000000..aa9622a --- /dev/null +++ b/scripts/test-setup.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# @file test-setup +# @brief Fetch Bats test helper libraries. +# @description +# Downloads bats-support, bats-assert, and bats-file into tests/lib. + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +LIB_DIR="$ROOT_DIR/tests/lib" + +mkdir -p "$LIB_DIR" + +clone_lib() { + local name="$1" repo="$2" sha="$3" + local dest="$LIB_DIR/$name" + if [[ -d "$dest/.git" ]]; then + return 0 + fi + git clone --quiet "$repo" "$dest" + git -C "$dest" checkout --quiet "$sha" +} + +clone_lib "bats-support" "https://github.com/bats-core/bats-support.git" "64e7436962affbe15974d181173c37e1fac70073" +clone_lib "bats-assert" "https://github.com/bats-core/bats-assert.git" "123860c029685bc0a4150ed57ee97fc7f7cc9d31" +clone_lib "bats-file" "https://github.com/bats-core/bats-file.git" "13ad5e2ffcc360281432db3d43a306f7b3667d60" + +echo "Bats libs ready in $LIB_DIR" diff --git a/tests/config_output.bats b/tests/config_output.bats index cd1a89a..d05c307 100644 --- a/tests/config_output.bats +++ b/tests/config_output.bats @@ -1,5 +1,7 @@ #!/usr/bin/env bats +load test_helper + @test "installer writes get-bashedrc with git identity" { TMPDIR="$(mktemp -d)" HOME="$TMPDIR/home" @@ -11,7 +13,18 @@ HOME="$HOME" bash ./install.sh --auto --name "$USER_NAME" --email "$USER_EMAIL" --prefix "$HOME/.get-bashed" --force run grep -F "GET_BASHED_USER_NAME=\"${USER_NAME}\"" "$HOME/.get-bashed/get-bashedrc.sh" - [ "$status" -eq 0 ] + assert_success run grep -F "GET_BASHED_USER_EMAIL=\"${USER_EMAIL}\"" "$HOME/.get-bashed/get-bashedrc.sh" - [ "$status" -eq 0 ] + assert_success +} + +@test "cli features override profile defaults" { + TMPDIR="$(mktemp -d)" + HOME="$TMPDIR/home" + mkdir -p "$HOME" + + HOME="$HOME" bash ./install.sh --auto --profiles minimal --features gnu_over_bsd --prefix "$HOME/.get-bashed" --force + + run grep -F "export GET_BASHED_GNU=1" "$HOME/.get-bashed/get-bashedrc.sh" + assert_success } diff --git a/tests/install.bats b/tests/install.bats index 9ef6dc5..a250db8 100755 --- a/tests/install.bats +++ b/tests/install.bats @@ -1,15 +1,17 @@ #!/usr/bin/env bats +load test_helper + @test "installer writes to prefix and wires bashrc" { TMPDIR="$(mktemp -d)" HOME="$TMPDIR" bash ./install.sh --prefix "$TMPDIR/.get-bashed" --force - [ -f "$TMPDIR/.get-bashed/bashrc" ] - [ -d "$TMPDIR/.get-bashed/bashrc.d" ] + assert_file_exist "$TMPDIR/.get-bashed/bashrc" + assert_dir_exist "$TMPDIR/.get-bashed/bashrc.d" run grep -F "# get-bashed: source modular bashrc" "$TMPDIR/.bashrc" - [ "$status" -eq 0 ] + assert_success run grep -F "# get-bashed: source login bash_profile" "$TMPDIR/.bash_profile" - [ "$status" -eq 0 ] + assert_success } diff --git a/tests/link_dotfiles.bats b/tests/link_dotfiles.bats index b51d1ea..496cfdd 100644 --- a/tests/link_dotfiles.bats +++ b/tests/link_dotfiles.bats @@ -1,5 +1,7 @@ #!/usr/bin/env bats +load test_helper + @test "link-dotfiles creates symlinks and updates gitconfig" { TMPDIR="$(mktemp -d)" HOME="$TMPDIR/home" @@ -10,17 +12,23 @@ HOME="$HOME" bash ./install.sh --auto --link-dotfiles --name "$USER_NAME" --email "$USER_EMAIL" --prefix "$HOME/.get-bashed" --force - [ -L "$HOME/.bashrc" ] - [ -L "$HOME/.bash_profile" ] - [ -L "$HOME/.inputrc" ] - [ -L "$HOME/.bash_aliases" ] - [ -L "$HOME/.vimrc" ] - [ -L "$HOME/.gitconfig" ] + run test -L "$HOME/.bashrc" + assert_success + run test -L "$HOME/.bash_profile" + assert_success + run test -L "$HOME/.inputrc" + assert_success + run test -L "$HOME/.bash_aliases" + assert_success + run test -L "$HOME/.vimrc" + assert_success + run test -L "$HOME/.gitconfig" + assert_success run grep -F "name = ${USER_NAME}" "$HOME/.get-bashed/gitconfig" - [ "$status" -eq 0 ] + assert_success run grep -F "email = ${USER_EMAIL}" "$HOME/.get-bashed/gitconfig" - [ "$status" -eq 0 ] + assert_success } @test "link-dotfiles backs up existing dotfiles" { @@ -31,8 +39,9 @@ HOME="$HOME" bash ./install.sh --auto --link-dotfiles --prefix "$HOME/.get-bashed" --force - [ -L "$HOME/.bashrc" ] - [ -d "$HOME/.get-bashed/backup" ] + run test -L "$HOME/.bashrc" + assert_success + assert_dir_exist "$HOME/.get-bashed/backup" run ls "$HOME/.get-bashed/backup" | grep -E '^bashrc\.[0-9]+' - [ "$status" -eq 0 ] + assert_success } diff --git a/tests/optional_deps.bats b/tests/optional_deps.bats index f383b3d..3b26256 100644 --- a/tests/optional_deps.bats +++ b/tests/optional_deps.bats @@ -1,5 +1,7 @@ #!/usr/bin/env bats +load test_helper + @test "optional deps are added when feature enabled" { TMPDIR="$(mktemp -d)" HOME="$TMPDIR/home" @@ -8,5 +10,5 @@ HOME="$HOME" bash ./install.sh --auto --prefix "$HOME/.get-bashed" --force --features git_signing --install git --dry-run > "$TMPDIR/out" run grep -F "would install: gnupg" "$TMPDIR/out" - [ "$status" -eq 0 ] + assert_success } diff --git a/tests/registry_idempotent.bats b/tests/registry_idempotent.bats index 71a1c59..761e6a7 100644 --- a/tests/registry_idempotent.bats +++ b/tests/registry_idempotent.bats @@ -1,5 +1,7 @@ #!/usr/bin/env bats +load test_helper + @test "tool registry exposes expected installers" { TMPDIR="$(mktemp -d)" HOME="$TMPDIR/home" @@ -8,7 +10,7 @@ HOME="$HOME" bash ./install.sh --auto --list-installers > "$TMPDIR/list" run grep -F " - git" "$TMPDIR/list" - [ "$status" -eq 0 ] + assert_success run grep -F " - bash_it" "$TMPDIR/list" - [ "$status" -eq 0 ] + assert_success } diff --git a/tests/test_helper.bash b/tests/test_helper.bash new file mode 100644 index 0000000..ebd96dd --- /dev/null +++ b/tests/test_helper.bash @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +load "${BATS_TEST_DIRNAME}/lib/bats-support/load" +load "${BATS_TEST_DIRNAME}/lib/bats-assert/load" +load "${BATS_TEST_DIRNAME}/lib/bats-file/load" From cca5675ee664c62e6e191616d0a5d5f5e7db9ce4 Mon Sep 17 00:00:00 2001 From: Jon B Date: Tue, 3 Feb 2026 15:10:53 -0600 Subject: [PATCH 20/33] fix: harden installers and tests --- installers/_helpers.sh | 36 ++++++++++++++++++++++++++++-------- tests/link_dotfiles.bats | 2 +- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/installers/_helpers.sh b/installers/_helpers.sh index 1ec05c2..3aad2d3 100755 --- a/installers/_helpers.sh +++ b/installers/_helpers.sh @@ -197,9 +197,12 @@ component_install() { local prefix="${GET_BASHED_HOME:-$HOME/.get-bashed}" local target="$prefix/vendor/$term" mkdir -p "$prefix/vendor" - git clone --depth=1 "${GET_BASHED_GIT_SOURCES[$term]}" "$target" + if ! git clone --depth=1 "${GET_BASHED_GIT_SOURCES[$term]}" "$target"; then + echo "Failed to clone $term" >&2 + return 1 + fi if [[ -n "${GET_BASHED_GIT_POST[$term]:-}" ]]; then - (cd "$target" && sh "${GET_BASHED_GIT_POST[$term]}") + (cd "$target" && sh "${GET_BASHED_GIT_POST[$term]}") || return 1 fi return 0 fi @@ -207,9 +210,16 @@ component_install() { if [[ -n "${GET_BASHED_CURL_SOURCES[$term]:-}" ]] && _using_curl; then local tmp_dir tmp_dir="$(mktemp -d)" - curl -fsSL "${GET_BASHED_CURL_SOURCES[$term]}" -o "$tmp_dir/install.sh" + if ! curl -fsSL "${GET_BASHED_CURL_SOURCES[$term]}" -o "$tmp_dir/install.sh"; then + rm -rf "$tmp_dir" + echo "Failed to download installer for $term" >&2 + return 1 + fi local cmd="${GET_BASHED_CURL_CMD[$term]:-bash}" - $cmd "$tmp_dir/install.sh" + if ! $cmd "$tmp_dir/install.sh"; then + rm -rf "$tmp_dir" + return 1 + fi rm -rf "$tmp_dir" return 0 fi @@ -277,8 +287,7 @@ install_tool() { return 0 fi mkdir -p "$prefix/vendor" - git clone --depth=1 "$url" "$target" - return 0 + git clone --depth=1 "$url" "$target" && return 0 ;; curl) _using_curl || continue @@ -286,9 +295,15 @@ install_tool() { [[ -n "$url" ]] || continue local tmp_dir tmp_dir="$(mktemp -d)" - curl -fsSL "$url" -o "$tmp_dir/install.sh" + if ! curl -fsSL "$url" -o "$tmp_dir/install.sh"; then + rm -rf "$tmp_dir" + return 1 + fi local cmd="${TOOL_CURL_CMD[$id]:-bash}" - $cmd "$tmp_dir/install.sh" + if ! $cmd "$tmp_dir/install.sh"; then + rm -rf "$tmp_dir" + return 1 + fi rm -rf "$tmp_dir" return 0 ;; @@ -496,6 +511,11 @@ install_actionlint() { return 1 fi + if ! command -v python3 >/dev/null 2>&1; then + echo "python3 is required to resolve actionlint release metadata" >&2 + return 1 + fi + # Fallback: download latest release binary local tag version os arch url tmp_dir tag="$(python3 - <<'PY' diff --git a/tests/link_dotfiles.bats b/tests/link_dotfiles.bats index 496cfdd..ad20da2 100644 --- a/tests/link_dotfiles.bats +++ b/tests/link_dotfiles.bats @@ -42,6 +42,6 @@ load test_helper run test -L "$HOME/.bashrc" assert_success assert_dir_exist "$HOME/.get-bashed/backup" - run ls "$HOME/.get-bashed/backup" | grep -E '^bashrc\.[0-9]+' + run bash -c 'ls "$1" | grep -E "^bashrc\\.[0-9]+"' _ "$HOME/.get-bashed/backup" assert_success } From ae7879b55e861e6d17d0ee7b3c718e8cbc423a30 Mon Sep 17 00:00:00 2001 From: Jon B Date: Tue, 3 Feb 2026 15:12:12 -0600 Subject: [PATCH 21/33] fix: harden extractor and ssh-agent --- bashrc.d/90-functions.sh | 26 ++++++++++++++------------ bashrc.d/95-ssh-agent.sh | 20 +++++++++++++++++++- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/bashrc.d/90-functions.sh b/bashrc.d/90-functions.sh index 2fb5613..002f4f7 100644 --- a/bashrc.d/90-functions.sh +++ b/bashrc.d/90-functions.sh @@ -8,18 +8,20 @@ ex () { local f="$1" [[ -f "$f" ]] || { echo "'$f' is not a valid file"; return 1; } - case "$f" in - *.tar.bz2) tar xjf "$f" ;; - *.tar.gz) tar xzf "$f" ;; - *.bz2) bunzip2 "$f" ;; - *.rar) unrar x "$f" ;; - *.gz) gunzip "$f" ;; - *.tar) tar xf "$f" ;; - *.tbz2) tar xjf "$f" ;; - *.tgz) tar xzf "$f" ;; - *.zip) unzip "$f" ;; - *.Z) uncompress "$f" ;; - *.7z) 7z x "$f" ;; + local safe_f="$f" + [[ "$safe_f" == -* ]] && safe_f="./$safe_f" + case "$safe_f" in + *.tar.bz2) tar xjf "$safe_f" ;; + *.tar.gz) tar xzf "$safe_f" ;; + *.bz2) bunzip2 "$safe_f" ;; + *.rar) unrar x "$safe_f" ;; + *.gz) gunzip "$safe_f" ;; + *.tar) tar xf "$safe_f" ;; + *.tbz2) tar xjf "$safe_f" ;; + *.tgz) tar xzf "$safe_f" ;; + *.zip) unzip "$safe_f" ;; + *.Z) uncompress "$safe_f" ;; + *.7z) 7z x "$safe_f" ;; *) echo "'$f' cannot be extracted via ex()" ;; esac } diff --git a/bashrc.d/95-ssh-agent.sh b/bashrc.d/95-ssh-agent.sh index 3ea8e4a..c106d15 100644 --- a/bashrc.d/95-ssh-agent.sh +++ b/bashrc.d/95-ssh-agent.sh @@ -6,7 +6,25 @@ # Start SSH agent in interactive TTYs if [[ "${GET_BASHED_SSH_AGENT:-0}" == "1" ]] && [[ -t 1 ]]; then - eval "$(ssh-agent -s)" >/dev/null + _ssh_agent_usable() { + local sock="$1" rc + [[ -S "$sock" ]] || return 1 + SSH_AUTH_SOCK="$sock" SSH_AGENT_PID= ssh-add -l >/dev/null 2>&1 + rc=$? + [[ $rc -eq 0 || $rc -eq 1 ]] + } + + if [[ -n "${SSH_AUTH_SOCK:-}" ]] && _ssh_agent_usable "$SSH_AUTH_SOCK"; then + : + else + SSH_AGENT_SOCK="${HOME}/.ssh/agent.sock" + if _ssh_agent_usable "$SSH_AGENT_SOCK"; then + export SSH_AUTH_SOCK="$SSH_AGENT_SOCK" + else + eval "$(ssh-agent -a "$SSH_AGENT_SOCK" -s)" >/dev/null + fi + fi + [[ -f "$HOME/.ssh/id_rsa" ]] && ssh-add "$HOME/.ssh/id_rsa" 2>/dev/null || true [[ -f "$HOME/.ssh/id_ed25519" ]] && ssh-add "$HOME/.ssh/id_ed25519" 2>/dev/null || true fi From 53668567308d2cdc26cb047590f9cabdd770fcb3 Mon Sep 17 00:00:00 2001 From: Jon B Date: Tue, 3 Feb 2026 15:14:19 -0600 Subject: [PATCH 22/33] docs: expand security and contributing --- CONTRIBUTING.md | 45 +++++++++++++++++++++++++++------------------ README.md | 8 +++++--- SECURITY.md | 35 +++++++++++++++++++++++++++++++---- 3 files changed, 63 insertions(+), 25 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7cb314b..b2f58aa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,31 +1,28 @@ # Contributing -Thanks for helping improve get-bashed. +Thanks for helping improve get-bashed. This repo is intentionally shell-first, portable, and designed to be easy to reason about. -## Architecture +## Project Layout -- `bashrc` / `bash_profile` are the entrypoints. -- `bashrc.d/` is the ordered module system. +- `bashrc` and `bash_profile` are the entrypoints. +- `bashrc.d/` contains ordered runtime modules. - `install.sh` is the installer and config generator. -- `installers/` contains dependency-aware installers. -- `scripts/` holds CI and doc helpers. +- `installers/` holds dependency-aware installers and helpers. +- `scripts/` includes CI and doc helpers. +- `tests/` contains Bats tests and helper libraries. -## Development +## Local Setup ```bash make docs make lint +make test ``` ## Pre-commit -Install hooks: ```bash pre-commit install -``` - -Run all checks: -```bash pre-commit run --all-files ``` @@ -44,17 +41,29 @@ bats tests ## CI -CI uses `scripts/ci-setup.sh` to bootstrap tools into `GET_BASHED_HOME`. +CI bootstraps tools into `GET_BASHED_HOME` via `scripts/ci-setup.sh`. + +## Style Guidelines -## Guidelines +- Prefer POSIX shell in `install.sh` bootstrap and Bash elsewhere. +- Keep modules idempotent and safe to source repeatedly. +- Avoid hardcoding user paths. Use `$HOME` and `$GET_BASHED_HOME`. +- Add shdoc annotations for public functions and new scripts. -- Keep scripts portable and dependency-light. -- Avoid hardcoding user-specific paths. -- Add shdoc annotations for new scripts. +## Pull Requests + +- Keep PRs focused and small where possible. +- Include a clear summary and testing notes. +- Update docs when behavior changes. +- Avoid adding unpinned dependencies or unverified downloads. ## Conventional Commits -PR titles must follow Conventional Commits (e.g., `feat: add installer`). +PR titles must follow Conventional Commits, for example `feat: add installer`. + +## Security + +Security-sensitive changes (installers, PATH, secrets) require extra scrutiny. See `SECURITY.md` for reporting guidance. ## Branch Protection diff --git a/README.md b/README.md index a94c932..6c52799 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ # get-bashed -[![CI](https://github.com/jbcom/get-bashed/actions/workflows/ci.yml/badge.svg)](https://github.com/jbcom/get-bashed/actions/workflows/ci.yml) -[![Docs](https://github.com/jbcom/get-bashed/actions/workflows/docs.yml/badge.svg)](https://github.com/jbcom/get-bashed/actions/workflows/docs.yml) -[![Release](https://github.com/jbcom/get-bashed/actions/workflows/release-please.yml/badge.svg)](https://github.com/jbcom/get-bashed/actions/workflows/release-please.yml) +[![CI](https://img.shields.io/github/actions/workflow/status/jbcom/get-bashed/ci.yml?branch=main)](https://github.com/jbcom/get-bashed/actions/workflows/ci.yml) +[![Docs](https://img.shields.io/github/actions/workflow/status/jbcom/get-bashed/docs.yml?branch=main)](https://github.com/jbcom/get-bashed/actions/workflows/docs.yml) +[![Release](https://img.shields.io/github/v/release/jbcom/get-bashed?display_name=tag&sort=semver)](https://github.com/jbcom/get-bashed/releases) +[![License](https://img.shields.io/github/license/jbcom/get-bashed)](LICENSE) +[![Stars](https://img.shields.io/github/stars/jbcom/get-bashed)](https://github.com/jbcom/get-bashed/stargazers) A modern, modular Bash environment you can install anywhere. get-bashed is designed to be readable, portable, and safe to extend, with a clean installer that supports interactive and non-interactive setups. diff --git a/SECURITY.md b/SECURITY.md index 25c2477..197aa86 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,11 +1,38 @@ # Security Policy +This project prioritizes safe defaults, explicit install paths, and minimal privilege. Please help keep it secure by reporting issues responsibly. + +## Supported Versions + +- Only the latest release receives security updates. +- The `main` branch may include fixes before the next release. + ## Reporting a Vulnerability -Please report security issues privately. +- Use GitHub Security Advisories for private reporting. +- If you cannot use GitHub, open a private discussion with the maintainer via GitHub. +- Do not open a public issue for security reports. -Use GitHub Security Advisories for this repository, or contact the maintainer directly. +## What to Include -## Supported Versions +- A clear description of the issue and impact. +- Steps to reproduce, including relevant commands or configs. +- Affected versions or commit SHAs. +- Any known mitigations or workarounds. + +## Response Expectations + +- Acknowledgement within 3 business days. +- Initial triage within 7 business days. +- Fix and disclosure timeline will be shared after confirmation. + +## Disclosure + +- Coordinated disclosure is expected. +- Please avoid publishing proof-of-concepts until a fix is released. + +## Scope Notes -Only the latest release is supported with security updates. +- `install.sh` and scripts in `installers/` are security-sensitive. +- Any `curl` or `git` installation path must be verified and pinned when feasible. +- Changes that modify PATH, shell startup, or secret handling are in scope. From a8b9ef4a9e3ec7d3c8f45bc4983102f2b6cde083 Mon Sep 17 00:00:00 2001 From: Jon B Date: Tue, 3 Feb 2026 15:28:25 -0600 Subject: [PATCH 23/33] ci: add autofix and polish linting --- .github/workflows/autofix.yml | 32 ++++++++++++ .pre-commit-config.yaml | 2 +- CONTRIBUTING.md | 26 ++++++++++ SECURITY.md | 40 +++++++++++++++ bin/ram_usage | 92 +++++++++++++++++----------------- scripts/gen-docs.sh | 10 ++++ tests/config_output.bats | 12 +++-- tests/install.bats | 11 ++-- tests/link_dotfiles.bats | 28 ++++++----- tests/optional_deps.bats | 3 +- tests/registry_idempotent.bats | 3 +- 11 files changed, 187 insertions(+), 72 deletions(-) create mode 100644 .github/workflows/autofix.yml diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml new file mode 100644 index 0000000..3cd3784 --- /dev/null +++ b/.github/workflows/autofix.yml @@ -0,0 +1,32 @@ +name: PR Autofix +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: write + pull-requests: write + +jobs: + autofix: + if: github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 0 + - name: CI setup (get-bashed) + run: ./scripts/ci-setup.sh "pre_commit,shdoc,actionlint,shellcheck,bashate" + - name: Run pre-commit + run: pre-commit run --all-files || true + - name: Detect changes + id: changed + uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + - name: Commit fixes + if: steps.changed.outputs.any_modified == 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -A + git commit -m "chore: apply pre-commit fixes [skip actions]" || exit 0 + git push diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6034cb1..2af4a85 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,4 +25,4 @@ repos: rev: 2.1.1 hooks: - id: bashate - args: [--ignore=E003,E006] + args: [--ignore, E003,E006] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b2f58aa..2ec2d6f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,6 +50,32 @@ CI bootstraps tools into `GET_BASHED_HOME` via `scripts/ci-setup.sh`. - Avoid hardcoding user paths. Use `$HOME` and `$GET_BASHED_HOME`. - Add shdoc annotations for public functions and new scripts. +## Design Principles + +- Make defaults safe and predictable. +- Keep installers minimal and dependency-light. +- Prefer explicit opt-in for any behavior that modifies user files. + +## Adding a Module + +1. Add a new `bashrc.d/NN-name.sh` file. +2. Keep modules idempotent and avoid side effects on import. +3. Document any new env vars in `TOOLS.md` or `README.md`. +4. Add shdoc annotations if the module exports functions. + +## Adding an Installer + +1. Register tools in `installers/tools.sh`. +2. Use `dependencies` and `optional_dependencies` to express ordering. +3. Avoid unpinned downloads; prefer package managers when possible. +4. If a new tool needs a custom handler, add it to `installers/_helpers.sh`. + +## Docs Expectations + +- Run `./scripts/gen-docs.sh` after changing scripts. +- Ensure shdoc annotations are present for new functions. +- Keep README user-focused and link to docs for deeper details. + ## Pull Requests - Keep PRs focused and small where possible. diff --git a/SECURITY.md b/SECURITY.md index 197aa86..5f7188a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -36,3 +36,43 @@ This project prioritizes safe defaults, explicit install paths, and minimal priv - `install.sh` and scripts in `installers/` are security-sensitive. - Any `curl` or `git` installation path must be verified and pinned when feasible. - Changes that modify PATH, shell startup, or secret handling are in scope. + +## Threat Model (Summary) + +### Assets + +- User secrets in `secrets.d/` and any injected environment variables. +- Shell startup integrity (`bashrc`, `bash_profile`, `bashrc.d/`). +- Installer integrity (`install.sh`, `installers/`, `scripts/ci-setup.sh`). +- User PATH and toolchain selection (asdf/brew/system). + +### Adversaries + +- Supply-chain tampering (compromised GitHub releases, mirrors, or plugins). +- Local adversary modifying `~/.get-bashed` or symlinked dotfiles. +- Malicious PRs introducing unsafe shell behavior. + +### Common Risks + +- Unpinned downloads or unverified `curl`/`git` installers. +- Command injection via untrusted input in shell scripts. +- PATH poisoning via incorrect ordering or untrusted directories. +- Secrets leakage via logs or generated config files. + +### In-Scope Surfaces + +- Installer inputs, profiles, and feature handling. +- Tool registry definitions and dependency ordering. +- Any code touching secrets, PATH, or shell init files. + +### Out of Scope + +- Upstream security issues in third-party tools (report to upstream). +- User-specific misconfiguration outside of get-bashed artifacts. + +## Hardening Expectations + +- Avoid `eval` and unsafe command substitutions. +- Validate all user input that affects execution paths. +- Prefer pinned versions and checksums where feasible. +- Keep idempotency to avoid repeated side effects. diff --git a/bin/ram_usage b/bin/ram_usage index 6d5b013..5cd0d00 100755 --- a/bin/ram_usage +++ b/bin/ram_usage @@ -44,12 +44,12 @@ def format_bytes(bytes, precision=2): """Format bytes to human-readable format.""" if bytes == 0: return "0 B" - + size_names = ["B", "KB", "MB", "GB", "TB"] i = int(math.floor(math.log(bytes, 1024))) p = math.pow(1024, i) s = round(bytes / p, precision) - + return f"{s} {size_names[i]}" def parse_vm_stat(): @@ -57,18 +57,18 @@ def parse_vm_stat(): output = get_command_output(["vm_stat"]) if not output: return {} - + # Extract page size from the first line first_line = output.split('\n')[0] page_size_match = re.search(r'page size of (\d+) bytes', first_line) if not page_size_match: return {} - + page_size = int(page_size_match.group(1)) - + # Parse the rest of the output memory_stats = {} - + # Define the patterns to extract from vm_stat patterns = { 'free': r'Pages free:\s+(\d+)', @@ -79,35 +79,35 @@ def parse_vm_stat(): 'compressed': r'Pages occupied by compressor:\s+(\d+)', 'purgeable': r'Pages purgeable:\s+(\d+)', } - + for key, pattern in patterns.items(): match = re.search(pattern, output) if match: # Convert from page count to bytes pages = int(match.group(1).replace('.', '')) memory_stats[key] = pages * page_size - + # Calculate additional metrics memory_stats['total'] = get_total_ram() - + # Available = free + purgeable + inactive (potentially) memory_stats['available'] = memory_stats.get('free', 0) + memory_stats.get('purgeable', 0) - + # Used = total - available memory_stats['used'] = memory_stats['total'] - memory_stats['available'] - + # Used percentage if memory_stats['total'] > 0: memory_stats['used_percent'] = (memory_stats['used'] / memory_stats['total']) * 100 else: memory_stats['used_percent'] = 0 - + return memory_stats def get_process_memory(): """Get memory usage per process.""" output = get_command_output(["ps", "-eo", "pid,rss,vsz,user,comm"]) - + processes = [] for line in output.strip().split('\n')[1:]: # Skip header parts = line.split(None, 4) @@ -123,18 +123,18 @@ def get_process_memory(): }) except (ValueError, IndexError): continue - + return processes def group_processes_by_app(processes): """Group processes by application and sum their memory usage.""" app_groups = defaultdict(lambda: {'count': 0, 'memory': 0, 'pids': []}) - + for process in processes: # Extract base app name from command command = process['command'] app_name = os.path.basename(command) - + # Normalize app names if 'chrome' in command.lower() or 'google chrome' in command.lower(): app_name = 'Google Chrome' @@ -160,12 +160,12 @@ def group_processes_by_app(processes): app_name = 'System (launchd)' elif 'windowserver' in command.lower(): app_name = 'WindowServer' - + # Add to the group app_groups[app_name]['count'] += 1 app_groups[app_name]['memory'] += process['rss'] app_groups[app_name]['pids'].append(process['pid']) - + return app_groups def print_header(memory_stats): @@ -173,15 +173,15 @@ def print_header(memory_stats): print(f"{Colors.BLUE}{'=' * 60}{Colors.RESET}") print(f"{Colors.BLUE}{Colors.BOLD} MacOS RAM Usage Monitor {Colors.RESET}") print(f"{Colors.BLUE}{'=' * 60}{Colors.RESET}") - + total = format_bytes(memory_stats['total']) used = format_bytes(memory_stats['used']) available = format_bytes(memory_stats['available']) - + print(f"{Colors.GREEN}Total RAM:{Colors.RESET} {total}") print(f"{Colors.GREEN}Used RAM:{Colors.RESET} {used} ({memory_stats['used_percent']:.2f}%)") print(f"{Colors.GREEN}Available RAM:{Colors.RESET} {available}") - + # Memory pressure category if memory_stats['used_percent'] < 70: print(f"{Colors.GREEN}Memory Pressure: Low{Colors.RESET}") @@ -189,14 +189,14 @@ def print_header(memory_stats): print(f"{Colors.YELLOW}Memory Pressure: Medium{Colors.RESET}") else: print(f"{Colors.RED}Memory Pressure: High{Colors.RESET}") - + print(f"{Colors.BLUE}{'=' * 60}{Colors.RESET}") print() def print_memory_breakdown(memory_stats): """Print detailed memory breakdown.""" print(f"{Colors.GREEN}Memory Breakdown:{Colors.RESET}") - + categories = [ ('active', "Active", "Apps currently in use"), ('wired', "Wired", "System/kernel memory"), @@ -205,25 +205,25 @@ def print_memory_breakdown(memory_stats): ('free', "Free", "Immediately available"), ('purgeable', "Purgeable", "Can be reclaimed if needed") ] - + for key, name, description in categories: if key in memory_stats: value = format_bytes(memory_stats[key]) print(f" {name}: {value} ({description})") - + # Calculate unaccounted memory accounted = ( - memory_stats.get('active', 0) + - memory_stats.get('wired', 0) + - memory_stats.get('inactive', 0) + - memory_stats.get('compressed', 0) + + memory_stats.get('active', 0) + + memory_stats.get('wired', 0) + + memory_stats.get('inactive', 0) + + memory_stats.get('compressed', 0) + memory_stats.get('free', 0) ) - + unaccounted = memory_stats['total'] - accounted if unaccounted > 0: print(f" Unaccounted: {format_bytes(unaccounted)} (Memory used by GPU/file cache)") - + print() def print_top_processes(processes, count=10): @@ -231,14 +231,14 @@ def print_top_processes(processes, count=10): # Sort processes by RSS (Resident Set Size) in descending order sorted_processes = sorted(processes, key=lambda p: p['rss'], reverse=True) total_ram = get_total_ram() - + print(f"{Colors.GREEN}Top {count} Processes by RAM Usage:{Colors.RESET}") print(f"{Colors.BLUE}{'PID':<8}{'MEM':<12}{'%MEM':<8}{'USER':<15}{'COMMAND'}{Colors.RESET}") - + for proc in sorted_processes[:count]: mem = format_bytes(proc['rss']) mem_percent = (proc['rss'] / total_ram) * 100 - + # Color code based on memory percentage if mem_percent > 10: color = Colors.RED @@ -246,26 +246,26 @@ def print_top_processes(processes, count=10): color = Colors.YELLOW else: color = '' - + print(f"{color}{proc['pid']:<8}{mem:<12}{mem_percent:.1f}%{' ':<5}{proc['user']:<15}{proc['command']}{Colors.RESET}") - + print() def print_app_groups(app_groups, total_ram): """Print processes grouped by application.""" print(f"{Colors.GREEN}Memory Usage by Application Group:{Colors.RESET}") print(f"{Colors.BLUE}{'APPLICATION':<25}{'PROCESSES':<12}{'MEMORY':<15}{'%TOTAL'}{Colors.RESET}") - + # Sort by memory usage sorted_apps = sorted(app_groups.items(), key=lambda x: x[1]['memory'], reverse=True) - + total_shown_memory = 0 for app_name, data in sorted_apps[:20]: # Show top 20 count = data['count'] memory = data['memory'] memory_percent = (memory / total_ram) * 100 total_shown_memory += memory - + # Color code based on memory percentage if memory_percent > 10: color = Colors.RED @@ -273,9 +273,9 @@ def print_app_groups(app_groups, total_ram): color = Colors.YELLOW else: color = '' - + print(f"{color}{app_name:<25}{count:<12}{format_bytes(memory):<15}{memory_percent:.1f}%{Colors.RESET}") - + print(f"\n{Colors.YELLOW}Total Memory from Top Apps: {format_bytes(total_shown_memory)}{Colors.RESET}") print() @@ -285,26 +285,26 @@ def main(): if not memory_stats: print("Error: Could not get memory statistics") return 1 - + # Get process information processes = get_process_memory() if not processes: print("Error: Could not get process information") return 1 - + # Group processes by application app_groups = group_processes_by_app(processes) - + # Print report print_header(memory_stats) print_memory_breakdown(memory_stats) print_top_processes(processes) print_app_groups(app_groups, memory_stats['total']) - + # Print timestamp now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") print(f"{Colors.BLUE}Report generated: {now}{Colors.RESET}") - + return 0 if __name__ == "__main__": diff --git a/scripts/gen-docs.sh b/scripts/gen-docs.sh index 537bbdd..c6c34a8 100755 --- a/scripts/gen-docs.sh +++ b/scripts/gen-docs.sh @@ -43,8 +43,16 @@ fix_toc_anchors() { mv "$tmp" "$file" } +ensure_eof() { + local file="$1" + if [[ -s "$file" ]] && [[ -n "$(tail -c 1 "$file")" ]]; then + printf "\n" >> "$file" + fi +} + for doc in "$ROOT_DIR/docs/INSTALLER.md" "$ROOT_DIR/docs/INSTALLERS_HELPERS.md" "$ROOT_DIR/docs/INSTALLERS.md"; do fix_toc_anchors "$doc" + ensure_eof "$doc" done # Combine all runtime modules @@ -63,6 +71,7 @@ shopt -u nullglob shdoc < "$TMP_MODULES" > "$ROOT_DIR/docs/MODULES.md" rm -f "$TMP_MODULES" fix_toc_anchors "$ROOT_DIR/docs/MODULES.md" +ensure_eof "$ROOT_DIR/docs/MODULES.md" # Generate index { @@ -75,5 +84,6 @@ fix_toc_anchors "$ROOT_DIR/docs/MODULES.md" echo "- [$base]($base)" done } > "$ROOT_DIR/docs/INDEX.md" +ensure_eof "$ROOT_DIR/docs/INDEX.md" echo "Docs generated under docs/" diff --git a/tests/config_output.bats b/tests/config_output.bats index d05c307..d6de5ad 100644 --- a/tests/config_output.bats +++ b/tests/config_output.bats @@ -6,15 +6,16 @@ load test_helper TMPDIR="$(mktemp -d)" HOME="$TMPDIR/home" mkdir -p "$HOME" + TEST_HOME="$HOME" USER_NAME="Jane Doe" USER_EMAIL="jane@example.com" - HOME="$HOME" bash ./install.sh --auto --name "$USER_NAME" --email "$USER_EMAIL" --prefix "$HOME/.get-bashed" --force + HOME="$TEST_HOME" bash ./install.sh --auto --name "$USER_NAME" --email "$USER_EMAIL" --prefix "$TEST_HOME/.get-bashed" --force - run grep -F "GET_BASHED_USER_NAME=\"${USER_NAME}\"" "$HOME/.get-bashed/get-bashedrc.sh" + run grep -F "GET_BASHED_USER_NAME=\"${USER_NAME}\"" "$TEST_HOME/.get-bashed/get-bashedrc.sh" assert_success - run grep -F "GET_BASHED_USER_EMAIL=\"${USER_EMAIL}\"" "$HOME/.get-bashed/get-bashedrc.sh" + run grep -F "GET_BASHED_USER_EMAIL=\"${USER_EMAIL}\"" "$TEST_HOME/.get-bashed/get-bashedrc.sh" assert_success } @@ -22,9 +23,10 @@ load test_helper TMPDIR="$(mktemp -d)" HOME="$TMPDIR/home" mkdir -p "$HOME" + TEST_HOME="$HOME" - HOME="$HOME" bash ./install.sh --auto --profiles minimal --features gnu_over_bsd --prefix "$HOME/.get-bashed" --force + HOME="$TEST_HOME" bash ./install.sh --auto --profiles minimal --features gnu_over_bsd --prefix "$TEST_HOME/.get-bashed" --force - run grep -F "export GET_BASHED_GNU=1" "$HOME/.get-bashed/get-bashedrc.sh" + run grep -F "export GET_BASHED_GNU=1" "$TEST_HOME/.get-bashed/get-bashedrc.sh" assert_success } diff --git a/tests/install.bats b/tests/install.bats index a250db8..b2e5304 100755 --- a/tests/install.bats +++ b/tests/install.bats @@ -4,14 +4,15 @@ load test_helper @test "installer writes to prefix and wires bashrc" { TMPDIR="$(mktemp -d)" - HOME="$TMPDIR" bash ./install.sh --prefix "$TMPDIR/.get-bashed" --force + TEST_HOME="$TMPDIR" + HOME="$TEST_HOME" bash ./install.sh --prefix "$TEST_HOME/.get-bashed" --force - assert_file_exist "$TMPDIR/.get-bashed/bashrc" - assert_dir_exist "$TMPDIR/.get-bashed/bashrc.d" + assert_file_exist "$TEST_HOME/.get-bashed/bashrc" + assert_dir_exist "$TEST_HOME/.get-bashed/bashrc.d" - run grep -F "# get-bashed: source modular bashrc" "$TMPDIR/.bashrc" + run grep -F "# get-bashed: source modular bashrc" "$TEST_HOME/.bashrc" assert_success - run grep -F "# get-bashed: source login bash_profile" "$TMPDIR/.bash_profile" + run grep -F "# get-bashed: source login bash_profile" "$TEST_HOME/.bash_profile" assert_success } diff --git a/tests/link_dotfiles.bats b/tests/link_dotfiles.bats index ad20da2..5632533 100644 --- a/tests/link_dotfiles.bats +++ b/tests/link_dotfiles.bats @@ -6,28 +6,29 @@ load test_helper TMPDIR="$(mktemp -d)" HOME="$TMPDIR/home" mkdir -p "$HOME" + TEST_HOME="$HOME" USER_NAME="Jane Doe" USER_EMAIL="jane@example.com" - HOME="$HOME" bash ./install.sh --auto --link-dotfiles --name "$USER_NAME" --email "$USER_EMAIL" --prefix "$HOME/.get-bashed" --force + HOME="$TEST_HOME" bash ./install.sh --auto --link-dotfiles --name "$USER_NAME" --email "$USER_EMAIL" --prefix "$TEST_HOME/.get-bashed" --force - run test -L "$HOME/.bashrc" + run test -L "$TEST_HOME/.bashrc" assert_success - run test -L "$HOME/.bash_profile" + run test -L "$TEST_HOME/.bash_profile" assert_success - run test -L "$HOME/.inputrc" + run test -L "$TEST_HOME/.inputrc" assert_success - run test -L "$HOME/.bash_aliases" + run test -L "$TEST_HOME/.bash_aliases" assert_success - run test -L "$HOME/.vimrc" + run test -L "$TEST_HOME/.vimrc" assert_success - run test -L "$HOME/.gitconfig" + run test -L "$TEST_HOME/.gitconfig" assert_success - run grep -F "name = ${USER_NAME}" "$HOME/.get-bashed/gitconfig" + run grep -F "name = ${USER_NAME}" "$TEST_HOME/.get-bashed/gitconfig" assert_success - run grep -F "email = ${USER_EMAIL}" "$HOME/.get-bashed/gitconfig" + run grep -F "email = ${USER_EMAIL}" "$TEST_HOME/.get-bashed/gitconfig" assert_success } @@ -35,13 +36,14 @@ load test_helper TMPDIR="$(mktemp -d)" HOME="$TMPDIR/home" mkdir -p "$HOME" + TEST_HOME="$HOME" echo "legacy" > "$HOME/.bashrc" - HOME="$HOME" bash ./install.sh --auto --link-dotfiles --prefix "$HOME/.get-bashed" --force + HOME="$TEST_HOME" bash ./install.sh --auto --link-dotfiles --prefix "$TEST_HOME/.get-bashed" --force - run test -L "$HOME/.bashrc" + run test -L "$TEST_HOME/.bashrc" assert_success - assert_dir_exist "$HOME/.get-bashed/backup" - run bash -c 'ls "$1" | grep -E "^bashrc\\.[0-9]+"' _ "$HOME/.get-bashed/backup" + assert_dir_exist "$TEST_HOME/.get-bashed/backup" + run bash -c 'ls "$1" | grep -E "^bashrc\\.[0-9]+"' _ "$TEST_HOME/.get-bashed/backup" assert_success } diff --git a/tests/optional_deps.bats b/tests/optional_deps.bats index 3b26256..8cc326d 100644 --- a/tests/optional_deps.bats +++ b/tests/optional_deps.bats @@ -6,8 +6,9 @@ load test_helper TMPDIR="$(mktemp -d)" HOME="$TMPDIR/home" mkdir -p "$HOME" + TEST_HOME="$HOME" - HOME="$HOME" bash ./install.sh --auto --prefix "$HOME/.get-bashed" --force --features git_signing --install git --dry-run > "$TMPDIR/out" + HOME="$TEST_HOME" bash ./install.sh --auto --prefix "$TEST_HOME/.get-bashed" --force --features git_signing --install git --dry-run > "$TMPDIR/out" run grep -F "would install: gnupg" "$TMPDIR/out" assert_success diff --git a/tests/registry_idempotent.bats b/tests/registry_idempotent.bats index 761e6a7..666f439 100644 --- a/tests/registry_idempotent.bats +++ b/tests/registry_idempotent.bats @@ -6,8 +6,9 @@ load test_helper TMPDIR="$(mktemp -d)" HOME="$TMPDIR/home" mkdir -p "$HOME" + TEST_HOME="$HOME" - HOME="$HOME" bash ./install.sh --auto --list-installers > "$TMPDIR/list" + HOME="$TEST_HOME" bash ./install.sh --auto --list-installers > "$TMPDIR/list" run grep -F " - git" "$TMPDIR/list" assert_success From ba0365cba84c9bba8aaf684720c80faa45cf4f0d Mon Sep 17 00:00:00 2001 From: Jon B Date: Tue, 3 Feb 2026 15:49:15 -0600 Subject: [PATCH 24/33] refactor: split POSIX bootstrap and bash installer --- install.bash | 635 ++++++++++++++++++++++++++++++++++++ install.sh | 725 +++-------------------------------------- installers/_helpers.sh | 45 ++- installers/tools.sh | 18 + scripts/gen-docs.sh | 2 +- 5 files changed, 730 insertions(+), 695 deletions(-) create mode 100755 install.bash diff --git a/install.bash b/install.bash new file mode 100755 index 0000000..91b8218 --- /dev/null +++ b/install.bash @@ -0,0 +1,635 @@ +#!/usr/bin/env bash +# @file install +# @name get-bashed-installer +# @brief Installer and configurator for get-bashed. +# @description +# Supports non-interactive and interactive installation with profiles, +# feature flags, and installer bundles. + +# shellcheck disable=SC3040 +set -euo pipefail + +if [[ "${BASH_VERSINFO[0]}" -lt 4 ]]; then + echo "Bash 4+ is required. Install a newer bash and re-run." >&2 + exit 1 +fi + +# @description Print usage help. +# @noargs +usage() { + cat <<'USAGE' +Usage: install.sh [--prefix PATH] [--force] [--with-ui] + [--auto] [--yes] + [--profiles minimal|dev|ops[,..]] + [--features gnu_over_bsd,build_flags,...] + [--install brew,asdf,doppler,...] + [--vimrc-mode awesome|basic] + [--link-dotfiles] + [--name "Full Name"] [--email "me@example.com"] + [--list] [--list-profiles] [--list-features] [--list-installers] + [--dry-run] + +Notes: +- --auto disables prompts. +- --yes auto-accepts prompts. +- profiles set defaults; features override defaults. +USAGE +} + +REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$REPO_DIR/installers/_helpers.sh" +source "$REPO_DIR/installers/tools.sh" +PREFIX="${GET_BASHED_HOME:-$HOME/.get-bashed}" +FORCE=0 +WITH_UI=0 +AUTO=0 +YES=0 +PROFILES="" +FEATURES="" +INSTALLS="" +LIST=0 +DRY_RUN=0 +LIST_PROFILES=0 +LIST_FEATURES=0 +LIST_INSTALLERS=0 +GROUP_INSTALLS="" +VIMRC_MODE="awesome" +LINK_DOTFILES=0 +USER_NAME="" +USER_EMAIL="" + +# Feature flags (defaults) +GET_BASHED_GNU=0 +GET_BASHED_BUILD_FLAGS=0 +GET_BASHED_AUTO_TOOLS=0 +GET_BASHED_SSH_AGENT=0 +GET_BASHED_USE_DOPPLER=0 +GET_BASHED_USE_BASH_IT=0 +GET_BASHED_GIT_SIGNING=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --prefix) + if [[ $# -lt 2 ]]; then + echo "Error: --prefix requires a value" >&2 + usage + exit 1 + fi + PREFIX="$2"; shift 2 ;; + --force) + FORCE=1; shift ;; + --with-ui) + WITH_UI=1; shift ;; + --auto|-a) + AUTO=1; shift ;; + --yes|-y) + YES=1; shift ;; + --profiles|-w) + if [[ $# -lt 2 ]]; then + echo "Error: --profiles requires a value" >&2 + usage + exit 1 + fi + PROFILES="$2"; shift 2 ;; + --features) + if [[ $# -lt 2 ]]; then + echo "Error: --features requires a value" >&2 + usage + exit 1 + fi + FEATURES="$2"; shift 2 ;; + --install|-i) + if [[ $# -lt 2 ]]; then + echo "Error: --install requires a value" >&2 + usage + exit 1 + fi + INSTALLS="$2"; shift 2 ;; + --vimrc-mode) + if [[ $# -lt 2 ]]; then + echo "Error: --vimrc-mode requires a value" >&2 + usage + exit 1 + fi + VIMRC_MODE="$2"; shift 2 ;; + --link-dotfiles) + LINK_DOTFILES=1; shift ;; + --name|-n) + if [[ $# -lt 2 ]]; then + echo "Error: --name requires a value" >&2 + usage + exit 1 + fi + USER_NAME="$2"; shift 2 ;; + --email|-e) + if [[ $# -lt 2 ]]; then + echo "Error: --email requires a value" >&2 + usage + exit 1 + fi + USER_EMAIL="$2"; shift 2 ;; + --list) + LIST=1; shift ;; + --list-profiles) + LIST_PROFILES=1; shift ;; + --list-features) + LIST_FEATURES=1; shift ;; + --list-installers) + LIST_INSTALLERS=1; shift ;; + --dry-run) + DRY_RUN=1; shift ;; + -h|--help) + usage; exit 0 ;; + *) + echo "Unknown argument: $1"; usage; exit 1 ;; + esac +done + +if [[ "$YES" -eq 1 || "$AUTO" -eq 1 ]]; then + export GET_BASHED_AUTO_APPROVE=1 +fi + +# @description Apply a built-in profile. +# @arg $1 string Profile name. +# @exitcode 0 If applied. +# @exitcode 1 If unknown. +apply_profile() { + local p="$1" + case "$p" in + minimal) + GET_BASHED_GNU=0 + GET_BASHED_BUILD_FLAGS=0 + GET_BASHED_AUTO_TOOLS=0 + GET_BASHED_SSH_AGENT=0 + GET_BASHED_USE_DOPPLER=0 + ;; + dev) + GET_BASHED_GNU=1 + GET_BASHED_BUILD_FLAGS=1 + GET_BASHED_AUTO_TOOLS=1 + GET_BASHED_SSH_AGENT=0 + GET_BASHED_USE_DOPPLER=0 + ;; + ops) + GET_BASHED_GNU=1 + GET_BASHED_BUILD_FLAGS=1 + GET_BASHED_AUTO_TOOLS=1 + GET_BASHED_SSH_AGENT=1 + GET_BASHED_USE_DOPPLER=1 + ;; + *) + return 1 + ;; + esac +} + +# @description Apply a feature toggle. +# @arg $1 string Feature name (supports no- prefix). +# @exitcode 0 If applied. +# @exitcode 1 If unknown. +apply_feature() { + local f="$1" v=1 + if [[ "$f" == no-* ]]; then + v=0 + f="${f#no-}" + fi + case "$f" in + gnu_over_bsd) GET_BASHED_GNU=$v ;; + build_flags) GET_BASHED_BUILD_FLAGS=$v ;; + auto_tools) GET_BASHED_AUTO_TOOLS=$v ;; + ssh_agent) GET_BASHED_SSH_AGENT=$v ;; + doppler_env) GET_BASHED_USE_DOPPLER=$v ;; + bash_it) GET_BASHED_USE_BASH_IT=$v ;; + git_signing) GET_BASHED_GIT_SIGNING=$v ;; + dev_tools) GROUP_INSTALLS="${GROUP_INSTALLS},rg,fd,bat,fzf,jq,yq,tree,direnv,starship,nodejs,python,bash" ;; + ops_tools) GROUP_INSTALLS="${GROUP_INSTALLS},gh,git_lfs,terraform,awscli,kubectl,helm,stern,doppler,nodejs,python,java,bash" ;; + *) return 1 ;; + esac +} + +# @description Split a comma-delimited list into space-delimited output. +# @arg $1 string Comma list. +# @stdout Space-delimited items. +split_csv() { + local s="$1"; IFS=',' read -r -a _parts <<<"$s"; echo "${_parts[@]}"; +} + +# @internal +is_valid_profile() { + case "$1" in + minimal|dev|ops) return 0 ;; + *) return 1 ;; + esac +} + +# @internal +apply_gitconfig() { + local cfg="$PREFIX/gitconfig" + [[ -r "$cfg" ]] || return 0 + + if [[ -n "$USER_NAME" ]]; then + git config -f "$cfg" user.name "$USER_NAME" + fi + if [[ -n "$USER_EMAIL" ]]; then + git config -f "$cfg" user.email "$USER_EMAIL" + fi +} + +# @internal +ensure_block() { + local file="$1" marker="$2" snippet="$3" + mkdir -p "$(dirname "$file")" + if [[ -r "$file" ]]; then + if grep -Fq "$marker" "$file"; then + return 0 + fi + fi + { + echo "" + echo "$marker" + echo "$snippet" + } >> "$file" +} + +# @internal +backup_file() { + local file="$1" + [[ -e "$file" ]] || return 0 + local backup_dir="$PREFIX/backup" + mkdir -p "$backup_dir" + local base + base="$(basename "$file")" + local ts + ts="$(date +%s)" + mv "$file" "$backup_dir/${base}.${ts}" +} + +# @internal +link_dotfile() { + local name="$1" + local src="$PREFIX/$name" + local dest="$HOME/.${name}" + if [[ ! -e "$src" ]]; then + return 0 + fi + if [[ -L "$dest" ]]; then + return 0 + fi + if [[ -e "$dest" ]]; then + backup_file "$dest" + fi + ln -s "$src" "$dest" +} + +# @internal +install_dialog() { + if command -v dialog >/dev/null 2>&1; then + return 0 + fi + if command -v brew >/dev/null 2>&1; then + brew install dialog + elif command -v apt-get >/dev/null 2>&1; then + apt_install dialog + elif command -v dnf >/dev/null 2>&1; then + dnf_install dialog + elif command -v yum >/dev/null 2>&1; then + yum_install dialog + fi +} + +# @internal +prompt_yes_no() { + local label="$1" answer + if [[ "$YES" -eq 1 ]]; then + return 0 + fi + read -r -p "$label [y/N]: " answer + [[ "$answer" =~ ^[Yy]$ ]] +} + +if [[ "$WITH_UI" -eq 1 ]] && [[ "$AUTO" -eq 0 ]]; then + install_dialog || true +fi + +# If stdin isn't a TTY, default to non-interactive. +if [[ ! -t 0 ]] && [[ "$AUTO" -eq 0 ]]; then + AUTO=1 +fi + +# Load installer registry early for interactive UI. +load_installers + +# Preserve CLI features/installers so profiles do not clobber them. +CLI_FEATURES="${FEATURES:-}" +CLI_INSTALLS="${INSTALLS:-}" +FEATURES="" +INSTALLS="" + +# Apply profiles first +if [[ -n "$PROFILES" ]]; then + for p in $(split_csv "$PROFILES"); do + if ! is_valid_profile "$p"; then + echo "Invalid profile name: $p" >&2 + exit 1 + fi + # Load profile file if present + PROFILE_FILE="$REPO_DIR/profiles/${p}.env" + if [[ -r "$PROFILE_FILE" ]]; then + # shellcheck disable=SC1090 + source "$PROFILE_FILE" + PROFILE_FEATURES="${FEATURES:-}" + if [[ -n "$PROFILE_FEATURES" ]]; then + for f in $(split_csv "$PROFILE_FEATURES"); do + apply_feature "$f" || { echo "Unknown feature: $f"; exit 1; } + done + fi + if [[ -n "${INSTALLS:-}" ]]; then + GROUP_INSTALLS="${GROUP_INSTALLS},${INSTALLS}" + fi + FEATURES="" + INSTALLS="" + else + apply_profile "$p" || { echo "Unknown profile: $p"; exit 1; } + fi + done +fi + +FEATURES="$CLI_FEATURES" +INSTALLS="$CLI_INSTALLS" + +# Apply features overrides +if [[ -n "$FEATURES" ]]; then + for f in $(split_csv "$FEATURES"); do + apply_feature "$f" || { echo "Unknown feature: $f"; exit 1; } + done +fi + +# Interactive selection +if [[ "$AUTO" -eq 0 ]]; then + if [[ "$WITH_UI" -eq 1 ]] && command -v dialog >/dev/null 2>&1; then + if [[ "$YES" -ne 1 ]]; then + PROFILE_CHOICE=$(dialog --clear --title "get-bashed" --menu "Select a profile" 12 60 3 \ + minimal "Minimal defaults" \ + dev "Developer workstation" \ + ops "Ops/Platform workstation" \ + 3>&1 1>&2 2>&3) || true + if [[ -n "$PROFILE_CHOICE" ]]; then + apply_profile "$PROFILE_CHOICE" + fi + + CHOICES=$(dialog --clear --title "get-bashed" --checklist "Enable features" 18 70 8 \ + gnu_over_bsd "Prefer GNU tools on macOS" "$( [[ "$GET_BASHED_GNU" -eq 1 ]] && echo on || echo off )" \ + build_flags "Enable runtime build flags" "$( [[ "$GET_BASHED_BUILD_FLAGS" -eq 1 ]] && echo on || echo off )" \ + auto_tools "Auto-install optional tools" "$( [[ "$GET_BASHED_AUTO_TOOLS" -eq 1 ]] && echo on || echo off )" \ + ssh_agent "Auto-start ssh-agent" "$( [[ "$GET_BASHED_SSH_AGENT" -eq 1 ]] && echo on || echo off )" \ + doppler_env "Enable Doppler env usage" "$( [[ "$GET_BASHED_USE_DOPPLER" -eq 1 ]] && echo on || echo off )" \ + bash_it "Enable bash-it (if installed)" "$( [[ "$GET_BASHED_USE_BASH_IT" -eq 1 ]] && echo on || echo off )" \ + git_signing "Enable git signing (gnupg)" "$( [[ "$GET_BASHED_GIT_SIGNING" -eq 1 ]] && echo on || echo off )" \ + dev_tools "Developer tool bundle" off \ + ops_tools "Ops tool bundle" off \ + 3>&1 1>&2 2>&3) || true + + GET_BASHED_GNU=0 + GET_BASHED_BUILD_FLAGS=0 + GET_BASHED_AUTO_TOOLS=0 + GET_BASHED_SSH_AGENT=0 + GET_BASHED_USE_DOPPLER=0 + GET_BASHED_USE_BASH_IT=0 + GET_BASHED_GIT_SIGNING=0 + + for choice in $CHOICES; do + apply_feature "${choice//\"/}" || true + done + + dialog_opts=() + for id in $INSTALLERS; do + desc_var="INSTALL_DESC_${id}" + desc="${!desc_var}" + [[ -z "$desc" ]] && desc="$id" + default_state="off" + if [[ "$id" == "dialog" ]]; then + default_state="on" + fi + dialog_opts+=("$id" "$desc" "$default_state") + done + + INSTALLS_DIALOG=$(dialog --clear --title "get-bashed" --checklist "Select installers" 20 80 12 \ + "${dialog_opts[@]}" \ + 3>&1 1>&2 2>&3) || true + if [[ -n "$INSTALLS_DIALOG" ]]; then + INSTALLS="${INSTALLS_DIALOG//\"/}" + INSTALLS="${INSTALLS// /,}" + fi + + if [[ -z "$USER_NAME" ]]; then + USER_NAME=$(dialog --clear --title "get-bashed" --inputbox "Git user.name" 8 60 "${USER_NAME}" 3>&1 1>&2 2>&3) || true + fi + if [[ -z "$USER_EMAIL" ]]; then + USER_EMAIL=$(dialog --clear --title "get-bashed" --inputbox "Git user.email" 8 60 "${USER_EMAIL}" 3>&1 1>&2 2>&3) || true + fi + fi + else + if [[ "$YES" -eq 0 ]]; then + prompt_yes_no "Proceed with installation?" || exit 1 + fi + if prompt_yes_no "Enable GNU tools on macOS (gnu_over_bsd)?"; then GET_BASHED_GNU=1; fi + if prompt_yes_no "Enable build flags (build_flags)?"; then GET_BASHED_BUILD_FLAGS=1; fi + if prompt_yes_no "Enable auto tools (auto_tools)?"; then GET_BASHED_AUTO_TOOLS=1; fi + if prompt_yes_no "Enable ssh-agent (ssh_agent)?"; then GET_BASHED_SSH_AGENT=1; fi + if prompt_yes_no "Enable doppler env (doppler_env)?"; then GET_BASHED_USE_DOPPLER=1; fi + if prompt_yes_no "Enable bash-it (bash_it)?"; then GET_BASHED_USE_BASH_IT=1; fi + if prompt_yes_no "Enable git signing (git_signing)?"; then GET_BASHED_GIT_SIGNING=1; fi + fi +fi + +if [[ "$LIST" -eq 1 ]]; then + echo "Profiles: minimal, dev, ops" + echo "Features:" + echo " gnu_over_bsd" + echo " build_flags" + echo " auto_tools" + echo " ssh_agent" + echo " doppler_env" + echo " bash_it" + echo " git_signing" + echo " dev_tools" + echo " ops_tools" + echo "Installers:" + for id in $INSTALLERS; do + echo " $id" + done + exit 0 +fi + +if [[ "$LIST_PROFILES" -eq 1 ]]; then + echo "minimal" + echo "dev" + echo "ops" + exit 0 +fi + +if [[ "$LIST_FEATURES" -eq 1 ]]; then + echo "gnu_over_bsd" + echo "build_flags" + echo "auto_tools" + echo "ssh_agent" + echo "doppler_env" + echo "bash_it" + echo "git_signing" + echo "dev_tools" + echo "ops_tools" + exit 0 +fi + +if [[ "$LIST_INSTALLERS" -eq 1 ]]; then + for id in $INSTALLERS; do + desc_var="INSTALL_DESC_${id}" + desc="${!desc_var}" + [[ -z "$desc" ]] && desc="$id" + echo " - $id ($desc)" + done + exit 0 +fi + +if [[ "$DRY_RUN" -eq 1 ]]; then + echo "Dry run enabled. No changes will be made." + echo " Prefix: $PREFIX" + echo " Profiles: ${PROFILES:-}" + echo " Features: gnu_over_bsd=${GET_BASHED_GNU} build_flags=${GET_BASHED_BUILD_FLAGS} auto_tools=${GET_BASHED_AUTO_TOOLS} ssh_agent=${GET_BASHED_SSH_AGENT} doppler_env=${GET_BASHED_USE_DOPPLER} bash_it=${GET_BASHED_USE_BASH_IT} git_signing=${GET_BASHED_GIT_SIGNING}" + echo " Installers: ${INSTALLS:-}" +fi + +mkdir -p "$PREFIX" +export GET_BASHED_HOME="$PREFIX" + +copy_tree() { + local src="$1" dest="$2" + mkdir -p "$dest" + rsync -a --delete "$src"/ "$dest"/ +} + +# Copy base assets +copy_tree "$REPO_DIR/bashrc.d" "$PREFIX/bashrc.d" +cp -f "$REPO_DIR/bashrc" "$PREFIX/bashrc" +cp -f "$REPO_DIR/bash_profile" "$PREFIX/bash_profile" +cp -f "$REPO_DIR/bash_aliases" "$PREFIX/bash_aliases" +cp -f "$REPO_DIR/inputrc" "$PREFIX/inputrc" +cp -f "$REPO_DIR/vimrc" "$PREFIX/vimrc" +cp -f "$REPO_DIR/gitconfig" "$PREFIX/gitconfig" + +# secrets.d bootstrap (only inside GET_BASHED_HOME) +mkdir -p "$PREFIX/secrets.d" +if [[ ! -e "$PREFIX/secrets.d/00-local.sh" ]]; then + cat <<'__SECRETS__' > "$PREFIX/secrets.d/00-local.sh" +# Place local secrets here. Example: +# export FOO="bar" +__SECRETS__ +fi + +# Write config file +{ + echo "# Generated by get-bashed installer" + echo "export GET_BASHED_GNU=${GET_BASHED_GNU}" + echo "export GET_BASHED_BUILD_FLAGS=${GET_BASHED_BUILD_FLAGS}" + echo "export GET_BASHED_AUTO_TOOLS=${GET_BASHED_AUTO_TOOLS}" + echo "export GET_BASHED_SSH_AGENT=${GET_BASHED_SSH_AGENT}" + echo "export GET_BASHED_USE_DOPPLER=${GET_BASHED_USE_DOPPLER}" + echo "export GET_BASHED_USE_BASH_IT=${GET_BASHED_USE_BASH_IT}" + echo "export GET_BASHED_GIT_SIGNING=${GET_BASHED_GIT_SIGNING}" + if [[ -n "$USER_NAME" ]]; then + echo "export GET_BASHED_USER_NAME=\"${USER_NAME}\"" + fi + if [[ -n "$USER_EMAIL" ]]; then + echo "export GET_BASHED_USER_EMAIL=\"${USER_EMAIL}\"" + fi +} > "$PREFIX/get-bashedrc.sh" + +apply_gitconfig + +# Link dotfiles if requested (into $HOME only) +if [[ "$LINK_DOTFILES" -eq 1 ]]; then + link_dotfile "bashrc" + link_dotfile "bash_profile" + link_dotfile "inputrc" + link_dotfile "bash_aliases" + link_dotfile "vimrc" + if [[ -n "$USER_NAME" && -n "$USER_EMAIL" ]]; then + link_dotfile "gitconfig" + else + echo "Skipping gitconfig link (missing --name/--email)." >&2 + fi +fi + +# Update login shell snippets (idempotent) +BASHRC_LINE="# get-bashed: source modular bashrc" +BASHRC_SNIP='if [[ -r "$HOME/.get-bashed/bashrc" ]]; then source "$HOME/.get-bashed/bashrc"; fi' +BASH_PROFILE_LINE="# get-bashed: source login bash_profile" +BASH_PROFILE_SNIP='if [[ -r "$HOME/.get-bashed/bash_profile" ]]; then source "$HOME/.get-bashed/bash_profile"; fi' + +ensure_block "$HOME/.bashrc" "$BASHRC_LINE" "$BASHRC_SNIP" +ensure_block "$HOME/.bash_profile" "$BASH_PROFILE_LINE" "$BASH_PROFILE_SNIP" + +# Installers +if [[ -n "$INSTALLS" ]]; then + INSTALLS="${INSTALLS},${GROUP_INSTALLS#,}" +fi + +declare -A INSTALL_IN_PROGRESS=() +declare -A INSTALL_DONE=() + +get_deps() { + local id="$1" + echo "${TOOL_DEPS[$id]:-}" +} + +is_done() { + local id="$1" + [[ "${INSTALL_DONE[$id]:-}" == "1" ]] +} + +mark_done() { + local id="$1" + INSTALL_DONE["$id"]=1 +} + +run_install() { + local id="$1" + if is_done "$id"; then + return 0 + fi + if [[ "${INSTALL_IN_PROGRESS[$id]:-}" == "1" ]]; then + echo "Circular dependency detected while installing $id" >&2 + return 1 + fi + INSTALL_IN_PROGRESS["$id"]=1 + local deps + deps="$(get_deps "$id")" + if [[ -n "$deps" ]]; then + for dep in $(split_csv "$deps"); do + run_install "$dep" || return 1 + done + fi + if [[ "$DRY_RUN" -eq 1 ]]; then + echo "would install: $id" + else + if declare -f "install_${id}" >/dev/null 2>&1; then + "install_${id}" + else + install_tool "$id" + fi + fi + unset INSTALL_IN_PROGRESS["$id"] + mark_done "$id" +} + +if [[ -n "$INSTALLS" ]]; then + for id in $(split_csv "$INSTALLS"); do + run_install "$id" + done +fi + +if [[ "$DRY_RUN" -eq 1 ]]; then + exit 0 +fi + +echo "get-bashed installed to $PREFIX" diff --git a/install.sh b/install.sh index 2215cca..00e1fb3 100755 --- a/install.sh +++ b/install.sh @@ -1,711 +1,64 @@ #!/bin/sh -# POSIX shell bootstrap that re-execs with bash for full functionality. -if [ -z "${GET_BASHED_BOOTSTRAPPED:-}" ]; then - if command -v bash >/dev/null 2>&1; then - GET_BASHED_BOOTSTRAPPED=1 exec bash "$0" "$@" - fi - echo "Bash is required to run this installer." >&2 - echo "Install bash (recommended latest) and re-run: sh install.sh" >&2 - exit 1 -fi - -# shellcheck shell=bash -# @file install -# @name get-bashed-installer -# @brief Installer and configurator for get-bashed. -# @description -# Supports non-interactive and interactive installation with profiles, -# feature flags, and installer bundles. - -# shellcheck disable=SC3040 -set -euo pipefail - -# @description Print usage help. -# @noargs -usage() { - cat <<'USAGE' -Usage: install.sh [--prefix PATH] [--force] [--with-ui] - [--auto] [--yes] - [--profiles minimal|dev|ops[,..]] - [--features gnu_over_bsd,build_flags,...] - [--install brew,asdf,doppler,...] - [--vimrc-mode awesome|basic] - [--link-dotfiles] - [--name "Full Name"] [--email "me@example.com"] - [--list] [--list-profiles] [--list-features] [--list-installers] - [--dry-run] +# Minimal POSIX bootstrap. Installs/locates bash and hands off to the bash installer. -Notes: -- --auto disables prompts. -- --yes auto-accepts prompts. -- profiles set defaults; features override defaults. -USAGE -} - -REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$REPO_DIR/installers/_helpers.sh" -source "$REPO_DIR/installers/tools.sh" -PREFIX="${GET_BASHED_HOME:-$HOME/.get-bashed}" -FORCE=0 -WITH_UI=0 -AUTO=0 -YES=0 -PROFILES="" -FEATURES="" -INSTALLS="" -LIST=0 -DRY_RUN=0 -LIST_PROFILES=0 -LIST_FEATURES=0 -LIST_INSTALLERS=0 -GROUP_INSTALLS="" -VIMRC_MODE="awesome" -LINK_DOTFILES=0 -USER_NAME="" -USER_EMAIL="" - -# Feature flags (defaults) -GET_BASHED_GNU=0 -GET_BASHED_BUILD_FLAGS=0 -GET_BASHED_AUTO_TOOLS=0 -GET_BASHED_SSH_AGENT=0 -GET_BASHED_USE_DOPPLER=0 -GET_BASHED_USE_BASH_IT=0 -GET_BASHED_GIT_SIGNING=0 - -while [[ $# -gt 0 ]]; do - case "$1" in - --prefix) - if [[ $# -lt 2 ]]; then - echo "Error: --prefix requires a value" >&2 - usage - exit 1 - fi - PREFIX="$2"; shift 2 ;; - --force) - FORCE=1; shift ;; - --with-ui) - WITH_UI=1; shift ;; - --auto|-a) - AUTO=1; shift ;; - --yes|-y) - YES=1; shift ;; - --profiles|-w) - if [[ $# -lt 2 ]]; then - echo "Error: --profiles requires a value" >&2 - usage - exit 1 - fi - PROFILES="$2"; shift 2 ;; - --features) - if [[ $# -lt 2 ]]; then - echo "Error: --features requires a value" >&2 - usage - exit 1 - fi - FEATURES="$2"; shift 2 ;; - --install|-i) - if [[ $# -lt 2 ]]; then - echo "Error: --install requires a value" >&2 - usage - exit 1 - fi - INSTALLS="$2"; shift 2 ;; - --vimrc-mode) - if [[ $# -lt 2 ]]; then - echo "Error: --vimrc-mode requires a value" >&2 - usage - exit 1 - fi - VIMRC_MODE="$2"; shift 2 ;; - --link-dotfiles) - LINK_DOTFILES=1; shift ;; - --name|-n) - if [[ $# -lt 2 ]]; then - echo "Error: --name requires a value" >&2 - usage - exit 1 - fi - USER_NAME="$2"; shift 2 ;; - --email|-e) - if [[ $# -lt 2 ]]; then - echo "Error: --email requires a value" >&2 - usage - exit 1 - fi - USER_EMAIL="$2"; shift 2 ;; - --list) - LIST=1; shift ;; - --list-profiles) - LIST_PROFILES=1; shift ;; - --list-features) - LIST_FEATURES=1; shift ;; - --list-installers) - LIST_INSTALLERS=1; shift ;; - --dry-run) - DRY_RUN=1; shift ;; - -h|--help) - usage; exit 0 ;; - *) - echo "Unknown argument: $1"; usage; exit 1 ;; - esac -done +set -eu -if [[ "$YES" -eq 1 || "$AUTO" -eq 1 ]]; then - export GET_BASHED_AUTO_APPROVE=1 -fi - -# @description Apply a built-in profile. -# @arg $1 string Profile name. -# @exitcode 0 If applied. -# @exitcode 1 If unknown. -apply_profile() { - local p="$1" - case "$p" in - minimal) - GET_BASHED_GNU=0 - GET_BASHED_BUILD_FLAGS=0 - GET_BASHED_AUTO_TOOLS=0 - GET_BASHED_SSH_AGENT=0 - GET_BASHED_USE_DOPPLER=0 - ;; - dev) - GET_BASHED_GNU=1 - GET_BASHED_BUILD_FLAGS=1 - GET_BASHED_AUTO_TOOLS=1 - GET_BASHED_SSH_AGENT=0 - GET_BASHED_USE_DOPPLER=0 - ;; - ops) - GET_BASHED_GNU=1 - GET_BASHED_BUILD_FLAGS=1 - GET_BASHED_AUTO_TOOLS=1 - GET_BASHED_SSH_AGENT=1 - GET_BASHED_USE_DOPPLER=1 - ;; - *) - return 1 - ;; - esac +fail() { + printf '%s\n' "$*" >&2 + exit 1 } -# @description Apply a feature toggle. -# @arg $1 string Feature name (supports no- prefix). -# @exitcode 0 If applied. -# @exitcode 1 If unknown. -apply_feature() { - local f="$1" v=1 - if [[ "$f" == no-* ]]; then - v=0 - f="${f#no-}" +ensure_bash() { + brew_bin="" + if command -v brew >/dev/null 2>&1; then + brew_bin="$(command -v brew)" + elif [ -x "/opt/homebrew/bin/brew" ]; then + brew_bin="/opt/homebrew/bin/brew" + elif [ -x "/usr/local/bin/brew" ]; then + brew_bin="/usr/local/bin/brew" fi - case "$f" in - gnu_over_bsd) GET_BASHED_GNU=$v ;; - build_flags) GET_BASHED_BUILD_FLAGS=$v ;; - auto_tools) GET_BASHED_AUTO_TOOLS=$v ;; - ssh_agent) GET_BASHED_SSH_AGENT=$v ;; - doppler_env) GET_BASHED_USE_DOPPLER=$v ;; - bash_it) GET_BASHED_USE_BASH_IT=$v ;; - git_signing) GET_BASHED_GIT_SIGNING=$v ;; - dev_tools) GROUP_INSTALLS="${GROUP_INSTALLS},rg,fd,bat,fzf,jq,yq,tree,direnv,starship,nodejs,python,bash" ;; - ops_tools) GROUP_INSTALLS="${GROUP_INSTALLS},gh,git_lfs,terraform,awscli,kubectl,helm,stern,doppler,nodejs,python,java,bash" ;; - *) return 1 ;; - esac -} - -# @description Split a comma-delimited list into space-delimited output. -# @arg $1 string Comma list. -# @stdout Space-delimited items. -split_csv() { - local s="$1"; IFS=',' read -r -a _parts <<<"$s"; echo "${_parts[@]}"; -} - -# @internal -is_valid_id() { - [[ "$1" =~ ^[a-z0-9_]+$ ]] -} - -# @internal -is_valid_profile() { - [[ "$1" =~ ^[a-z0-9_-]+$ ]] -} - -# @internal -installer_exists() { - local needle="$1" id - for id in $INSTALLERS; do - [[ "$id" == "$needle" ]] && return 0 - done - return 1 -} -# Installer registry -INSTALLERS="" -declare -A INSTALL_IN_PROGRESS -# @internal -load_installers() { - local id - for id in "${TOOL_IDS[@]}"; do - if ! is_valid_id "$id"; then - echo "Invalid installer id: $id (from tools.sh)" >&2 - exit 1 - fi - INSTALLERS="$INSTALLERS $id" - printf -v "INSTALL_DEPS_${id}" "%s" "${TOOL_DEPS[$id]:-}" - printf -v "INSTALL_OPT_DEPS_${id}" "%s" "${TOOL_OPT_DEPS[$id]:-}" - printf -v "INSTALL_DESC_${id}" "%s" "${TOOL_DESC[$id]:-}" - printf -v "INSTALL_PLATFORMS_${id}" "%s" "${TOOL_PLATFORMS[$id]:-}" - done -} - -# @internal -get_deps() { - local id="$1" - local var="INSTALL_DEPS_${id}" - local deps="${!var:-}" - local opt_var="INSTALL_OPT_DEPS_${id}" - local opt="${!opt_var:-}" - if [[ -n "$opt" ]]; then - IFS=';' read -r -a _parts <<<"$opt" - local part key list val - for part in "${_parts[@]}"; do - key="${part%%:*}" - list="${part#*:}" - val="${!key:-0}" - if [[ "$val" == "1" ]]; then - if [[ -n "$deps" ]]; then - deps="${deps} ${list}" - else - deps="${list}" - fi - fi - done - fi - echo "${deps}" -} - -# @internal -is_done() { - local id="$1" - local var="INSTALLED_${id}" - [[ "${!var:-0}" == 1 ]] -} - -# @internal -mark_done() { - local id="$1" - printf -v "INSTALLED_${id}" "%s" 1 -} - -# @internal -run_install() { - local id="$1" dep - if ! is_valid_id "$id"; then - echo "Invalid installer id: $id" >&2 - return 1 - fi - if [[ "${INSTALL_IN_PROGRESS[$id]:-0}" == "1" ]]; then - echo "Circular dependency detected at: $id" >&2 - return 1 - fi - if is_done "$id"; then + if command -v bash >/dev/null 2>&1; then return 0 fi - INSTALL_IN_PROGRESS["$id"]=1 - for dep in $(get_deps "$id"); do - run_install "$dep" - done - if [[ "$DRY_RUN" -eq 1 ]]; then - echo "would install: $id" - else - if declare -f "install_${id}" >/dev/null 2>&1; then - "install_${id}" - else - install_tool "$id" - fi - fi - unset INSTALL_IN_PROGRESS["$id"] - mark_done "$id" -} - -# @internal -install_dialog() { - if command -v dialog >/dev/null 2>&1; then + if command -v /opt/homebrew/bin/bash >/dev/null 2>&1; then return 0 fi - if command -v brew >/dev/null 2>&1; then - brew install dialog - elif command -v apt-get >/dev/null 2>&1; then - apt_install dialog - elif command -v dnf >/dev/null 2>&1; then - dnf_install dialog - elif command -v yum >/dev/null 2>&1; then - yum_install dialog - fi -} - -# @internal -prompt_yes_no() { - local label="$1" answer - if [[ "$YES" -eq 1 ]]; then + if command -v /usr/local/bin/bash >/dev/null 2>&1; then return 0 fi - read -r -p "$label [y/N]: " answer - [[ "$answer" =~ ^[Yy]$ ]] -} - -if [[ "$WITH_UI" -eq 1 ]] && [[ "$AUTO" -eq 0 ]]; then - install_dialog || true -fi - -# If stdin isn't a TTY, default to non-interactive. -if [[ ! -t 0 ]] && [[ "$AUTO" -eq 0 ]]; then - AUTO=1 -fi - -# Load installer registry early for interactive UI. -load_installers - -# Preserve CLI features/installers so profiles do not clobber them. -CLI_FEATURES="${FEATURES:-}" -CLI_INSTALLS="${INSTALLS:-}" -FEATURES="" -INSTALLS="" - -# Apply profiles first -if [[ -n "$PROFILES" ]]; then - for p in $(split_csv "$PROFILES"); do - if ! is_valid_profile "$p"; then - echo "Invalid profile name: $p" >&2 - exit 1 - fi - # Load profile file if present - PROFILE_FILE="$REPO_DIR/profiles/${p}.env" - if [[ -r "$PROFILE_FILE" ]]; then - # shellcheck disable=SC1090 - source "$PROFILE_FILE" - PROFILE_FEATURES="${FEATURES:-}" - if [[ -n "$PROFILE_FEATURES" ]]; then - for f in $(split_csv "$PROFILE_FEATURES"); do - apply_feature "$f" || { echo "Unknown feature: $f"; exit 1; } - done - fi - if [[ -n "${INSTALLS:-}" ]]; then - GROUP_INSTALLS="${GROUP_INSTALLS},${INSTALLS}" - fi - FEATURES="" - INSTALLS="" - else - apply_profile "$p" || { echo "Unknown profile: $p"; exit 1; } - fi - done -fi - -FEATURES="$CLI_FEATURES" -INSTALLS="$CLI_INSTALLS" - -# Apply features overrides -if [[ -n "$FEATURES" ]]; then - for f in $(split_csv "$FEATURES"); do - apply_feature "$f" || { echo "Unknown feature: $f"; exit 1; } - done -fi -# Interactive selection -if [[ "$AUTO" -eq 0 ]]; then - if [[ "$WITH_UI" -eq 1 ]] && command -v dialog >/dev/null 2>&1; then - if [[ "$YES" -ne 1 ]]; then - PROFILE_CHOICE=$(dialog --clear --title "get-bashed" --menu "Select a profile" 12 60 3 \ - minimal "Minimal defaults" \ - dev "Developer workstation" \ - ops "Ops/Platform workstation" \ - 3>&1 1>&2 2>&3) || true - if [[ -n "$PROFILE_CHOICE" ]]; then - apply_profile "$PROFILE_CHOICE" - fi - - CHOICES=$(dialog --clear --title "get-bashed" --checklist "Enable features" 18 70 8 \ - gnu_over_bsd "Prefer GNU tools on macOS" "$( [[ "$GET_BASHED_GNU" -eq 1 ]] && echo on || echo off )" \ - build_flags "Enable runtime build flags" "$( [[ "$GET_BASHED_BUILD_FLAGS" -eq 1 ]] && echo on || echo off )" \ - auto_tools "Auto-install optional tools" "$( [[ "$GET_BASHED_AUTO_TOOLS" -eq 1 ]] && echo on || echo off )" \ - ssh_agent "Auto-start ssh-agent" "$( [[ "$GET_BASHED_SSH_AGENT" -eq 1 ]] && echo on || echo off )" \ - doppler_env "Enable Doppler env usage" "$( [[ "$GET_BASHED_USE_DOPPLER" -eq 1 ]] && echo on || echo off )" \ - bash_it "Enable bash-it (if installed)" "$( [[ "$GET_BASHED_USE_BASH_IT" -eq 1 ]] && echo on || echo off )" \ - git_signing "Enable git signing (gnupg)" "$( [[ "$GET_BASHED_GIT_SIGNING" -eq 1 ]] && echo on || echo off )" \ - dev_tools "Developer tool bundle" off \ - ops_tools "Ops tool bundle" off \ - 3>&1 1>&2 2>&3) || true - - GET_BASHED_GNU=0 - GET_BASHED_BUILD_FLAGS=0 - GET_BASHED_AUTO_TOOLS=0 - GET_BASHED_SSH_AGENT=0 - GET_BASHED_USE_DOPPLER=0 - GET_BASHED_USE_BASH_IT=0 - GET_BASHED_GIT_SIGNING=0 - - for choice in $CHOICES; do - apply_feature "${choice//\"/}" || true - done - - dialog_opts=() - for id in $INSTALLERS; do - desc_var="INSTALL_DESC_${id}" - desc="${!desc_var}" - [[ -z "$desc" ]] && desc="$id" - default_state="off" - if [[ "$id" == "dialog" ]]; then - default_state="on" - fi - dialog_opts+=("$id" "$desc" "$default_state") - done - - INSTALLS_DIALOG=$(dialog --clear --title "get-bashed" --checklist "Select installers" 20 80 12 \ - "${dialog_opts[@]}" \ - 3>&1 1>&2 2>&3) || true - if [[ -n "$INSTALLS_DIALOG" ]]; then - INSTALLS="${INSTALLS_DIALOG//\"/}" - INSTALLS="${INSTALLS// /,}" - fi - - if [[ -z "$USER_NAME" ]]; then - USER_NAME=$(dialog --clear --title "get-bashed" --inputbox "Git user.name" 8 60 "${USER_NAME}" 3>&1 1>&2 2>&3) || true - fi - if [[ -z "$USER_EMAIL" ]]; then - USER_EMAIL=$(dialog --clear --title "get-bashed" --inputbox "Git user.email" 8 60 "${USER_EMAIL}" 3>&1 1>&2 2>&3) || true - fi - - dialog --clear --title "get-bashed" --yesno \ - "Proceed with installation?\n\nFeatures: gnu_over_bsd=${GET_BASHED_GNU} build_flags=${GET_BASHED_BUILD_FLAGS} auto_tools=${GET_BASHED_AUTO_TOOLS} ssh_agent=${GET_BASHED_SSH_AGENT} doppler_env=${GET_BASHED_USE_DOPPLER} bash_it=${GET_BASHED_USE_BASH_IT} git_signing=${GET_BASHED_GIT_SIGNING}\nInstallers: ${INSTALLS}" \ - 12 70 || exit 0 - fi - else - if [[ "$YES" -ne 1 ]]; then - echo "Configure installation options (interactive)" - read -r -p "Profile (minimal/dev/ops, enter to skip): " PROFILE_CHOICE - if [[ -n "$PROFILE_CHOICE" ]]; then - apply_profile "$PROFILE_CHOICE" || true - fi - - prompt_yes_no "Enable GNU tools on macOS (gnu_over_bsd)?" && GET_BASHED_GNU=1 - prompt_yes_no "Enable build flags (build_flags)?" && GET_BASHED_BUILD_FLAGS=1 - prompt_yes_no "Auto-install optional tools (auto_tools)?" && GET_BASHED_AUTO_TOOLS=1 - prompt_yes_no "Start ssh-agent automatically (ssh_agent)?" && GET_BASHED_SSH_AGENT=1 - prompt_yes_no "Enable Doppler env support (doppler_env)?" && GET_BASHED_USE_DOPPLER=1 - prompt_yes_no "Enable bash-it (bash_it)?" && GET_BASHED_USE_BASH_IT=1 - prompt_yes_no "Enable git signing (git_signing)?" && GET_BASHED_GIT_SIGNING=1 - prompt_yes_no "Include developer tool bundle (dev_tools)?" && apply_feature "dev_tools" - prompt_yes_no "Include ops tool bundle (ops_tools)?" && apply_feature "ops_tools" - - read -r -p "Installers (comma list, e.g. brew,asdf,doppler): " INSTALLS_INPUT - if [[ -n "$INSTALLS_INPUT" ]]; then - INSTALLS="$INSTALLS_INPUT" - fi - - if [[ -z "$USER_NAME" ]]; then - read -r -p "Git user.name (enter to skip): " USER_NAME - fi - if [[ -z "$USER_EMAIL" ]]; then - read -r -p "Git user.email (enter to skip): " USER_EMAIL - fi - - echo "Proceeding with:" - echo " Features: gnu_over_bsd=${GET_BASHED_GNU} build_flags=${GET_BASHED_BUILD_FLAGS} auto_tools=${GET_BASHED_AUTO_TOOLS} ssh_agent=${GET_BASHED_SSH_AGENT} doppler_env=${GET_BASHED_USE_DOPPLER} bash_it=${GET_BASHED_USE_BASH_IT} git_signing=${GET_BASHED_GIT_SIGNING}" - echo " Installers: ${INSTALLS}" - [[ -n "$USER_NAME" ]] && echo " Git user.name: ${USER_NAME}" - [[ -n "$USER_EMAIL" ]] && echo " Git user.email: ${USER_EMAIL}" - prompt_yes_no "Continue?" || exit 0 - fi - fi -fi - -# Merge group installs into INSTALLS -if [[ -n "${GROUP_INSTALLS:-}" ]]; then - if [[ -n "$INSTALLS" ]]; then - INSTALLS="${INSTALLS},${GROUP_INSTALLS}" - else - INSTALLS="${GROUP_INSTALLS}" + if [ -n "$brew_bin" ]; then + "$brew_bin" install bash + return 0 fi -fi - -# Validate vimrc mode -case "$VIMRC_MODE" in - awesome|basic) ;; - *) - echo "Invalid --vimrc-mode: $VIMRC_MODE (expected awesome|basic)" >&2 - exit 1 - ;; -esac - -# Deduplicate installers -if [[ -n "$INSTALLS" ]]; then - INSTALLS="$(echo "$INSTALLS" | tr ',' '\n' | awk 'NF && !seen[$0]++' | paste -sd, -)" -fi - -# Validate installer ids (after dedupe) -if [[ -n "$INSTALLS" ]]; then - for id in $(split_csv "$INSTALLS"); do - if ! installer_exists "$id"; then - echo "Unknown installer: $id" >&2 - exit 1 - fi - done -fi - -if [[ "$LIST_FEATURES" -eq 1 ]]; then - echo "Features:" - echo " gnu_over_bsd" - echo " build_flags" - echo " auto_tools" - echo " ssh_agent" - echo " doppler_env" - echo " bash_it" - echo " git_signing" - echo " dev_tools (bundle)" - echo " ops_tools (bundle)" - exit 0 -fi - -if [[ "$LIST_PROFILES" -eq 1 ]]; then - echo "Profiles:" - for p in "$REPO_DIR"/profiles/*.env; do - [[ -e "$p" ]] || continue - echo " - $(basename "$p" .env)" - done - exit 0 -fi - -if [[ "$LIST_INSTALLERS" -eq 1 || "$LIST" -eq 1 ]]; then - echo "Available installers:" - for id in $INSTALLERS; do - desc_var="INSTALL_DESC_${id}" - plat_var="INSTALL_PLATFORMS_${id}" - desc="${!desc_var}" - plats="${!plat_var}" - printf " - %s%s%s\n" "$id" \ - "$( [[ -n "$desc" ]] && printf " :: %s" "$desc" )" \ - "$( [[ -n "$plats" ]] && printf " [%s]" "$plats" )" - done - exit 0 -fi - -if [[ -n "$INSTALLS" ]]; then - export GET_BASHED_HOME="$PREFIX" - export GET_BASHED_VIMRC_MODE="$VIMRC_MODE" - for id in $(split_csv "$INSTALLS"); do - run_install "$id" - done -fi - -# Install files -if [[ -e "$PREFIX" && "$FORCE" -ne 1 ]]; then - BACKUP="${PREFIX}.bak.$(date +%Y%m%d%H%M%S)" - echo "Backing up existing $PREFIX to $BACKUP" - mv "$PREFIX" "$BACKUP" -fi - -mkdir -p "$PREFIX" - -rsync -a \ - --exclude '.git' \ - --exclude '.github' \ - --exclude 'tests' \ - --exclude 'docs' \ - "$REPO_DIR/" "$PREFIX/" - -chmod +x "$PREFIX/bin"/* 2>/dev/null || true - -# secrets.d bootstrap -mkdir -p "$PREFIX/secrets.d" -if [[ ! -e "$PREFIX/secrets.d/00-local.sh" ]]; then - cat <<'__SECRETS__' > "$PREFIX/secrets.d/00-local.sh" -# Local secrets for get-bashed. -# This file is intentionally untracked. -__SECRETS__ -fi - -CONFIG_FILE="$PREFIX/get-bashedrc.sh" -cat <<__CFG__ > "$CONFIG_FILE" -# Generated by get-bashed installer. Edit if needed. -export GET_BASHED_GNU=${GET_BASHED_GNU} -export GET_BASHED_BUILD_FLAGS=${GET_BASHED_BUILD_FLAGS} -export GET_BASHED_AUTO_TOOLS=${GET_BASHED_AUTO_TOOLS} -export GET_BASHED_SSH_AGENT=${GET_BASHED_SSH_AGENT} -export GET_BASHED_USE_DOPPLER=${GET_BASHED_USE_DOPPLER} -export GET_BASHED_USE_BASH_IT=${GET_BASHED_USE_BASH_IT} -export GET_BASHED_GIT_SIGNING=${GET_BASHED_GIT_SIGNING} -export GET_BASHED_VIMRC_MODE=${VIMRC_MODE} -export GET_BASHED_LINK_DOTFILES=${LINK_DOTFILES} -export GET_BASHED_USER_NAME="${USER_NAME}" -export GET_BASHED_USER_EMAIL="${USER_EMAIL}" -__CFG__ - -# @internal -ensure_block() { - local file="$1" marker="$2" content="$3" - mkdir -p "$(dirname "$file")" - touch "$file" - if ! grep -Fqs "$marker" "$file"; then - printf '\n%s\n%s\n' "$marker" "$content" >> "$file" + if command -v apt-get >/dev/null 2>&1; then + sudo apt-get update -y + sudo apt-get install -y bash + return 0 fi -} - -BASHRC_LINE="# get-bashed: source modular bashrc" -BASHRC_SNIP='if [[ -r "$HOME/.get-bashed/bashrc" ]]; then source "$HOME/.get-bashed/bashrc"; fi' - -BASH_PROFILE_LINE="# get-bashed: source login bash_profile" -BASH_PROFILE_SNIP='if [[ -r "$HOME/.get-bashed/bash_profile" ]]; then source "$HOME/.get-bashed/bash_profile"; fi' - -if [[ "$LINK_DOTFILES" -ne 1 ]]; then - ensure_block "$HOME/.bashrc" "$BASHRC_LINE" "$BASHRC_SNIP" - ensure_block "$HOME/.bash_profile" "$BASH_PROFILE_LINE" "$BASH_PROFILE_SNIP" -fi - -link_dotfile() { - local name="$1" - local src="$PREFIX/$name" - local dst="$HOME/.${name}" - if [[ ! -e "$src" ]]; then + if command -v dnf >/dev/null 2>&1; then + sudo dnf install -y bash return 0 fi - if [[ -L "$dst" ]] && [[ "$(readlink "$dst")" == "$src" ]]; then + if command -v yum >/dev/null 2>&1; then + sudo yum install -y bash return 0 fi - if [[ -e "$dst" || -L "$dst" ]]; then - local backup_dir="$PREFIX/backup" - local stamp - stamp="$(date +%Y%m%d%H%M%S)" - mkdir -p "$backup_dir" - mv "$dst" "$backup_dir/${name}.${stamp}" + if command -v pacman >/dev/null 2>&1; then + sudo pacman -Sy --noconfirm bash + return 0 fi - ln -s "$src" "$dst" -} -apply_gitconfig() { - local cfg="$PREFIX/gitconfig" - [[ -f "$cfg" ]] || return 0 - if [[ -n "$USER_NAME" ]]; then - git config -f "$cfg" user.name "$USER_NAME" - fi - if [[ -n "$USER_EMAIL" ]]; then - git config -f "$cfg" user.email "$USER_EMAIL" - fi + fail "Bash is required but was not found or installed. Install bash and re-run." } -if [[ "$LINK_DOTFILES" -eq 1 ]]; then - if [[ -z "$USER_NAME" || -z "$USER_EMAIL" ]]; then - echo "Skipping gitconfig link: missing --name/--email." >&2 - else - apply_gitconfig - fi - link_dotfile "bashrc" - link_dotfile "bash_profile" - link_dotfile "inputrc" - link_dotfile "bash_aliases" - link_dotfile "vimrc" - if [[ -n "$USER_NAME" && -n "$USER_EMAIL" ]]; then - link_dotfile "gitconfig" - fi -fi +ensure_bash -echo "Installed get-bashed to $PREFIX" +if [ -x "/opt/homebrew/bin/bash" ]; then + exec /opt/homebrew/bin/bash "$(dirname "$0")/install.bash" "$@" +fi +if [ -x "/usr/local/bin/bash" ]; then + exec /usr/local/bin/bash "$(dirname "$0")/install.bash" "$@" +fi +exec bash "$(dirname "$0")/install.bash" "$@" diff --git a/installers/_helpers.sh b/installers/_helpers.sh index 3aad2d3..27504d5 100755 --- a/installers/_helpers.sh +++ b/installers/_helpers.sh @@ -10,7 +10,31 @@ _using_asdf() { command -v asdf >/dev/null 2>&1; } # @internal -_using_brew() { command -v brew >/dev/null 2>&1; } +_brew_bin() { + if command -v brew >/dev/null 2>&1; then + command -v brew + return 0 + fi + if [[ -x "/opt/homebrew/bin/brew" ]]; then + echo "/opt/homebrew/bin/brew" + return 0 + fi + if [[ -x "/usr/local/bin/brew" ]]; then + echo "/usr/local/bin/brew" + return 0 + fi + return 1 +} + +# @internal +_using_brew() { _brew_bin >/dev/null 2>&1; } + +# @internal +brew_exec() { + local brew_bin + brew_bin="$(_brew_bin)" || return 1 + "$brew_bin" "$@" +} # @internal _using_git() { command -v git >/dev/null 2>&1; } @@ -170,7 +194,7 @@ component_install() { fi if _using_brew; then - if brew install "$term"; then + if brew_exec install "$term"; then return 0 fi fi @@ -240,6 +264,11 @@ install_tool() { return $? fi + local bin="${TOOL_BIN[$id]:-}" + if [[ -n "$bin" ]] && command -v "$bin" >/dev/null 2>&1; then + return 0 + fi + local methods="${TOOL_METHODS[$id]:-}" if [[ -z "$methods" ]]; then echo "No install methods defined for $id" >&2 @@ -252,7 +281,7 @@ install_tool() { case "$method" in brew) _using_brew || continue - brew install "${TOOL_BREW[$id]:-$id}" && return 0 + brew_exec install "${TOOL_BREW[$id]:-$id}" && return 0 ;; apt) command -v apt-get >/dev/null 2>&1 || continue @@ -323,7 +352,7 @@ install_asdf() { fi if _using_brew; then - brew install asdf + brew_exec install asdf return 0 fi @@ -347,7 +376,7 @@ install_asdf() { # @description Install GNU tools (handler). install_gnu_tools() { if _using_brew; then - brew install coreutils findutils gnu-sed gnu-tar + brew_exec install coreutils findutils gnu-sed gnu-tar return 0 fi echo "GNU tools install requires Homebrew." >&2 @@ -458,7 +487,7 @@ install_shdoc() { local bash_major bash_major="$(bash -c 'echo ${BASH_VERSINFO[0]:-0}' 2>/dev/null || echo 0)" if [[ "$bash_major" -lt 4 ]] && _using_brew; then - brew install bash || true + brew_exec install bash || true fi local tmp_dir @@ -498,7 +527,7 @@ install_actionlint() { fi if _using_brew; then - brew install actionlint && return 0 + brew_exec install actionlint && return 0 fi if command -v apt-get >/dev/null 2>&1; then if apt_install actionlint; then @@ -562,7 +591,7 @@ PY pkg_install() { local brew_pkg="$1" apt_pkg="${2:-$1}" dnf_pkg="${3:-$1}" yum_pkg="${4:-$1}" if _using_brew; then - brew install "$brew_pkg" + brew_exec install "$brew_pkg" elif command -v apt-get >/dev/null 2>&1; then apt_install "$apt_pkg" elif command -v dnf >/dev/null 2>&1; then diff --git a/installers/tools.sh b/installers/tools.sh index ee3656e..1728bd5 100644 --- a/installers/tools.sh +++ b/installers/tools.sh @@ -20,6 +20,7 @@ declare -A TOOL_CURL_URL declare -A TOOL_CURL_CMD declare -A TOOL_HANDLER declare -A TOOL_OPT_DEPS +declare -A TOOL_BIN tool_register() { local id="$1" desc="$2" deps="$3" platforms="$4" methods="$5" @@ -59,9 +60,25 @@ tool_opt_deps() { local id="$1" spec="$2" TOOL_OPT_DEPS["$id"]="$spec" } + +tool_bin() { + local id="$1" bin="$2" + TOOL_BIN["$id"]="$bin" +} + +# @internal +load_installers() { + INSTALLERS="" + for id in "${TOOL_IDS[@]}"; do + INSTALLERS="${INSTALLERS}${id} " + local desc="${TOOL_DESC[$id]:-$id}" + printf -v "INSTALL_DESC_${id}" "%s" "$desc" + done +} # Core tools tool_register brew "Homebrew/Linuxbrew installer" "" "macos,linux,wsl" "curl" tool_curl brew "https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh" "/bin/bash" +tool_bin brew "brew" tool_register asdf "asdf version manager" "" "macos,linux,wsl" "handler" tool_handler asdf "install_asdf" @@ -124,6 +141,7 @@ tool_pkgs direnv "direnv" "direnv" "" "" "" tool_register starship "starship" "" "macos,linux,wsl" "brew" tool_pkgs starship "starship" "" "" "" "" +tool_bin starship "starship" tool_register rg "ripgrep" "" "macos,linux,wsl" "brew,apt,dnf,yum,pacman" tool_pkgs rg "ripgrep" "ripgrep" "ripgrep" "ripgrep" "ripgrep" diff --git a/scripts/gen-docs.sh b/scripts/gen-docs.sh index c6c34a8..a9f4b5b 100755 --- a/scripts/gen-docs.sh +++ b/scripts/gen-docs.sh @@ -13,7 +13,7 @@ command -v shdoc >/dev/null 2>&1 || { exit 1 } -shdoc < "$ROOT_DIR/install.sh" > "$ROOT_DIR/docs/INSTALLER.md" +shdoc < "$ROOT_DIR/install.bash" > "$ROOT_DIR/docs/INSTALLER.md" shdoc < "$ROOT_DIR/installers/_helpers.sh" > "$ROOT_DIR/docs/INSTALLERS_HELPERS.md" shdoc < "$ROOT_DIR/installers/tools.sh" > "$ROOT_DIR/docs/INSTALLERS.md" From 5d797312faeee14067e942bae273fbe75da06dcc Mon Sep 17 00:00:00 2001 From: Jon B Date: Tue, 3 Feb 2026 15:54:02 -0600 Subject: [PATCH 25/33] fix: link dotfiles and install bash-it --- AGENTS.md | 3 ++- CONTRIBUTING.md | 3 ++- TOOLS.md | 1 + install.bash | 37 ++++++++++++++++++++++++------------- tests/link_dotfiles.bats | 16 ++++++++++++++++ 5 files changed, 45 insertions(+), 15 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4f4fcab..930a87d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,8 @@ This repository contains the **get-bashed** modular Bash setup. It is intended t - `bashrc` / `bash_profile`: entrypoints sourced by the installer. - `bashrc.d/`: ordered modules (`00-` to `99-`) loaded in sequence. - `bin/`: curated helper scripts meant to be portable and non-sensitive. -- `install.sh`: idempotent installer that wires user dotfiles. +- `install.sh`: POSIX bootstrap that re-execs `install.bash`. +- `install.bash`: idempotent installer that wires user dotfiles. - `installers/`: dependency-aware installers with metadata. - `tests/`: BATS test suite. - `.github/workflows/`: CI and release automation. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2ec2d6f..50946ed 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,8 @@ Thanks for helping improve get-bashed. This repo is intentionally shell-first, p - `bashrc` and `bash_profile` are the entrypoints. - `bashrc.d/` contains ordered runtime modules. -- `install.sh` is the installer and config generator. +- `install.sh` is the POSIX bootstrap that hands off to `install.bash`. +- `install.bash` is the full installer and config generator. - `installers/` holds dependency-aware installers and helpers. - `scripts/` includes CI and doc helpers. - `tests/` contains Bats tests and helper libraries. diff --git a/TOOLS.md b/TOOLS.md index 42976b2..2e68feb 100644 --- a/TOOLS.md +++ b/TOOLS.md @@ -108,6 +108,7 @@ manager (Homebrew, apt, dnf, yum). Otherwise it falls back to plain prompts. ## Listing and Dry Run +- `./install.sh` is a POSIX bootstrap that runs `install.bash`. - `./install.sh --list` shows the catalog of installers. - `./install.sh --list-profiles` shows available profiles. - `./install.sh --list-features` shows available features. diff --git a/install.bash b/install.bash index 91b8218..9b62f42 100755 --- a/install.bash +++ b/install.bash @@ -199,7 +199,12 @@ apply_feature() { auto_tools) GET_BASHED_AUTO_TOOLS=$v ;; ssh_agent) GET_BASHED_SSH_AGENT=$v ;; doppler_env) GET_BASHED_USE_DOPPLER=$v ;; - bash_it) GET_BASHED_USE_BASH_IT=$v ;; + bash_it) + GET_BASHED_USE_BASH_IT=$v + if [[ "$v" -eq 1 ]]; then + GROUP_INSTALLS="${GROUP_INSTALLS},bash_it" + fi + ;; git_signing) GET_BASHED_GIT_SIGNING=$v ;; dev_tools) GROUP_INSTALLS="${GROUP_INSTALLS},rg,fd,bat,fzf,jq,yq,tree,direnv,starship,nodejs,python,bash" ;; ops_tools) GROUP_INSTALLS="${GROUP_INSTALLS},gh,git_lfs,terraform,awscli,kubectl,helm,stern,doppler,nodejs,python,java,bash" ;; @@ -273,9 +278,13 @@ link_dotfile() { return 0 fi if [[ -L "$dest" ]]; then - return 0 - fi - if [[ -e "$dest" ]]; then + local current + current="$(readlink "$dest" || true)" + if [[ "$current" == "$src" ]]; then + return 0 + fi + backup_file "$dest" + elif [[ -e "$dest" ]]; then backup_file "$dest" fi ln -s "$src" "$dest" @@ -558,20 +567,22 @@ if [[ "$LINK_DOTFILES" -eq 1 ]]; then else echo "Skipping gitconfig link (missing --name/--email)." >&2 fi +else + # Update login shell snippets (idempotent) + BASHRC_LINE="# get-bashed: source modular bashrc" + BASHRC_SNIP='if [[ -r "$HOME/.get-bashed/bashrc" ]]; then source "$HOME/.get-bashed/bashrc"; fi' + BASH_PROFILE_LINE="# get-bashed: source login bash_profile" + BASH_PROFILE_SNIP='if [[ -r "$HOME/.get-bashed/bash_profile" ]]; then source "$HOME/.get-bashed/bash_profile"; fi' + + ensure_block "$HOME/.bashrc" "$BASHRC_LINE" "$BASHRC_SNIP" + ensure_block "$HOME/.bash_profile" "$BASH_PROFILE_LINE" "$BASH_PROFILE_SNIP" fi -# Update login shell snippets (idempotent) -BASHRC_LINE="# get-bashed: source modular bashrc" -BASHRC_SNIP='if [[ -r "$HOME/.get-bashed/bashrc" ]]; then source "$HOME/.get-bashed/bashrc"; fi' -BASH_PROFILE_LINE="# get-bashed: source login bash_profile" -BASH_PROFILE_SNIP='if [[ -r "$HOME/.get-bashed/bash_profile" ]]; then source "$HOME/.get-bashed/bash_profile"; fi' - -ensure_block "$HOME/.bashrc" "$BASHRC_LINE" "$BASHRC_SNIP" -ensure_block "$HOME/.bash_profile" "$BASH_PROFILE_LINE" "$BASH_PROFILE_SNIP" - # Installers if [[ -n "$INSTALLS" ]]; then INSTALLS="${INSTALLS},${GROUP_INSTALLS#,}" +else + INSTALLS="${GROUP_INSTALLS#,}" fi declare -A INSTALL_IN_PROGRESS=() diff --git a/tests/link_dotfiles.bats b/tests/link_dotfiles.bats index 5632533..4df5c2c 100644 --- a/tests/link_dotfiles.bats +++ b/tests/link_dotfiles.bats @@ -47,3 +47,19 @@ load test_helper run bash -c 'ls "$1" | grep -E "^bashrc\\.[0-9]+"' _ "$TEST_HOME/.get-bashed/backup" assert_success } + +@test "link-dotfiles replaces existing symlink" { + TMPDIR="$(mktemp -d)" + HOME="$TMPDIR/home" + mkdir -p "$HOME" + TEST_HOME="$HOME" + mkdir -p "$TEST_HOME/other" + echo "legacy" > "$TEST_HOME/other/.bashrc" + ln -s "$TEST_HOME/other/.bashrc" "$TEST_HOME/.bashrc" + + HOME="$TEST_HOME" bash ./install.sh --auto --link-dotfiles --prefix "$TEST_HOME/.get-bashed" --force + + run readlink "$TEST_HOME/.bashrc" + assert_success + [[ "$output" == "$TEST_HOME/.get-bashed/bashrc" ]] +} From 12d22200e5081213c4494905795d1d76f75fb222 Mon Sep 17 00:00:00 2001 From: Jon B Date: Tue, 3 Feb 2026 15:58:22 -0600 Subject: [PATCH 26/33] fix: harden installer and diagnostics --- .github/workflows/ci.yml | 2 ++ bashrc.d/90-functions.sh | 2 +- bashrc.d/95-ssh-agent.sh | 2 +- bin/ram_usage | 13 +++++++++---- install.bash | 8 ++++++-- installers/_helpers.sh | 37 +++++++++++++++++++++++++++++++------ scripts/verify-install.sh | 36 ++++++++++++++++++++++++++++++++++++ 7 files changed, 86 insertions(+), 14 deletions(-) create mode 100755 scripts/verify-install.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 721c993..08ab0b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,3 +22,5 @@ jobs: run: ./scripts/test-setup.sh - name: Run tests run: bats tests + - name: Verify install wiring + run: ./scripts/verify-install.sh diff --git a/bashrc.d/90-functions.sh b/bashrc.d/90-functions.sh index 002f4f7..5f39999 100644 --- a/bashrc.d/90-functions.sh +++ b/bashrc.d/90-functions.sh @@ -22,6 +22,6 @@ ex () { *.zip) unzip "$safe_f" ;; *.Z) uncompress "$safe_f" ;; *.7z) 7z x "$safe_f" ;; - *) echo "'$f' cannot be extracted via ex()" ;; + *) echo "'$f' cannot be extracted via ex()" >&2; return 1 ;; esac } diff --git a/bashrc.d/95-ssh-agent.sh b/bashrc.d/95-ssh-agent.sh index c106d15..2363385 100644 --- a/bashrc.d/95-ssh-agent.sh +++ b/bashrc.d/95-ssh-agent.sh @@ -9,7 +9,7 @@ if [[ "${GET_BASHED_SSH_AGENT:-0}" == "1" ]] && [[ -t 1 ]]; then _ssh_agent_usable() { local sock="$1" rc [[ -S "$sock" ]] || return 1 - SSH_AUTH_SOCK="$sock" SSH_AGENT_PID= ssh-add -l >/dev/null 2>&1 + SSH_AUTH_SOCK="$sock" SSH_AGENT_PID="" ssh-add -l >/dev/null 2>&1 rc=$? [[ $rc -eq 0 || $rc -eq 1 ]] } diff --git a/bin/ram_usage b/bin/ram_usage index 5cd0d00..65fdd29 100755 --- a/bin/ram_usage +++ b/bin/ram_usage @@ -90,8 +90,13 @@ def parse_vm_stat(): # Calculate additional metrics memory_stats['total'] = get_total_ram() - # Available = free + purgeable + inactive (potentially) - memory_stats['available'] = memory_stats.get('free', 0) + memory_stats.get('purgeable', 0) + # Available = free + purgeable + inactive (+ speculative) + memory_stats['available'] = ( + memory_stats.get('free', 0) + + memory_stats.get('purgeable', 0) + + memory_stats.get('inactive', 0) + + memory_stats.get('speculative', 0) + ) # Used = total - available memory_stats['used'] = memory_stats['total'] - memory_stats['available'] @@ -237,7 +242,7 @@ def print_top_processes(processes, count=10): for proc in sorted_processes[:count]: mem = format_bytes(proc['rss']) - mem_percent = (proc['rss'] / total_ram) * 100 + mem_percent = (proc['rss'] / total_ram) * 100 if total_ram else 0 # Color code based on memory percentage if mem_percent > 10: @@ -263,7 +268,7 @@ def print_app_groups(app_groups, total_ram): for app_name, data in sorted_apps[:20]: # Show top 20 count = data['count'] memory = data['memory'] - memory_percent = (memory / total_ram) * 100 + memory_percent = (memory / total_ram) * 100 if total_ram else 0 total_shown_memory += memory # Color code based on memory percentage diff --git a/install.bash b/install.bash index 9b62f42..83cdeb4 100755 --- a/install.bash +++ b/install.bash @@ -514,7 +514,11 @@ export GET_BASHED_HOME="$PREFIX" copy_tree() { local src="$1" dest="$2" mkdir -p "$dest" - rsync -a --delete "$src"/ "$dest"/ + if [[ "${FORCE:-0}" -eq 1 ]]; then + rsync -a --delete "$src"/ "$dest"/ + else + rsync -a "$src"/ "$dest"/ + fi } # Copy base assets @@ -629,7 +633,7 @@ run_install() { install_tool "$id" fi fi - unset INSTALL_IN_PROGRESS["$id"] + unset "INSTALL_IN_PROGRESS[$id]" mark_done "$id" } diff --git a/installers/_helpers.sh b/installers/_helpers.sh index 27504d5..401b896 100755 --- a/installers/_helpers.sh +++ b/installers/_helpers.sh @@ -377,7 +377,7 @@ install_asdf() { install_gnu_tools() { if _using_brew; then brew_exec install coreutils findutils gnu-sed gnu-tar - return 0 + return $? fi echo "GNU tools install requires Homebrew." >&2 return 1 @@ -509,7 +509,10 @@ install_vimrc() { return 0 fi mkdir -p "$prefix/vendor" - git clone --depth=1 https://github.com/amix/vimrc.git "$target" + if ! git clone --depth=1 https://github.com/amix/vimrc.git "$target"; then + echo "Failed to clone vimrc repo." >&2 + return 1 + fi case "${GET_BASHED_VIMRC_MODE:-awesome}" in basic) sh "$target/install_basic_vimrc.sh" @@ -573,8 +576,21 @@ PY url="https://github.com/rhysd/actionlint/releases/download/${tag}/actionlint_${version}_${os}_${arch}.tar.gz" tmp_dir="$(mktemp -d)" - curl -fsSL "$url" -o "$tmp_dir/actionlint.tgz" - tar -xzf "$tmp_dir/actionlint.tgz" -C "$tmp_dir" + if ! curl -fsSL "$url" -o "$tmp_dir/actionlint.tgz"; then + rm -rf "$tmp_dir" + echo "Failed to download actionlint from $url" >&2 + return 1 + fi + if [[ ! -s "$tmp_dir/actionlint.tgz" ]]; then + rm -rf "$tmp_dir" + echo "Downloaded actionlint archive is empty." >&2 + return 1 + fi + if ! tar -xzf "$tmp_dir/actionlint.tgz" -C "$tmp_dir"; then + rm -rf "$tmp_dir" + echo "Failed to extract actionlint archive." >&2 + return 1 + fi local prefix="${GET_BASHED_HOME:-$HOME/.get-bashed}" mkdir -p "$prefix/bin" mv "$tmp_dir/actionlint" "$prefix/bin/actionlint" @@ -589,17 +605,26 @@ PY # @exitcode 0 If installed. # @exitcode 1 If no supported package manager. pkg_install() { - local brew_pkg="$1" apt_pkg="${2:-$1}" dnf_pkg="${3:-$1}" yum_pkg="${4:-$1}" + local brew_pkg="$1" + local apt_pkg="${2:-$1}" + local dnf_pkg="${3:-$1}" + local yum_pkg="${4:-$1}" + local pacman_pkg="${5:-$1}" if _using_brew; then brew_exec install "$brew_pkg" + return $? elif command -v apt-get >/dev/null 2>&1; then apt_install "$apt_pkg" + return $? elif command -v dnf >/dev/null 2>&1; then dnf_install "$dnf_pkg" + return $? elif command -v yum >/dev/null 2>&1; then yum_install "$yum_pkg" + return $? elif command -v pacman >/dev/null 2>&1; then - pacman_install "$brew_pkg" + pacman_install "$pacman_pkg" + return $? else echo "No supported package manager found for $brew_pkg" >&2 return 1 diff --git a/scripts/verify-install.sh b/scripts/verify-install.sh new file mode 100755 index 0000000..7feacf7 --- /dev/null +++ b/scripts/verify-install.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# @file verify-install +# @brief Minimal verification of get-bashed install wiring. +# @description +# Installs into a temp HOME and verifies symlinks and structure. + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +TMPDIR="$(mktemp -d)" +TEST_HOME="$TMPDIR/home" +mkdir -p "$TEST_HOME" + +HOME="$TEST_HOME" "$ROOT_DIR/install.sh" --auto --profiles minimal --link-dotfiles --name "Test User" --email "test@example.com" + +[[ -L "$TEST_HOME/.bashrc" ]] +[[ -L "$TEST_HOME/.bash_profile" ]] +[[ -L "$TEST_HOME/.inputrc" ]] +[[ -L "$TEST_HOME/.bash_aliases" ]] +[[ -L "$TEST_HOME/.vimrc" ]] +[[ -L "$TEST_HOME/.gitconfig" ]] + +[[ -d "$TEST_HOME/.get-bashed/bashrc.d" ]] +[[ -d "$TEST_HOME/.get-bashed/secrets.d" ]] + +if [[ -d "$TEST_HOME/.bashrc.d" ]]; then + echo "Unexpected .bashrc.d created under HOME" >&2 + exit 1 +fi + +if [[ -d "$TEST_HOME/.secrets.d" ]]; then + echo "Unexpected .secrets.d created under HOME" >&2 + exit 1 +fi + +echo "verify-install: ok" From 947e91d04d8325fd95ed894fc7dfcabd9bea22cf Mon Sep 17 00:00:00 2001 From: Jon B Date: Tue, 3 Feb 2026 16:08:50 -0600 Subject: [PATCH 27/33] fix: address linting and doppler loading --- .pre-commit-config.yaml | 2 +- bashrc.d/30-buildflags.sh | 1 + bashrc.d/40-completions.sh | 2 ++ bashrc.d/65-tools.sh | 1 + bashrc.d/99-secrets.sh | 6 ++++++ install.bash | 34 +++++++++++++++++++++++++++++----- memory-bank/activeContext.md | 20 +++++++++----------- memory-bank/progress.md | 6 ++++-- memory-bank/systemPatterns.md | 4 +++- scripts/pre-commit-ci.sh | 4 ++++ 10 files changed, 60 insertions(+), 20 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2af4a85..6034cb1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,4 +25,4 @@ repos: rev: 2.1.1 hooks: - id: bashate - args: [--ignore, E003,E006] + args: [--ignore=E003,E006] diff --git a/bashrc.d/30-buildflags.sh b/bashrc.d/30-buildflags.sh index 5b42eb1..2653bb4 100644 --- a/bashrc.d/30-buildflags.sh +++ b/bashrc.d/30-buildflags.sh @@ -1,3 +1,4 @@ +#!/usr/bin/env bash # @file 30-buildflags # @brief get-bashed module: 30-buildflags # @description diff --git a/bashrc.d/40-completions.sh b/bashrc.d/40-completions.sh index 5ae6589..ab7eaf8 100644 --- a/bashrc.d/40-completions.sh +++ b/bashrc.d/40-completions.sh @@ -1,4 +1,6 @@ #!/usr/bin/env bash +#!/usr/bin/env bash +# shellcheck disable=SC1090,SC1091 # @file 40-completions # @brief get-bashed module: 40-completions # @description diff --git a/bashrc.d/65-tools.sh b/bashrc.d/65-tools.sh index d12ab30..410ea9f 100644 --- a/bashrc.d/65-tools.sh +++ b/bashrc.d/65-tools.sh @@ -1,3 +1,4 @@ +#!/usr/bin/env bash # @file 65-tools # @brief get-bashed module: 65-tools # @description diff --git a/bashrc.d/99-secrets.sh b/bashrc.d/99-secrets.sh index 693a2d1..1e28737 100644 --- a/bashrc.d/99-secrets.sh +++ b/bashrc.d/99-secrets.sh @@ -7,6 +7,12 @@ GET_BASHED_HOME="${GET_BASHED_HOME:-$HOME/.get-bashed}" GET_BASHED_SECRETS_DIR="${GET_BASHED_SECRETS_DIR:-$GET_BASHED_HOME/secrets.d}" +if [[ "${GET_BASHED_USE_DOPPLER:-0}" == "1" ]] && command -v doppler >/dev/null 2>&1; then + set -a + source <(doppler secrets download --no-file --format env) + set +a +fi + if [[ -d "$GET_BASHED_SECRETS_DIR" ]]; then for f in "$GET_BASHED_SECRETS_DIR"/*.sh; do [[ -r "$f" ]] && source "$f" diff --git a/install.bash b/install.bash index 83cdeb4..3a8671e 100755 --- a/install.bash +++ b/install.bash @@ -37,7 +37,9 @@ USAGE } REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck disable=SC1091 source "$REPO_DIR/installers/_helpers.sh" +# shellcheck disable=SC1091 source "$REPO_DIR/installers/tools.sh" PREFIX="${GET_BASHED_HOME:-$HOME/.get-bashed}" FORCE=0 @@ -264,6 +266,7 @@ backup_file() { mkdir -p "$backup_dir" local base base="$(basename "$file")" + base="${base#.}" local ts ts="$(date +%s)" mv "$file" "$backup_dir/${base}.${ts}" @@ -327,6 +330,7 @@ fi # Load installer registry early for interactive UI. load_installers +: "${INSTALLERS:=}" # Preserve CLI features/installers so profiles do not clobber them. CLI_FEATURES="${FEATURES:-}" @@ -411,6 +415,7 @@ if [[ "$AUTO" -eq 0 ]]; then done dialog_opts=() + # shellcheck disable=SC2153 for id in $INSTALLERS; do desc_var="INSTALL_DESC_${id}" desc="${!desc_var}" @@ -464,6 +469,7 @@ if [[ "$LIST" -eq 1 ]]; then echo " dev_tools" echo " ops_tools" echo "Installers:" + # shellcheck disable=SC2153 for id in $INSTALLERS; do echo " $id" done @@ -510,6 +516,7 @@ fi mkdir -p "$PREFIX" export GET_BASHED_HOME="$PREFIX" +export GET_BASHED_VIMRC_MODE="$VIMRC_MODE" copy_tree() { local src="$1" dest="$2" @@ -549,6 +556,7 @@ fi echo "export GET_BASHED_USE_DOPPLER=${GET_BASHED_USE_DOPPLER}" echo "export GET_BASHED_USE_BASH_IT=${GET_BASHED_USE_BASH_IT}" echo "export GET_BASHED_GIT_SIGNING=${GET_BASHED_GIT_SIGNING}" + echo "export GET_BASHED_VIMRC_MODE=\"${GET_BASHED_VIMRC_MODE}\"" if [[ -n "$USER_NAME" ]]; then echo "export GET_BASHED_USER_NAME=\"${USER_NAME}\"" fi @@ -573,10 +581,13 @@ if [[ "$LINK_DOTFILES" -eq 1 ]]; then fi else # Update login shell snippets (idempotent) - BASHRC_LINE="# get-bashed: source modular bashrc" - BASHRC_SNIP='if [[ -r "$HOME/.get-bashed/bashrc" ]]; then source "$HOME/.get-bashed/bashrc"; fi' - BASH_PROFILE_LINE="# get-bashed: source login bash_profile" - BASH_PROFILE_SNIP='if [[ -r "$HOME/.get-bashed/bash_profile" ]]; then source "$HOME/.get-bashed/bash_profile"; fi' + # shellcheck disable=SC2016 +BASHRC_LINE="# get-bashed: source modular bashrc" + # shellcheck disable=SC2016 +BASHRC_SNIP='if [[ -r "$HOME/.get-bashed/bashrc" ]]; then source "$HOME/.get-bashed/bashrc"; fi' +BASH_PROFILE_LINE="# get-bashed: source login bash_profile" + # shellcheck disable=SC2016 +BASH_PROFILE_SNIP='if [[ -r "$HOME/.get-bashed/bash_profile" ]]; then source "$HOME/.get-bashed/bash_profile"; fi' ensure_block "$HOME/.bashrc" "$BASHRC_LINE" "$BASHRC_SNIP" ensure_block "$HOME/.bash_profile" "$BASH_PROFILE_LINE" "$BASH_PROFILE_SNIP" @@ -594,7 +605,20 @@ declare -A INSTALL_DONE=() get_deps() { local id="$1" - echo "${TOOL_DEPS[$id]:-}" + _ensure_tools_loaded + local deps="${TOOL_DEPS[$id]:-}" + local opt="${TOOL_OPT_DEPS[$id]:-}" + if [[ -n "$opt" ]]; then + IFS=',' read -r -a _opt_specs <<<"$opt" + for spec in "${_opt_specs[@]}"; do + local flag="${spec%%:*}" + local dep="${spec#*:}" + if [[ -n "${!flag:-}" && "${!flag}" != "0" ]]; then + deps="${deps},${dep}" + fi + done + fi + echo "${deps#,}" } is_done() { diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md index edf3885..5e2efd8 100644 --- a/memory-bank/activeContext.md +++ b/memory-bank/activeContext.md @@ -1,15 +1,13 @@ # Active Context -- Focus: stabilize installer registry, idempotent dotfile linking, and CI. +- Focus: POSIX bootstrap + bash installer split, idempotent symlink wiring, CI autofix/verification. - Recent changes: - - centralized tool registry in `installers/tools.sh` - - unified install handlers in `installers/_helpers.sh` - - optional dependencies keyed off config (e.g. git_signing -> gnupg) - - added dotfile linking (`--link-dotfiles`) with backups - - added default dotfiles (inputrc, vimrc, gitconfig, bash_aliases) - - added `--name/--email` prompts + config wiring - - added tests for dotfile linking and config output + - split installer into POSIX `install.sh` bootstrap and `install.bash` full installer + - dotfiles now symlink to `~/.get-bashed` (no root-level `~/.bashrc.d`/`~/.secrets.d`) + - added symlink replacement + backup behavior + - added CI autofix workflow and install verification script + - hardened installers (brew detection, actionlint downloads, vimrc clone, pkg_install) + - updated runtime modules (ssh-agent reuse, extractor error handling) - Next steps: - - finish CI green (bashate/shellcheck) - - expand BATS coverage beyond smoke tests - - refresh README to document registry + optional deps + - resolve any failing CI workflows via `gh` + - verify dogfood install on local machine stays green after new changes diff --git a/memory-bank/progress.md b/memory-bank/progress.md index 150f21b..461dcf1 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -9,11 +9,13 @@ - Dotfile linking with backups + defaults - Git identity prompts + config output - Optional dependency gating by feature flags +- POSIX bootstrap (`install.sh`) + bash installer (`install.bash`) +- Symlink-only dotfile wiring to `~/.get-bashed` +- Install verification script wired into CI ## What's left -- CI green after bashate/shellcheck changes +- Confirm CI green after latest installer refactors - Expand BATS coverage to core flows -- Document tool registry and optional deps in README ## Known issues - shdoc not available via Homebrew on macOS; uses local prefix install. diff --git a/memory-bank/systemPatterns.md b/memory-bank/systemPatterns.md index f647600..9b4ec95 100644 --- a/memory-bank/systemPatterns.md +++ b/memory-bank/systemPatterns.md @@ -1,8 +1,10 @@ # System Patterns - Modular runtime: `bashrc` loads ordered `bashrc.d/*` modules. -- Config generation: installer writes `get-bashedrc.sh` and reads it at startup. +- Config generation: `install.bash` writes `get-bashedrc.sh` and reads it at startup. - Installers: centralized tool registry in `installers/tools.sh` with handlers in `_helpers.sh`. - Profiles/features: profiles set defaults; features override; bundles expand installers. - Secrets: sourced from `~/.get-bashed/secrets.d/*.sh` only. - Optional deps: tool optional dependencies can be gated by config flags. +- Bootstrap: `install.sh` is POSIX-only and re-execs `install.bash`. +- Dotfiles: `--link-dotfiles` creates symlinks into `~/.get-bashed` and backs up existing files. diff --git a/scripts/pre-commit-ci.sh b/scripts/pre-commit-ci.sh index c4c3048..e2586d8 100755 --- a/scripts/pre-commit-ci.sh +++ b/scripts/pre-commit-ci.sh @@ -10,6 +10,10 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" . "$ROOT_DIR/scripts/ci-setup.sh" "pre_commit,actionlint,shellcheck,bashate,shdoc" +if command -v shdoc >/dev/null 2>&1; then + "$ROOT_DIR/scripts/gen-docs.sh" +fi + if command -v pre-commit >/dev/null 2>&1; then pre-commit run --all-files else From 06f823375a80d08eccedf27488c97311e0ac1e01 Mon Sep 17 00:00:00 2001 From: Jon B Date: Tue, 3 Feb 2026 16:16:06 -0600 Subject: [PATCH 28/33] fix: address shellcheck and bashate --- .pre-commit-config.yaml | 2 +- bashrc.d/00-options.sh | 1 + bashrc.d/10-helpers.sh | 1 + bashrc.d/50-tool-init.sh | 1 + bashrc.d/60-asdf.sh | 1 + bashrc.d/70-bash-it.sh | 2 +- bashrc.d/70-env.sh | 1 + bashrc.d/80-aliases.sh | 1 + bashrc.d/95-ssh-agent.sh | 8 ++++++-- bashrc.d/99-secrets.sh | 1 + installers/_helpers.sh | 4 ++-- scripts/pre-commit-ci.sh | 1 + 12 files changed, 18 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6034cb1..031ce40 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,4 +25,4 @@ repos: rev: 2.1.1 hooks: - id: bashate - args: [--ignore=E003,E006] + args: [-i, E003,E006] diff --git a/bashrc.d/00-options.sh b/bashrc.d/00-options.sh index 8f24f34..2550999 100644 --- a/bashrc.d/00-options.sh +++ b/bashrc.d/00-options.sh @@ -1,3 +1,4 @@ +#!/usr/bin/env bash # @file 00-options # @brief get-bashed module: 00-options # @description diff --git a/bashrc.d/10-helpers.sh b/bashrc.d/10-helpers.sh index 39d06e4..a5449a6 100644 --- a/bashrc.d/10-helpers.sh +++ b/bashrc.d/10-helpers.sh @@ -1,3 +1,4 @@ +#!/usr/bin/env bash # @file 10-helpers # @brief get-bashed module: 10-helpers # @description diff --git a/bashrc.d/50-tool-init.sh b/bashrc.d/50-tool-init.sh index 75138bd..9922774 100644 --- a/bashrc.d/50-tool-init.sh +++ b/bashrc.d/50-tool-init.sh @@ -1,3 +1,4 @@ +#!/usr/bin/env bash # @file 50-tool-init # @brief get-bashed module: 50-tool-init # @description diff --git a/bashrc.d/60-asdf.sh b/bashrc.d/60-asdf.sh index ab2106f..5ac6bf9 100644 --- a/bashrc.d/60-asdf.sh +++ b/bashrc.d/60-asdf.sh @@ -1,3 +1,4 @@ +#!/usr/bin/env bash # @file 60-asdf # @brief get-bashed module: 60-asdf # @description diff --git a/bashrc.d/70-bash-it.sh b/bashrc.d/70-bash-it.sh index e54f53e..c3bce27 100644 --- a/bashrc.d/70-bash-it.sh +++ b/bashrc.d/70-bash-it.sh @@ -8,7 +8,7 @@ if [[ "${GET_BASHED_USE_BASH_IT:-0}" == "1" ]]; then GET_BASHED_HOME="${GET_BASHED_HOME:-$HOME/.get-bashed}" BASH_IT="$GET_BASHED_HOME/vendor/bash-it" if [[ -r "$BASH_IT/bash_it.sh" ]]; then - # shellcheck disable=SC1090 + # shellcheck disable=SC1090,SC1091 source "$BASH_IT/bash_it.sh" get_bashed_component() { diff --git a/bashrc.d/70-env.sh b/bashrc.d/70-env.sh index 265b476..f623829 100644 --- a/bashrc.d/70-env.sh +++ b/bashrc.d/70-env.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +#!/usr/bin/env bash # @file 70-env # @brief get-bashed module: 70-env # @description diff --git a/bashrc.d/80-aliases.sh b/bashrc.d/80-aliases.sh index d74a597..a0ed903 100644 --- a/bashrc.d/80-aliases.sh +++ b/bashrc.d/80-aliases.sh @@ -1,3 +1,4 @@ +#!/usr/bin/env bash # @file 80-aliases # @brief get-bashed module: 80-aliases # @description diff --git a/bashrc.d/95-ssh-agent.sh b/bashrc.d/95-ssh-agent.sh index 2363385..1d4f524 100644 --- a/bashrc.d/95-ssh-agent.sh +++ b/bashrc.d/95-ssh-agent.sh @@ -25,6 +25,10 @@ if [[ "${GET_BASHED_SSH_AGENT:-0}" == "1" ]] && [[ -t 1 ]]; then fi fi - [[ -f "$HOME/.ssh/id_rsa" ]] && ssh-add "$HOME/.ssh/id_rsa" 2>/dev/null || true - [[ -f "$HOME/.ssh/id_ed25519" ]] && ssh-add "$HOME/.ssh/id_ed25519" 2>/dev/null || true + if [[ -f "$HOME/.ssh/id_rsa" ]]; then + ssh-add "$HOME/.ssh/id_rsa" 2>/dev/null || true + fi + if [[ -f "$HOME/.ssh/id_ed25519" ]]; then + ssh-add "$HOME/.ssh/id_ed25519" 2>/dev/null || true + fi fi diff --git a/bashrc.d/99-secrets.sh b/bashrc.d/99-secrets.sh index 1e28737..0ada727 100644 --- a/bashrc.d/99-secrets.sh +++ b/bashrc.d/99-secrets.sh @@ -1,3 +1,4 @@ +#!/usr/bin/env bash # @file 99-secrets # @brief get-bashed module: 99-secrets # @description diff --git a/installers/_helpers.sh b/installers/_helpers.sh index 401b896..8f94ced 100755 --- a/installers/_helpers.sh +++ b/installers/_helpers.sh @@ -64,7 +64,7 @@ _ensure_tools_loaded() { _tools_loaded && return 0 local repo_dir repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" - # shellcheck disable=SC1090 + # shellcheck disable=SC1090,SC1091 source "$repo_dir/installers/tools.sh" } @@ -160,7 +160,7 @@ _bash_it_search() { local action="$1"; shift local prefix="${GET_BASHED_HOME:-$HOME/.get-bashed}" local bash_it="$prefix/vendor/bash-it" - # shellcheck disable=SC1090 + # shellcheck disable=SC1090,SC1091 source "$bash_it/bash_it.sh" NO_COLOR=1 bash-it search "$@" "--${action}" } diff --git a/scripts/pre-commit-ci.sh b/scripts/pre-commit-ci.sh index e2586d8..253d54d 100755 --- a/scripts/pre-commit-ci.sh +++ b/scripts/pre-commit-ci.sh @@ -8,6 +8,7 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +# shellcheck disable=SC1091 . "$ROOT_DIR/scripts/ci-setup.sh" "pre_commit,actionlint,shellcheck,bashate,shdoc" if command -v shdoc >/dev/null 2>&1; then From bd86dd5b339208c155257365f406986d4bdcdbb0 Mon Sep 17 00:00:00 2001 From: Jon B Date: Tue, 3 Feb 2026 16:37:59 -0600 Subject: [PATCH 29/33] fix: stabilize lint configuration --- .pre-commit-config.yaml | 2 +- bashrc.d/10-helpers.sh | 1 + bashrc.d/60-asdf.sh | 1 + bashrc.d/99-secrets.sh | 1 + installers/_helpers.sh | 4 +++- 5 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 031ce40..fcfd3e0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,4 +25,4 @@ repos: rev: 2.1.1 hooks: - id: bashate - args: [-i, E003,E006] + args: [-i, E003 -i, E006] diff --git a/bashrc.d/10-helpers.sh b/bashrc.d/10-helpers.sh index a5449a6..5015b1b 100644 --- a/bashrc.d/10-helpers.sh +++ b/bashrc.d/10-helpers.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# shellcheck disable=SC1090 # @file 10-helpers # @brief get-bashed module: 10-helpers # @description diff --git a/bashrc.d/60-asdf.sh b/bashrc.d/60-asdf.sh index 5ac6bf9..f0ac7e7 100644 --- a/bashrc.d/60-asdf.sh +++ b/bashrc.d/60-asdf.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# shellcheck disable=SC1091 # @file 60-asdf # @brief get-bashed module: 60-asdf # @description diff --git a/bashrc.d/99-secrets.sh b/bashrc.d/99-secrets.sh index 0ada727..f1835d8 100644 --- a/bashrc.d/99-secrets.sh +++ b/bashrc.d/99-secrets.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# shellcheck disable=SC1090 # @file 99-secrets # @brief get-bashed module: 99-secrets # @description diff --git a/installers/_helpers.sh b/installers/_helpers.sh index 8f94ced..ad474a8 100755 --- a/installers/_helpers.sh +++ b/installers/_helpers.sh @@ -87,7 +87,9 @@ auto_exec() { shift done else - while [[ $# -gt 0 ]]; do shift; done + while [[ $# -gt 0 ]]; do + shift + done fi "$cmd" "${flags[@]}" } From 0ceeb82c106a175159890b232506ec19500a3f74 Mon Sep 17 00:00:00 2001 From: Jon B Date: Tue, 3 Feb 2026 16:43:47 -0600 Subject: [PATCH 30/33] fix: ensure docs newline and bashate args --- .pre-commit-config.yaml | 2 +- scripts/gen-docs.sh | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fcfd3e0..0f9cd62 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,4 +25,4 @@ repos: rev: 2.1.1 hooks: - id: bashate - args: [-i, E003 -i, E006] + args: ["-i", "E003", "-i", "E006"] diff --git a/scripts/gen-docs.sh b/scripts/gen-docs.sh index a9f4b5b..04ae974 100755 --- a/scripts/gen-docs.sh +++ b/scripts/gen-docs.sh @@ -45,9 +45,14 @@ fix_toc_anchors() { ensure_eof() { local file="$1" - if [[ -s "$file" ]] && [[ -n "$(tail -c 1 "$file")" ]]; then - printf "\n" >> "$file" - fi + python3 - "$file" <<'PY' +import sys +from pathlib import Path +path = Path(sys.argv[1]) +data = path.read_bytes() +if not data.endswith(b"\n"): + path.write_bytes(data + b"\n") +PY } for doc in "$ROOT_DIR/docs/INSTALLER.md" "$ROOT_DIR/docs/INSTALLERS_HELPERS.md" "$ROOT_DIR/docs/INSTALLERS.md"; do From fce33786d7fd8524dc68c2737cb27487da088f3c Mon Sep 17 00:00:00 2001 From: Jon B Date: Tue, 3 Feb 2026 16:48:48 -0600 Subject: [PATCH 31/33] fix: tune pre-commit exclusions --- .pre-commit-config.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0f9cd62..0e520e4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,6 +17,7 @@ repos: - id: detect-private-key - id: trailing-whitespace - id: end-of-file-fixer + exclude: ^docs/ - repo: https://github.com/rhysd/actionlint rev: v1.7.10 hooks: @@ -25,4 +26,5 @@ repos: rev: 2.1.1 hooks: - id: bashate - args: ["-i", "E003", "-i", "E006"] + entry: bashate --ignore=E003,E006 + exclude: \\.bats$ From a6a0f05b8f8420387ee1a29ad2ca9a13dda354d0 Mon Sep 17 00:00:00 2001 From: Jon B Date: Tue, 3 Feb 2026 16:51:37 -0600 Subject: [PATCH 32/33] fix: scope bashate to shell scripts --- .pre-commit-config.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0e520e4..f725d32 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,4 +27,5 @@ repos: hooks: - id: bashate entry: bashate --ignore=E003,E006 - exclude: \\.bats$ + files: ^(bin/.*|.*\\.(sh|bash)$) + exclude: ^tests/.*\\.bats$ From 01c711f56dcc0fe778c68e8e044b34f2a37a02fb Mon Sep 17 00:00:00 2001 From: Jon B Date: Tue, 3 Feb 2026 19:00:40 -0600 Subject: [PATCH 33/33] ci: install bats via get-bashed --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 08ab0b4..a1b121a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - uses: bats-core/bats-action@e412797c46257a2dbf3775f6f6010b33ee6cb99f # v3.0.1 + - name: CI setup (get-bashed) + run: ./scripts/ci-setup.sh "bats" - name: Fetch Bats helpers run: ./scripts/test-setup.sh - name: Run tests