Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 206 additions & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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_<name>` 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 <N>` | 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/): `<type>(<scope>): <description>`

**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)
95 changes: 95 additions & 0 deletions .claude/rules/bash-style.md
Original file line number Diff line number Diff line change
@@ -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
```
88 changes: 88 additions & 0 deletions .claude/rules/tdd-workflow.md
Original file line number Diff line number Diff line change
@@ -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_<thing>_<behavior>`
- [ ] `test_bashdep_<thing>_<failure_mode>`

## 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 <test_name>` β€” **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 <test_name>` β€” **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)
Loading
Loading