diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 0000000..710147f --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,206 @@ +# bashdep - Bash Dependency Manager + +## Project Overview + +**bashdep** is a minimal, zero-runtime bash dependency manager. Declare URLs in +`.bashdep`; bashdep downloads them into `lib/` and keeps installs idempotent +via a per-directory `.bashdep.lock`. No registry, no daemon — just `curl` + +`awk` + `mktemp`. + +**Repo:** https://github.com/Chemaclass/bashdep + +## Core Principles + +### TDD by Default +**RED → GREEN → REFACTOR** — every change starts from a failing test. The +`tests/unit/bashdep_test.sh` suite is the only safety net; keep it green. + +### Bash 3.2+ Compatible + +The library must run on **macOS default bash (3.2)** as well as Linux bash 4+. +`[[ ]]` is allowed (Bash 3.2+); avoid features that need 4.0+: + +- `declare -A` (associative arrays) +- `${var,,}` / `${var^^}` (case conversion) +- `${array[-1]}` (negative indexing — needs 4.3+) +- `&>>` (append both streams) +- `mapfile` / `readarray` + +See `.claude/rules/bash-style.md` (auto-loaded when editing `bashdep` or any +`*.sh`). + +### Zero Runtime Dependencies + +Library code may only call: `curl`, `awk`, `mktemp`, `mkdir`, `rm`, `cp`, +`mv`, `printf`, `cat`, `grep`, `sort`, `tr`, `dirname`, `basename`, plus +shell builtins. **No** `jq`, `python`, `node`, etc. at runtime. + +### Quality Standards + +Every change must pass: +```bash +make test # bashunit suite +make sa # ShellCheck +make lint # editorconfig-checker +``` + +`make pre_commit/run` runs all three. + +## Architecture + +``` +bashdep/ +├── bashdep # Library entry point — sourced by consumers +├── release.sh # Release automation (see docs/releasing.md) +├── install-dependencies.sh # Installs the test runner via bashdep itself +├── Makefile # test / sa / lint / deps / release +├── tests/ +│ ├── bootstrap.sh # Shared test helpers +│ └── unit/ +│ ├── bashdep_test.sh # The unit suite +│ └── snapshots/ # Captured stdout for snapshot assertions +├── docs/ +│ ├── api.md # Public function reference +│ ├── behavior.md # Lockfile / dev-dep / error semantics +│ └── releasing.md # Release process +├── example/ # End-to-end demo +├── lib/ # Vendored test runner (bashunit) — git-ignored +├── .claude/ # Claude Code configuration +│ ├── CLAUDE.md # This file +│ ├── rules/ # Path-scoped guidelines +│ └── skills/ # Custom workflows (invoke with /skill-name) +└── .github/workflows/ # CI: tests, ShellCheck, editorconfig +``` + +## Public API Surface + +All public functions live in the single `bashdep` script under the +`bashdep::` namespace: + +| Function | Purpose | +|----------|---------| +| `bashdep::install` | Install from an inline URL array | +| `bashdep::install_from` | Install from a `.bashdep` file | +| `bashdep::setup` | Configure dirs / modes (`force`, `dry-run`, `silent`, `verbose`) | +| `bashdep::list` | List installed deps | +| `bashdep::uninstall` | Remove specific deps | +| `bashdep::clean` | Remove orphan files (in dir, not in lockfile) | +| `bashdep::doctor` | Report missing/orphan inconsistencies | +| `bashdep::self_update` | Re-download `bashdep` from upstream | +| `bashdep::version` | Print `BASHDEP_VERSION` | + +Private helpers use a leading `_` (`bashdep::_classify_dep`, `_lock_get`, …). + +## Common Commands + +```bash +make test # Run the suite +make pre_commit/run # test + sa + lint +make deps # Install bashunit (test runner) +lib/bashunit tests # Run bashunit directly +lib/bashunit tests --filter NAME # Run a single test by name +./bashdep --help # CLI usage (when invoked, not sourced) +``` + +## Test Patterns + +`tests/unit/bashdep_test.sh` is split into two layers: + +1. **Pure-logic tests** (top of file) — no filesystem, no network. Set + `BASHDEP_*` globals directly and call `bashdep::_*` helpers. +2. **Filesystem tests** — use `$TEST_DIR` (a `mktemp -d` set up in + `set_up`, cleaned in `tear_down`). Use the `_seed_lock` / + `_seed_installed` helpers to scaffold lockfile fixtures. + +**Snapshot tests** keep hardcoded `/tmp/test_` paths because the +snapshot embeds the path. Don't migrate those to `$TEST_DIR`. + +Test fixtures must never reach the network — mock or stub `curl`. + +## Skills + +Invoke with `/skill-name`: + +| Skill | Purpose | +|-------|---------| +| `/tdd-cycle` | RED → GREEN → REFACTOR for a single test | +| `/fix-test` | Debug and fix failing tests | +| `/add-command` | Add a new `bashdep::` lifecycle command with TDD | +| `/doctor-check` | Coverage gap analysis + `bashdep::doctor` audit | +| `/pre-release` | Pre-release validation checklist | +| `/release` | Run pre-release checks then execute `release.sh` | +| `/commit` | Stage and commit with conventional commits | +| `/gh-issue ` | GitHub issue → branch → implement → PR | +| `/pr [#N]` | Push branch and open a PR | + +## Path-Scoped Guidelines + +Rules auto-load based on file paths being edited (via `paths:` frontmatter +in each rule file). + +### `bashdep` and `**/*.sh` +- Bash 3.2+ compatible (no associative arrays, no `${var,,}`) +- Public functions in `bashdep::` namespace, private with leading `_` +- 2-space indent, quote variables, `$()` not backticks +- Pass `make sa` (ShellCheck) + +### `tests/**/*_test.sh` +- One assertion per test; name describes behavior +- Pure-logic tests above filesystem tests +- Use `$TEST_DIR` from `set_up`, never `/tmp/...` (except snapshot tests) +- Mock `curl` — no real network calls +- Snapshot updates: `lib/bashunit --update-snapshots tests/` + +### `docs/**/*.md` +- Reflect the actual public API in `bashdep` — no inventing flags +- Cross-link `api.md` ↔ `behavior.md` ↔ `releasing.md` where relevant +- Keep examples copy-pasteable and runnable + +## Guardrails + +### Never: +- Add a runtime dependency beyond `curl` / `awk` / `mktemp` / POSIX builtins +- Break Bash 3.2+ compatibility (test on macOS default bash mentally) +- Change public function signatures without updating `docs/api.md` and `CHANGELOG.md` +- Skip `make pre_commit/run` before pushing +- Network in tests +- Commit `lib/`, `local/`, or `tmp/` artifacts +- Commit without a corresponding test for behavior changes + +### Always: +- Write a failing test first +- Use existing patterns in `tests/unit/bashdep_test.sh` +- Update `CHANGELOG.md` `## Unreleased` section for any user-visible change +- Update `docs/api.md` when adding/changing a public `bashdep::` function +- Update `docs/behavior.md` when changing lockfile / dev-dep / error semantics +- Honor `BASHDEP_DRY_RUN`, `BASHDEP_SILENT`, `BASHDEP_VERBOSE`, `BASHDEP_FORCE` in any new mutating command + +## Definition of Done + +- All tests green for the **right reason** +- `make sa` passes (ShellCheck) +- `make lint` passes (editorconfig-checker) +- Bash 3.2+ compatible +- `CHANGELOG.md` updated (if user-facing) +- `docs/api.md` / `docs/behavior.md` updated (if API or semantics changed) +- Modes (`dry-run`, `silent`, `verbose`, `force`) honored in any new mutating path + +## Commit Message Format + +[Conventional Commits](https://conventionalcommits.org/): `(): ` + +**Types:** `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`, `perf`, `ci` +**Scopes:** `install`, `lock`, `doctor`, `clean`, `uninstall`, `setup`, `cli`, `docs`, `ci`, `release` + +Never mention AI / Claude / automation in commit messages. + +## Prohibited Actions + +**Never without explicit user request:** +- Commit secrets or `.env` contents +- Force push to `main` +- Skip git hooks (`--no-verify`) +- Amend published commits +- Use destructive git commands (`reset --hard`, `clean -f`, `branch -D`) +- Push to remote without confirmation +- Cut a release (only via `/release` after explicit user confirmation) diff --git a/.claude/rules/bash-style.md b/.claude/rules/bash-style.md new file mode 100644 index 0000000..ea9ac6c --- /dev/null +++ b/.claude/rules/bash-style.md @@ -0,0 +1,95 @@ +--- +paths: + - "bashdep" + - "**/*.sh" + - "bin/pre-commit" +--- + +# Bash Style & Compatibility Rules + +## Bash 3.2+ Compatibility (Critical) + +`bashdep` must run on **macOS default bash (3.2)** and Linux bash 4+. The +following are allowed because they exist in 3.2: `[[ ]]`, indexed arrays, +`${var:-default}`, `${var/pattern/replacement}`, `$()`. The following are +**prohibited** (Bash 4.0+): + +| Feature | Bash ver | Alternative | +|---------|----------|-------------| +| `declare -A` (associative arrays) | 4.0+ | Parallel indexed arrays or sorted lockfile lines | +| `${var,,}` / `${var^^}` (case) | 4.0+ | `tr '[:upper:]' '[:lower:]'` | +| `${array[-1]}` (negative index) | 4.3+ | `${array[${#array[@]}-1]}` | +| `&>>` (append both streams) | 4.0+ | `>> file 2>&1` | +| `mapfile` / `readarray` | 4.0+ | `while IFS= read -r line; do …; done < file` | +| `${parameter@Q}` (transformations) | 4.4+ | `printf '%q' "$parameter"` | + +When in doubt: assume macOS default bash and avoid the feature. + +## Runtime Dependency Policy + +Library code in `bashdep` may only call: `curl`, `awk`, `mktemp`, `mkdir`, +`rm`, `cp`, `mv`, `printf`, `cat`, `grep`, `sort`, `tr`, `dirname`, +`basename`, `stat`, plus shell builtins. **Do not** introduce `jq`, +`python`, `node`, `xargs --max-args`, GNU-only flags, etc. + +If a feature seems to need a new dependency, propose it in an issue first. + +## Coding Conventions + +- **2 spaces** indent, no tabs (enforced by `.editorconfig` + `make lint`) +- **No trailing whitespace**, file ends with a single newline +- Soft 100-char line cap; break long pipelines across lines with `\` +- Follow [Google Shell Style Guide](https://google.github.io/styleguide/shellguide.html) where it does not conflict with this repo +- Always quote variables unless explicit word splitting is wanted +- Use `$()` for command substitution, never backticks +- Use `printf` over `echo -e` (portable) + +### Naming + +- **Public functions:** `bashdep::function_name` +- **Private functions:** `bashdep::_function_name` (leading underscore after the namespace) +- **Local variables:** `lowercase_with_underscores`, declared `local` +- **Globals / config:** `BASHDEP_UPPERCASE_WITH_UNDERSCORES` + +### Function Header (public functions) + +Document non-trivial public functions with a short header: + +```bash +# Brief description. +# Usage: +# bashdep::name [arg=value] ... +# +# Returns: 0 on success, 1 on validation/IO failure. +function bashdep::name() { + local arg=$1 + ... +} +``` + +### File Structure of `bashdep` + +Constants → globals → private helpers (`bashdep::_*`) → public API +(`bashdep::*`). Do not interleave; readers scan top-to-bottom. + +### Modes + +Every mutating command must honor the four mode globals: + +- `BASHDEP_DRY_RUN=true` — print `[dry-run] Would …` instead of acting +- `BASHDEP_SILENT=true` — suppress informational logs (errors still go to stderr) +- `BASHDEP_VERBOSE=true` — emit additional `bashdep::_vlog` traces +- `BASHDEP_FORCE=true` — bypass idempotence guards (re-download, overwrite) + +Use the existing helpers (`bashdep::is_dry_run`, `bashdep::_log`, +`bashdep::_vlog`) — do not branch on the globals directly. + +## ShellCheck + +All code must pass `make sa`. Repo-wide directives in `Makefile`: +`-e SC1091 -e SC2155`. Add per-line directives sparingly with a reason: + +```bash +# shellcheck disable=SC2034 # used by `eval` in caller +local var=value +``` diff --git a/.claude/rules/tdd-workflow.md b/.claude/rules/tdd-workflow.md new file mode 100644 index 0000000..3dc68d3 --- /dev/null +++ b/.claude/rules/tdd-workflow.md @@ -0,0 +1,88 @@ +--- +paths: + - "bashdep" + - "**/*.sh" + - ".tasks/**/*.md" +--- + +# TDD Workflow + +**Test-Driven Development is mandatory** for behavior changes in `bashdep`. + +## The Cycle + +``` +RED -> Write a failing test (must fail for the RIGHT reason) +GREEN -> Write minimal code to make it pass (nothing extra) +REFACTOR -> Improve code while keeping all tests green +REPEAT -> Until acceptance criteria are met +``` + +Bug fixes: write the regression test first (it must fail without the fix), +then fix. + +## Task File (Recommended for non-trivial changes) + +For features, refactors, or multi-test work, create +`.tasks/YYYY-MM-DD-slug.md`. Skip for one-line bug fixes. + +```markdown +# [Feature/Fix Name] + +**Date:** YYYY-MM-DD **Status:** In Progress + +## Acceptance Criteria +- [ ] Criterion 1 + +## Test Inventory +- [ ] `test_bashdep__` +- [ ] `test_bashdep__` + +## Current Red Bar +Test: (none yet) + +## Logbook +### YYYY-MM-DD HH:MM +- Created task, analyzed existing code +``` + +## RED Phase + +1. Pick the **smallest next test** from inventory +2. Read the surrounding tests in `tests/unit/bashdep_test.sh` to match style: + - Pure-logic tests at the top use `BASHDEP_*` globals + `bashdep::_*` helpers + - Filesystem tests use `$TEST_DIR` + `_seed_lock` / `_seed_installed` +3. Write the test following Arrange-Act-Assert +4. Run: `lib/bashunit tests --filter ` — **must fail** +5. Verify the failure is for the RIGHT reason (assertion mismatch, not + "command not found" or syntax error) + +## GREEN Phase + +1. Write **minimal** code in `bashdep` to pass — no extra features +2. Run: `lib/bashunit tests --filter ` — **must pass** +3. Run the full suite: `make test` — nothing else regressed + +## REFACTOR Phase + +1. Improve readability, naming, extract duplication — **no behavior changes** +2. Run `make test` after each change +3. Run quality checks: `make sa && make lint` + +## Quality Gate (Before Commit) + +```bash +make pre_commit/run # test + sa + lint +``` + +If `pre_commit/install` was run, the hook does this on every commit. + +## Definition of Done + +- All tests green for the **right reason** +- All acceptance criteria met +- Quality gate passes (`make pre_commit/run`) +- Bash 3.2+ compatible +- `CHANGELOG.md` `## Unreleased` updated (if user-facing) +- `docs/api.md` updated (if public API changed) +- `docs/behavior.md` updated (if lockfile / dev-dep / error semantics changed) diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md new file mode 100644 index 0000000..1f7d499 --- /dev/null +++ b/.claude/rules/testing.md @@ -0,0 +1,126 @@ +--- +paths: + - "tests/**/*_test.sh" + - "tests/bootstrap.sh" +--- + +# Testing Guidelines + +## Organization + +bashdep currently has a single test layer: + +| Directory | Purpose | Pattern | +|-----------|---------|---------| +| `tests/unit/` | Behavior tests for `bashdep::*` functions | Pure-logic on top, filesystem-using below | +| `tests/unit/snapshots/` | Captured stdout for snapshot assertions | Hardcoded `/tmp/test_` paths | + +**Naming:** Files end with `_test.sh`. Functions: +`test_bashdep__`. + +Pure-logic tests live above filesystem tests in +`tests/unit/bashdep_test.sh`. Keep that ordering when adding new tests. + +## Common bashunit Assertions + +```bash +assert_equals "expected" "$actual" +assert_not_equals "not_this" "$actual" +assert_contains "substring" "$haystack" +assert_not_contains "substring" "$haystack" +assert_matches "regex" "$string" +assert_empty "$var" +assert_not_empty "$var" +assert_successful_code "$?" +assert_general_error "$?" +assert_file_exists "$path" +assert_file_not_exists "$path" +assert_directory_exists "$path" +assert_match_snapshot "$output" +``` + +## Lifecycle Hooks (already wired in `tests/unit/bashdep_test.sh`) + +```bash +function set_up() { + source "$(current_dir)/../../bashdep" + BASHDEP_FORCE=false + BASHDEP_SILENT=false + BASHDEP_DRY_RUN=false + BASHDEP_VERBOSE=false + TEST_DIR=$(mktemp -d) +} + +function tear_down() { + if [[ -n "$TEST_DIR" && -d "$TEST_DIR" ]]; then + rm -rf "$TEST_DIR" + fi +} +``` + +Always reset `BASHDEP_*` globals in `set_up` so tests don't leak state. + +## Test Fixtures + +Two helpers at the top of `tests/unit/bashdep_test.sh`: + +```bash +# Lockfile entry only. +_seed_lock "$TEST_DIR" "file_name" "https://example.com/url" + +# Lockfile entry + the on-disk file. +_seed_installed "$TEST_DIR" "file_name" "https://example.com/url" +``` + +Use these instead of writing lockfile lines by hand — they keep the +on-disk format consistent. + +## Snapshot Tests + +Snapshot tests assert that stdout matches `tests/unit/snapshots/`. +Update with: + +```bash +lib/bashunit --update-snapshots tests/ +``` + +**Important:** snapshot tests embed the temp path into the snapshot, so +they keep hardcoded `/tmp/test_` paths. Do **not** migrate them to +`$TEST_DIR`. + +## Test Isolation + +- Use `$TEST_DIR` (per-test `mktemp -d`) for filesystem tests +- No shared global state between tests — always reset `BASHDEP_*` in `set_up` +- **No network calls** — mock or stub `curl`. Set + `BASHDEP_DRY_RUN=true` if you only need to assert what *would* happen +- No real `release.sh` invocations from tests +- Tests must be safe to run in any order + +## Mocking `curl` + +bashdep's only network surface is `curl`. To test install paths without +hitting the network, either: + +1. Set `BASHDEP_DRY_RUN=true` and assert on the `[dry-run] Would …` log, or +2. Define a `curl` shell function in the test that writes a fixture file + and returns 0: + +```bash +function curl() { + while [[ $# -gt 0 ]]; do + case "$1" in + -o) printf 'fixture\n' > "$2"; shift 2 ;; + *) shift ;; + esac + done + return 0 +} +``` + +## Reference Tests + +- **Pure logic:** top of `tests/unit/bashdep_test.sh` + (`test_bashdep_classify_dep_*`) +- **Filesystem:** `_seed_installed` / `_seed_lock`-based tests further down +- **Snapshot:** `test_bashdep_*_snapshot` tests diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..223f7a7 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "includeCoAuthoredBy": false, + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "file=\"$CLAUDE_FILE_PATH\"; if [ -n \"$file\" ] && { echo \"$file\" | grep -qE '\\.sh$|/bashdep$|/pre-commit$'; }; then shellcheck -e SC1091 -e SC2155 -x \"$file\" 2>&1 | head -20; fi" + } + ] + } + ] + } +} diff --git a/.claude/skills/add-command/SKILL.md b/.claude/skills/add-command/SKILL.md new file mode 100644 index 0000000..f6a95f5 --- /dev/null +++ b/.claude/skills/add-command/SKILL.md @@ -0,0 +1,81 @@ +--- +name: add-command +description: Add a new bashdep:: lifecycle command with TDD +allowed-tools: Read, Edit, Write, Bash, Grep, Glob +--- + +# Add Command Skill + +Add a new public `bashdep::` lifecycle command (sibling of `install`, +`uninstall`, `clean`, `doctor`, `list`, `self_update`) following strict TDD. + +## Prerequisites + +1. **Task file** — create `.tasks/YYYY-MM-DD-add-.md` +2. **Read the existing commands** — `bashdep` is a single file; scan how + `bashdep::clean`, `bashdep::doctor`, `bashdep::uninstall` are + structured to match style +3. **Read the API doc** — `docs/api.md` defines the public contract; the + new command's signature must fit the same shape + +## Workflow + +### 1. Plan + +Decide with the user: +- Command name (`bashdep::`) +- Arguments (positional vs `key=value` like `bashdep::setup`) +- Mutating? If yes, must honor `BASHDEP_DRY_RUN`, `BASHDEP_SILENT`, + `BASHDEP_VERBOSE`, `BASHDEP_FORCE` +- Lockfile interaction (read / write / both / none) +- Exit codes (0 success, 1 validation/IO failure) +- Logging surface (`bashdep::_log` / `bashdep::_vlog`) + +Document acceptance criteria + test inventory in the task file. + +### 2. Study Existing Patterns + +Read the closest sibling command in `bashdep` and its tests in +`tests/unit/bashdep_test.sh`. Reuse helpers: `bashdep::_classify_dep`, +`bashdep::_lock_get`, `bashdep::_lock_remove`, `bashdep::is_dry_run`. + +### 3. TDD Cycles + +For each test in the inventory, follow RED → GREEN → REFACTOR: + +1. **Happy path** — pure-logic test if possible (no filesystem) +2. **Filesystem path** — use `$TEST_DIR` + `_seed_*` helpers +3. **Mode coverage** — one test each for `dry-run`, `silent`, `verbose`, + `force` if the command is mutating +4. **Failure modes** — bad arguments, missing lockfile, IO error +5. **Snapshot test** for any new user-visible output + +### 4. Integration + +- Add the public function near the bottom of `bashdep` (after the + existing public commands, before any trailing CLI dispatch) +- Add a CLI dispatch case if `bashdep` is invoked as an executable for + this command +- Run full suite: `make test` +- Quality gate: `make pre_commit/run` + +### 5. Documentation + +- Add the function to `docs/api.md` with usage, arguments, return codes, + and a worked example +- Update `docs/behavior.md` if the command introduces new lockfile or + filesystem semantics +- Update `CHANGELOG.md` `## Unreleased` under `### Added` +- Update the public-API table in `.claude/CLAUDE.md` +- Update `README.md` Quick Start if the command is part of the typical + workflow + +## Final Checklist + +- [ ] All tests passing (happy path, failure modes, mode flags, snapshot) +- [ ] Function namespaced `bashdep::` and documented in `docs/api.md` +- [ ] Mutating? Modes honored (`dry-run` / `silent` / `verbose` / `force`) +- [ ] Quality gate passes (`make pre_commit/run`) +- [ ] Bash 3.2+ compatible +- [ ] `CHANGELOG.md` updated +- [ ] Task file completed diff --git a/.claude/skills/commit/SKILL.md b/.claude/skills/commit/SKILL.md new file mode 100644 index 0000000..1629d00 --- /dev/null +++ b/.claude/skills/commit/SKILL.md @@ -0,0 +1,104 @@ +--- +name: commit +description: Stage and commit changes using conventional commits format +user-invocable: true +argument-hint: "[message hint]" +disable-model-invocation: true +allowed-tools: Bash, Read, Grep, Glob +--- + +# Commit with Conventional Commits + +Stage and commit current changes using the conventional commits format. + +> **IMPORTANT:** This skill MUST be used for ALL commits — even when the +> user just says "commit" without `/commit`. Never commit without +> following these steps. + +## Current State +- Branch: !`git branch --show-current` +- Status: !`git status --short` +- Staged diff: !`git diff --cached --stat 2>/dev/null` +- Unstaged diff: !`git diff --stat 2>/dev/null` +- Recent commits: !`git log --oneline -5 2>/dev/null` + +## Arguments +- `$ARGUMENTS` — Optional hint for the commit message + (e.g., `clean orphan files race`) + +## Instructions + +1. **Review the state above** — understand what changed and why. + +2. **Stage files** — add only the relevant changed files by name. Never + use `git add -A` or `git add .`. Never stage files that contain + secrets (`.env`, credentials, `lib/` artifacts, `local/`, `tmp/`). + +3. **Determine the commit type** from the nature of the changes: + - `feat` — new feature or capability + - `fix` — bug fix + - `docs` — documentation only + - `style` — formatting, whitespace (no logic change) + - `refactor` — code restructuring (no behavior change) + - `test` — adding or updating tests + - `chore` — maintenance, tooling, config + - `perf` — performance improvement + - `ci` — CI/CD config (`.github/workflows/`) + +4. **Determine the scope** from the area of the codebase affected: + - `install` — `bashdep::install` / `install_from` + - `lock` — lockfile read/write helpers + - `doctor` — `bashdep::doctor` + - `clean` — `bashdep::clean` + - `uninstall` — `bashdep::uninstall` + - `setup` — `bashdep::setup` + - `cli` — CLI dispatch / flags + - `release` — `release.sh` and release tooling + - `docs` — `docs/`, `README.md` + - `ci` — `.github/workflows/` + - Use the most specific scope that fits. Omit if changes span many areas. + +5. **Write the commit message**: + - Format: `(): ` + - Description: imperative mood, lowercase, no period, under 70 chars + - Focus on **why**, not what + - Add a body (separated by blank line) only when the why isn't + obvious from the description + - **Never mention AI, Claude, or automation** in the message + - **Never** add a `Co-Authored-By` trailer (repo + `settings.json` sets `includeCoAuthoredBy: false`) + +6. **Create the commit**: + ```bash + git commit -m "$(cat <<'EOF' + (): + + + EOF + )" + ``` + +7. **Verify** the commit was created: + ```bash + git log --oneline -1 + ``` + +## Examples + +``` +feat(doctor): report orphan files in dev dir +fix(lock): preserve trailing newline on lockfile rewrite +test(clean): cover dry-run output for orphan removal +refactor(install): extract _classify_dep helper +docs(api): document bashdep::self_update target_path arg +chore(ci): bump shellcheck to v0.10 +perf(install): skip re-download when lock + file are in sync +``` + +## Rules + +- **One logical change per commit** — don't mix unrelated changes +- **Never use `--no-verify`** — if hooks fail, fix the underlying issue +- **Never amend** unless the user explicitly asks +- **Always create a NEW commit** — even after a hook failure +- **Author**: use the git config identity (never commit as default user) diff --git a/.claude/skills/doctor-check/SKILL.md b/.claude/skills/doctor-check/SKILL.md new file mode 100644 index 0000000..d0a8e9b --- /dev/null +++ b/.claude/skills/doctor-check/SKILL.md @@ -0,0 +1,87 @@ +--- +name: doctor-check +description: Coverage gap analysis for the bashdep public API plus a bashdep::doctor audit +allowed-tools: Read, Bash, Grep, Glob +--- + +# Doctor Check + +Two-part health check: + +1. **Test coverage** — every public `bashdep::*` command must have tests + for its happy path, mode flags, and failure modes. +2. **Runtime invariants** — `bashdep::doctor` itself reports lockfile vs + filesystem inconsistencies; verify it covers every relevant directory. + +## Workflow + +### 1. Enumerate the Public API + +```bash +grep -nE '^function bashdep::[a-z]' bashdep | grep -v '::_' +``` + +That list is the source of truth for what needs coverage. + +### 2. Map Functions to Tests + +For each public function: + +```bash +grep -nE "bashdep::\b" tests/unit/bashdep_test.sh +``` + +For each, check coverage of: +- Happy path +- `BASHDEP_DRY_RUN=true` +- `BASHDEP_SILENT=true` +- `BASHDEP_VERBOSE=true` +- `BASHDEP_FORCE=true` (when relevant) +- At least one failure mode (bad arg, missing file, IO error) + +### 3. Categorize Coverage + +- **Well tested** — happy path + mode flags + ≥1 failure mode +- **Partially tested** — happy path only +- **Not tested** — no test references the function + +### 4. Identify Critical Gaps + +**Priority 1:** Any public `bashdep::*` function with zero tests +**Priority 2:** Mutating commands missing `dry-run` coverage (silently +breaking the contract is the easiest regression to ship) +**Priority 3:** Failure paths (`return 1`) with no test +**Priority 4:** Snapshot drift — re-run the suite; if any snapshot +mismatch appears, decide drift vs regression + +### 5. Run the Runtime Doctor + +In the demo workspace (`example/`) or a scratch dir: + +```bash +( cd example && bash demo.sh ) +# Then, from the same dir: +source bashdep && bashdep::doctor +``` + +Confirm `bashdep::doctor`: +- Reports `OK` when lockfile and dir match +- Reports `missing file` when the lockfile entry has no on-disk file +- Reports `orphan file` when a file in the dir has no lockfile entry +- Honors `BASHDEP_SILENT` (errors still on stderr) + +### 6. Generate Report + +Output a markdown report with: +- Summary (total public functions, tested, % with mode coverage) +- Coverage table by function (✅ happy / 🟡 partial / ❌ none) +- Critical gaps with line references in `bashdep` +- Recommended new tests with proposed function names + +## Coverage Goals + +- 100% of public `bashdep::*` functions have ≥1 test +- 100% of mutating commands have `dry-run` coverage +- ≥1 failure-mode test per public function +- `bashdep::doctor` exercised in the test suite for: missing file, + orphan file, OK case diff --git a/.claude/skills/fix-test/SKILL.md b/.claude/skills/fix-test/SKILL.md new file mode 100644 index 0000000..81924e7 --- /dev/null +++ b/.claude/skills/fix-test/SKILL.md @@ -0,0 +1,70 @@ +--- +name: fix-test +description: Debug and fix failing bashdep tests systematically +allowed-tools: Read, Edit, Bash, Grep, Glob +--- + +# Fix Test + +Systematically debug and fix failing test(s) in `tests/unit/bashdep_test.sh`. + +## Workflow + +### 1. Identify Failures + +```bash +make test 2>&1 +# Or, narrower: +lib/bashunit tests --filter +``` + +Parse: which test functions fail, what assertions, what the actual vs +expected was. + +### 2. Categorize Each Failure + +- **Test bug** — wrong expected value, missing `set_up` reset, leaky state +- **Implementation bug** — `bashdep` doesn't match expected behavior +- **Snapshot drift** — output legitimately changed; snapshot needs updating +- **Environment issue** — missing `$TEST_DIR`, hardcoded path that doesn't + exist, missing fixture +- **Bash 3.2 compat regression** — code uses a Bash 4+ feature +- **Mode-flag leak** — `BASHDEP_*` global from a prior test wasn't reset + +### 3. Fix + +- **Test bug:** correct the assertion, fixture, or `set_up` +- **Implementation bug:** minimal fix in `bashdep`, follow TDD + (regression test must already exist or be added now) +- **Snapshot drift:** confirm the new output is correct, then + `lib/bashunit --update-snapshots tests/`. Review the diff before committing +- **Environment:** fix the fixture path; never hardcode `/tmp/...` outside + snapshot tests +- **Bash 3.2 compat:** swap to a 3.2-safe construct + (see `.claude/rules/bash-style.md`) +- **Mode-flag leak:** add the reset to `set_up`, or scope the change to + the single test + +### 4. Verify + +```bash +lib/bashunit tests --filter # Specific test +make test # Full suite +make sa && make lint # Static checks +``` + +### 5. Prevent Regression + +- Document the root cause in the commit message body when non-obvious +- Add an edge-case test if the failure exposed a missing case +- If the bug crossed mode-flag boundaries, add tests for each mode + (`dry-run`, `silent`, `verbose`, `force`) + +## Debugging Tips + +- Use `--filter "test_name"` to run a single test in isolation +- Add `printf 'DEBUG: %s\n' "$var" >&2` temporarily — strip before commit +- If a test passes alone but fails in the suite, suspect a leaked + `BASHDEP_*` global; reset in `set_up` +- For snapshot mismatches, diff the snapshot file vs current stdout + before regenerating diff --git a/.claude/skills/gh-issue/SKILL.md b/.claude/skills/gh-issue/SKILL.md new file mode 100644 index 0000000..3aa42e1 --- /dev/null +++ b/.claude/skills/gh-issue/SKILL.md @@ -0,0 +1,125 @@ +--- +name: gh-issue +description: Fetch a GitHub issue, create a branch, plan and implement with TDD, then open a PR +user-invocable: true +argument-hint: "" +allowed-tools: Bash, Read, Edit, Write, Grep, Glob, Agent, WebFetch +--- + +# GitHub Issue Workflow + +Fetch a GitHub issue, create a branch, implement following TDD, and open +a PR. + +## Arguments +- `$ARGUMENTS` — Issue number (e.g., `42` or `#42`) + +## Instructions + +### Phase 1: Setup + +1. **Parse the issue number** from `$ARGUMENTS` (strip `#` if present). + +2. **Fetch issue details**: + ```bash + gh issue view --json title,body,labels,assignees,milestone,state + ``` + +3. **Assign yourself if unassigned**: + ```bash + gh issue edit --add-assignee @me + ``` + +4. **Create a branch** from `main`: + + Determine prefix from labels: + - `bug` → `fix/` + - `enhancement` → `feat/` + - `documentation` → `docs/` + - Default → `feat/` + + Branch name: `-` (slug: lowercase title, + spaces → `-`, max 50 chars). + + ```bash + git checkout main && git pull + git checkout -b + ``` + +### Phase 2: Plan + +5. **Analyze the issue**: requirements, labels, referenced issues, + affected areas (which `bashdep::` function or doc). + +6. **Explore the codebase** for context: + - Find the relevant section in `bashdep` (single file) + - Find the closest related tests in `tests/unit/bashdep_test.sh` + - Read the corresponding entry in `docs/api.md` or `docs/behavior.md` + +7. **Create implementation plan**: + - Acceptance Criteria + - Test Strategy (which tests to write first — pure-logic vs + filesystem vs snapshot) + - Files to Change (`bashdep`, `tests/unit/bashdep_test.sh`, + `docs/*.md`, `CHANGELOG.md`) + - Implementation Order (smallest first step) + +8. **Use EnterPlanMode** if implementation is non-trivial. + +### Phase 3: Implement + +9. **Follow strict TDD workflow** (see `.claude/rules/tdd-workflow.md`): + + For each test: + - **RED** — Write failing test, verify it fails for the RIGHT reason + - **GREEN** — Minimal code in `bashdep` to pass + - **REFACTOR** — Improve while keeping tests green + +10. **Run the suite frequently**: + ```bash + make test + ``` + +11. **Quality checks** after each refactor: + ```bash + make sa && make lint + ``` + +12. **Mode coverage** — if the change is mutating, add tests for + `BASHDEP_DRY_RUN`, `BASHDEP_SILENT`, `BASHDEP_VERBOSE`, + `BASHDEP_FORCE` as relevant. + +### Phase 4: Ship + +13. **Final verification**: + ```bash + make pre_commit/run + ``` + +14. **Commit** using the `/commit` skill, with a body referencing the + issue (e.g., `Closes #`). + +15. **Create PR** using the `/pr` skill: + ``` + /pr # + ``` + +## Output Format + +After fetching, present: + +``` +## Issue #: + +**Labels:** <labels> +**State:** <state> +**Branch:** <branch-name> + +### Description +<body content> + +### Next Steps +1. Explore `bashdep` for the relevant function +2. Create implementation plan +3. Begin TDD cycle +``` diff --git a/.claude/skills/pr/SKILL.md b/.claude/skills/pr/SKILL.md new file mode 100644 index 0000000..2246685 --- /dev/null +++ b/.claude/skills/pr/SKILL.md @@ -0,0 +1,123 @@ +--- +name: pr +description: Push branch and create a GitHub PR following the bashdep PR template +user-invocable: true +argument-hint: "[#issue]" +disable-model-invocation: true +allowed-tools: Bash, Read, Grep, Glob +--- + +# Create Pull Request + +Push the current branch and open a PR using the project's PR template. + +> **IMPORTANT:** This skill MUST be used for ALL pull request creation — +> even when the user just says "create pr" without `/pr`. Never create a +> PR without following these steps. + +## Current Branch Context +- Branch: !`git branch --show-current` +- Commits: !`git log main..HEAD --oneline 2>/dev/null` +- Changed files: !`git diff main..HEAD --stat 2>/dev/null` + +## Arguments +- `$ARGUMENTS` — Issue reference (optional, e.g., `#42` or `42`). If + provided, the PR Background section links to it. + +## Instructions + +1. **Review the branch context above** — commits and changed files are + already loaded. + +2. **MANDATORY: Update `CHANGELOG.md`** — Read it and check the + `## Unreleased` section. If the changes from this branch aren't + already listed there, you MUST update it before pushing. **Do NOT + skip. Do NOT proceed without verifying.** + - Add entries under the appropriate subsection + (`### Added`, `### Changed`, `### Fixed`, `### Removed`) + - Reference the issue number where applicable (e.g., `(#123)`) + - Commit the update: + ```bash + git add CHANGELOG.md && git commit -m "docs: update changelog" + ``` + +3. **Verify quality gate locally before pushing**: + ```bash + make pre_commit/run + ``` + The pre-commit hook (if installed) runs the same checks. If it fails, + read the output, fix the underlying issue, commit the fix, then retry. + **Do NOT use `--no-verify` to bypass.** + +4. **Push branch**: + ```bash + git push -u origin HEAD + ``` + +5. **Generate PR title**: + - If `$ARGUMENTS` contains an issue number, fetch the issue title: + ```bash + gh issue view <number> --json title -q '.title' + ``` + - Format: `<type>(<scope>): <short description>` (conventional commit + style, under 70 chars) + - Derive the type from the branch prefix + (`feat/` → feat, `fix/` → fix, `docs/` → docs) + +6. **Create PR** using the structure from + `.github/PULL_REQUEST_TEMPLATE.md`: + + ```bash + gh pr create --title "<title>" --assignee @me --body "$(cat <<'EOF' + ### 🔗 Ticket + + <Related #<issue-number>, or "n/a"> + + ## 🤔 Background + + <1-2 sentences: motivation and context for the changes> + + ## 💡 Goal + + <One sentence: the outcome this PR delivers> + + ## 🔖 Changes + + - <bullet 1: what changed and why> + - <bullet 2> + - <bullet 3> (optional) + + ## 🖼️ Screenshots + + n/a + EOF + )" + ``` + + **MANDATORY:** Always follow the template structure + (`🔗 Ticket` → `🤔 Background` → `💡 Goal` → `🔖 Changes` → + `🖼️ Screenshots`). Never use a different format. + + **Assignee:** Always assign to `@me`. + + **Issue linking:** Use `Related #<n>` in the Ticket section. + `Closes #<n>` / `Fixes #<n>` are fine in commit bodies if the user + wants auto-close, but keep the PR template's prose form. + + **Body guidelines:** + - Background: 1-2 sentences of context (the why) + - Goal: a single sentence framing the outcome + - Changes: 2-4 short bullets focused on **what + why** + - **No file lists, no code snippets, no class names** in the body + - Drop the `🖼️ Screenshots` section content for non-UI changes + (replace with `n/a`) + +7. **Report the PR URL** to the user. + +## Example Usage + +``` +/pr +/pr #42 +/pr 15 +``` diff --git a/.claude/skills/pre-release/SKILL.md b/.claude/skills/pre-release/SKILL.md new file mode 100644 index 0000000..2c85fe4 --- /dev/null +++ b/.claude/skills/pre-release/SKILL.md @@ -0,0 +1,115 @@ +--- +name: pre-release +description: Run a comprehensive pre-release validation checklist for bashdep +allowed-tools: Read, Bash, Grep, Glob +--- + +# Pre-Release Validation + +Run all checks before releasing a new bashdep version. + +## Checklist + +### 1. Version Consistency + +```bash +grep -n 'BASHDEP_VERSION="' bashdep +awk '/^## \[/{print; exit}' CHANGELOG.md +git tag --list --sort=-v:refname | head -3 +``` + +`bashdep`'s `BASHDEP_VERSION` is the source of truth. The release script +bumps it; verify it matches the intended target version. + +### 2. Full Test Suite + +```bash +make test +``` + +Run 3-5 times to catch flaky tests. All must pass. + +### 3. Static Analysis & Lint + +```bash +make sa # ShellCheck — zero warnings +make lint # editorconfig-checker — clean +``` + +### 4. Documentation + +- `CHANGELOG.md` `## [Unreleased]` section is complete and reflects + every user-visible change since the last tag +- `docs/api.md` reflects every public `bashdep::*` function +- `docs/behavior.md` reflects current lockfile / dev-dep / error semantics +- `docs/releasing.md` is current +- `README.md` examples copy-paste cleanly +- No remaining `TODO` / `FIXME` in docs + +### 5. Bash 3.2+ Compatibility (must return no results) + +```bash +grep -nE 'declare -A' bashdep +grep -nE '\$\{[A-Za-z_][A-Za-z0-9_]*,,\}' bashdep +grep -nE '\$\{[A-Za-z_][A-Za-z0-9_]*\^\^\}' bashdep +grep -nE '\bmapfile\b|\breadarray\b' bashdep +grep -nE '&>>' bashdep +grep -nE '\$\{[A-Za-z_][A-Za-z0-9_]*\[-1\]\}' bashdep +``` + +### 6. Runtime Dependency Audit + +```bash +grep -nwE 'jq|python|node|yq' bashdep || echo "OK" +``` + +Library code must only call `curl` / `awk` / `mktemp` / POSIX builtins. + +### 7. Mode-Flag Coverage + +Every public mutating command honors `BASHDEP_DRY_RUN`, +`BASHDEP_SILENT`, `BASHDEP_VERBOSE`, `BASHDEP_FORCE`. Spot-check each +in the test suite: + +```bash +grep -nE 'BASHDEP_(DRY_RUN|SILENT|VERBOSE|FORCE)=true' tests/unit/bashdep_test.sh +``` + +### 8. Release Script Dry Run + +```bash +./release.sh --dry-run +``` + +Verify: +- Version bump target matches expectation +- CHANGELOG roll preview is correct +- Compare links at the bottom of CHANGELOG would update correctly + +### 9. Breaking Changes + +Review `git log <last-tag>..HEAD` for any breaking changes. Each must be +called out in `CHANGELOG.md` under `### Changed` or `### Removed` with a +brief migration note. + +### 10. Git & CI Status + +- Working tree clean (`git status --short` empty) +- On `main` +- All CI checks green: `gh run list --limit 5 --branch main` +- Target tag does not yet exist: + `git tag --list <X.Y.Z>` returns empty + +### 11. Smoke Test + +```bash +( cd "$(mktemp -d)" \ + && cp /path/to/bashdep . \ + && source ./bashdep \ + && bashdep::version ) +``` + +## Output + +Report each check as ✅ pass / ❌ fail / ⚠️ warning. Only proceed to +`/release` when all checks are ✅. diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md new file mode 100644 index 0000000..e738b2c --- /dev/null +++ b/.claude/skills/release/SKILL.md @@ -0,0 +1,120 @@ +--- +name: release +description: Run pre-release validation and execute the release process via release.sh +user-invocable: true +argument-hint: "[version|--major|--minor|--patch]" +allowed-tools: Bash, Read, Grep, Glob +--- + +# Release + +Run pre-release checks then cut a new bashdep release via `release.sh`. + +## Arguments + +- `$ARGUMENTS` — Optional. Either an explicit version (`0.5.0`) or a + bump flag (`--major`, `--minor`, `--patch`). Default: + `release.sh` auto-bumps the **minor** version. + +## Current State + +- Current version: !`grep -o 'BASHDEP_VERSION="[^"]*"' bashdep | cut -d'"' -f2` +- Branch: !`git branch --show-current` +- Working tree: !`git status --short` +- Unreleased changes: !`awk '/^## \[Unreleased\]$/,/^## \[/' CHANGELOG.md | head -40` + +## Instructions + +### 1. Pre-flight validation + +Run `/pre-release` first, or replay the critical checks here: + +```bash +make test +make sa +make lint +grep -nE 'declare -A|mapfile|\$\{[A-Za-z_][A-Za-z0-9_]*,,\}' bashdep || echo "Bash 3.2+: OK" +gh run list --limit 3 --branch main +git status --short +git branch --show-current +``` + +If ANY check fails, **stop and report**. Do NOT proceed to release. + +### 2. Confirm with user + +Show a summary: +- Current version → target version (resolved from `$ARGUMENTS`) +- Key entries from `CHANGELOG.md` `## [Unreleased]` (abbreviated) +- All checks passed + +Ask the user to confirm before running `release.sh`. + +### 3. Dry-run the release + +Always preview first — `release.sh` is idempotent in dry-run mode: + +```bash +./release.sh $ARGUMENTS --dry-run +``` + +Show the user the proposed: +- Version bump +- CHANGELOG diff (renamed `[Unreleased]` section + new compare links) +- Tag name + +Ask for explicit confirmation before the real run. + +### 4. Execute release + +```bash +./release.sh $ARGUMENTS +``` + +`release.sh` will: +1. Validate the version is semver and greater than current +2. Confirm clean `main`, tag does not exist +3. Bump `BASHDEP_VERSION` in `bashdep` +4. Roll `CHANGELOG.md`: rename `[Unreleased]` → `[X.Y.Z] - YYYY-MM-DD` + and refresh compare links +5. Run `make test sa lint` as a release gate +6. Commit `chore(release): X.Y.Z` and tag `X.Y.Z` +7. Push commit + tag to the remote +8. Create the GitHub release via `gh release create` and attach the + `bashdep` script as the downloadable asset + +The release URL is printed at the end. + +### 5. Post-release verification + +```bash +git log --oneline -1 +git tag --list --sort=-v:refname | head -1 +gh release view "$(git tag --list --sort=-v:refname | head -1)" +``` + +Verify: +- Latest commit is `chore(release): X.Y.Z` +- Latest tag matches the target version +- GitHub release page lists the `bashdep` asset + +Report the release URL to the user. + +## Useful flags + +``` +./release.sh --dry-run # preview, no changes +./release.sh --patch # auto-bump patch +./release.sh --major # auto-bump major +./release.sh 0.5.0 # explicit version +./release.sh --no-gh # skip GitHub release (still push tag) +./release.sh --force # skip "Release X.Y.Z?" prompt (CI) +``` + +## Example Usage + +``` +/release +/release --patch +/release 0.5.0 +``` diff --git a/.claude/skills/tdd-cycle/SKILL.md b/.claude/skills/tdd-cycle/SKILL.md new file mode 100644 index 0000000..4afbd69 --- /dev/null +++ b/.claude/skills/tdd-cycle/SKILL.md @@ -0,0 +1,56 @@ +--- +name: tdd-cycle +description: Run a complete RED -> GREEN -> REFACTOR cycle for a single test +allowed-tools: Read, Edit, Write, Bash, Grep, Glob +--- + +# TDD Cycle + +Execute a complete RED -> GREEN -> REFACTOR cycle for one test. + +## Workflow + +### 1. Verify Task File + +Check for `.tasks/YYYY-MM-DD-*.md`. If missing for a non-trivial change, +create one before proceeding. + +### 2. RED — Write Failing Test + +1. Show the test inventory from the task file; ask which test to implement +2. Read the surrounding tests in `tests/unit/bashdep_test.sh` to match style: + - Pure-logic test (no filesystem) → add near the top + - Filesystem test → add near the bottom; use `$TEST_DIR` and the + `_seed_lock` / `_seed_installed` helpers +3. Reset `BASHDEP_*` globals in the test body if it depends on a mode flag +4. Run: `lib/bashunit tests --filter <test_name>` — **must fail** +5. Verify the failure is for the RIGHT reason (assertion mismatch, not + syntax error or missing function) +6. Update the task file: current red bar + logbook entry + +### 3. GREEN — Make It Pass + +1. Write **minimal** code in `bashdep` — only enough to satisfy THIS test +2. Run: `lib/bashunit tests --filter <test_name>` — **must pass** +3. Run the full suite: `make test` — nothing else regressed +4. Update the task file: tick the test + logbook entry + +### 4. REFACTOR — Improve Code + +1. Improve readability, naming, extract duplication — no behavior changes +2. Run `make test` after each change +3. Quality checks: `make sa && make lint` +4. Update the task file with refactoring notes + +### 5. Next Test + +Show the updated test inventory. Ask whether to continue or stop. + +## Critical Rules + +- **Never skip RED** — always verify the test fails first +- **Minimal code in GREEN** — resist scope creep +- **All tests green during REFACTOR** — revert immediately if broken +- **Bash 3.2+** — see `.claude/rules/bash-style.md` +- **Honor modes** — any new mutating code must respect + `BASHDEP_DRY_RUN`, `BASHDEP_SILENT`, `BASHDEP_VERBOSE`, `BASHDEP_FORCE`