# Module Patterns A decision guide for what kind of module you are building and how it should connect to the others. --- ## 1. Module type | Module type | Has `api/` | Has `Taskfile.yml` | Example | |---|:---:|:---:|---| | Support-only library | No | No | `loggers` | | Shared foundation with public commands | Yes | Optional / no | `base` | | Feature module | Yes | Usually yes | `kube`, `github`, `data-migration` | | Wrapper around an external CLI | Yes | Usually yes | `helm`, `openshift`, `github` | | Lifecycle hook | Yes | No | `devbox` (an `on-init-hook`) | A **lifecycle hook** module is invoked by external tooling at a fixed moment — a devbox init hook, a container entrypoint, a git hook — rather than by an operator. It keeps `api/` because the tool calls that path directly, and skips `Taskfile.yml` because nobody runs it by hand. Its implementation commonly branches per platform (`-install-dependencies-osx.sh`, `-install-dependencies-linux-ubuntu.sh`), one file per branch, sourced from `--index-api.sh` like any other implementation file. Pick the type before creating any file — it decides whether `api/` and `Taskfile.yml` exist at all, and adding them later means reworking every consumer that has already started sourcing the module. A module with no user-facing command is a support library. Do not give it an `api/` directory speculatively; every file in `api/` is a public interface someone may come to depend on. --- ## 2. Dependency sourcing | You need | Source | |---|---| | Logging | `./.scripts/loggers/lib/--index-api.sh` | | Shared base functions | `./.scripts/base/lib/--index-api.sh` | | Another module's exported functions | that module's `lib/--index-api.sh` | | Your own module's full runtime, from its `api/` wrapper | your `lib/--index.sh` | The rule underneath: **`--index-api.sh` is the public edge between modules; `--index.sh` is a module's own private entrypoint.** Sourcing another module's `--index.sh` runs its reader and validator inside your command — importing its defaults, its required-input checks, and in `base`'s case its `.env` loading. Symptoms show up as variables nobody set, or a validation failure naming a module the user never invoked. --- ## 3. Command exposure Three independent decisions, in increasing order of commitment: 1. **`lib/` only** — internal. Reachable from other modules that source your `--index-api.sh`. 2. **`api/.sh`** — user-facing. Someone can run it directly from the repository root. 3. **Module `Taskfile.yml`** — discoverable through `task --list-all`. 4. **Root `Taskfile.yml` include** — a stable public namespace, effectively permanent. Escalate only as far as the command genuinely needs. Each step is easy to add later and disruptive to withdraw, because each one is a promise someone may already be relying on. --- ## 4. Common mistakes - Putting full implementation logic into `api/*.sh`. - Adding a `lib/-*.sh` file and forgetting to source it from `lib/--index-api.sh`. - Parsing CLI flags in implementation files instead of `--env-vars-reader.sh`. - Printing secrets in logs. - Sourcing `base/lib/--index.sh` from a library function and unexpectedly loading `.env`. - Adding root Taskfile includes for experimental modules. - Giving a support-only library an `api/` directory it does not need. - Mixing function prefixes inside one module after a rename.