Fold two overlapping local skills (scripts-shell, shell-scripts-module) into this one, keeping only what it did not already cover. - Add references/environment-model.md: the three-layer .env selector model, why set -a dot-sourcing beats export $(grep|xargs), tracked samples, the no-re-sourcing rule for module readers, and the deployment boundary (repo-presence is not git-tracked is not deployed). - Extend references/taskfile.md with the one-env-driven-task rule (no :default pairs) and composing commands through the task surface. - Add the lifecycle-hook module type to references/module-patterns.md. - Keep this skill's _moduleExampleV1_ naming as canonical; the retired skills' HL_V1_ and double-underscore dialects were not carried over. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
3.3 KiB
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:
lib/only — internal. Reachable from other modules that source your--index-api.sh.api/<command>.sh— user-facing. Someone can run it directly from the repository root.- Module
Taskfile.yml— discoverable throughtask --list-all. - Root
Taskfile.ymlinclude — 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/-*.shfile and forgetting to source it fromlib/--index-api.sh. - Parsing CLI flags in implementation files instead of
--env-vars-reader.sh. - Printing secrets in logs.
- Sourcing
base/lib/--index.shfrom 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.