Files
development-scripts-3darch/references/module-anatomy.md
T
oleg-lukasonokandClaude Opus 5 31a5408a6c Add development-scripts agent skill
Ports generic-scripts-module-v1 into a routed skill: module anatomy, shared
loggers/base modules, Taskfile integration, workflows, and pattern decisions.

Fixes a latent defect in the loggers example that silenced all logging in the
module scaffold: loggers/lib/--index-api.sh now sources --env-vars-reader.sh,
and the reader preserves already-exported values so caller overrides still win.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 22:18:55 +03:00

6.7 KiB

Module Anatomy

The four file types inside lib/, the api/ wrapper, and the patterns each follows.

Read this when writing or changing a reader, a validator, an implementation function, or a public wrapper.


1. --env-vars-reader.sh — defaults and CLI parsing

Responsibilities, in order:

  1. export defaults, preserving any value already exported by the caller;
  2. parse CLI flags into environment variables;
  3. restore the positional arguments so later consumers still see them.
#!/bin/bash

export MODULE_EXAMPLE_V1_NAME="${MODULE_EXAMPLE_V1_NAME:-default-name}"
export MODULE_EXAMPLE_V1_OUTPUT_DIR="${MODULE_EXAMPLE_V1_OUTPUT_DIR:-.tmp/module-example-v1}"
export MODULE_EXAMPLE_V1_DRY_RUN="${MODULE_EXAMPLE_V1_DRY_RUN:-false}"

ALL_ARGS=("$@")
while [[ "$#" -gt 0 ]]; do
  case $1 in
  --name)
    export MODULE_EXAMPLE_V1_NAME="$2"
    shift
    ;;
  --output-dir)
    export MODULE_EXAMPLE_V1_OUTPUT_DIR="$2"
    shift
    ;;
  --dry-run)
    export MODULE_EXAMPLE_V1_DRY_RUN="true"
    ;;
  *) ;;
  esac
  shift
done
set -- "${ALL_ARGS[@]}"

Rules:

  • Use a stable uppercase environment prefix per module.
  • Always use ${VAR:-default}, never a bare export VAR=value. A bare assignment silently discards a caller's override, and the caller has no way to detect it. This is what makes a reader safe to source more than once.
  • Prefer explicit CLI flags over positional arguments.
  • Defaults must be safe to run locally — a default that writes outside .tmp/ or touches a shared system is not a safe default.
  • Never print secrets here.

The ALL_ARGS save/restore matters because shift consumes the arguments. Without the set -- at the end, anything sourced after the reader sees an empty argument list.


2. --env-vars-validator.sh — required inputs

#!/bin/bash

. ./.scripts/loggers/lib/--index-api.sh

if [ -z "${MODULE_EXAMPLE_V1_NAME}" ]; then
  _loggers_error "module-example-v1" "Missing required --name / MODULE_EXAMPLE_V1_NAME"
  exit 1
fi

Rules:

  • Runs from --index.sh, after the reader and before the API index, so failures happen before any implementation function is even defined.
  • Use _loggers_error for the failure and _loggers_info for a usage hint.
  • exit 1 is correct here — the validator runs during bootstrap of a top-level command, and stopping is the intent.
  • Validate only what every command in the module needs. A variable that only one command requires is validated inside that command's function, not globally — otherwise unrelated commands fail on input they never use.

3. --index-api.sh — dependency and implementation wiring

#!/bin/bash

. ./.scripts/loggers/lib/--index-api.sh
. ./.scripts/base/lib/--index-api.sh

. ./.scripts/module-example-v1/lib/-initialize-one.sh

Rules:

  • Upstream dependencies first, then this module's own implementation files.
  • Source other modules' --index-api.sh, not their --index.sh — see references/shared-modules.md §2.
  • Every implementation file must appear here. A lib/-*.sh that is never sourced defines nothing, and the failure surfaces as command not found for a function you can see on disk.
  • Safe to source repeatedly. Sourcing defines functions; it should have no other side effect.

4. --index.sh — runtime bootstrap

#!/bin/bash

#       --> passed parameters are read & exported environment variables
. ./.scripts/module-example-v1/lib/--env-vars-reader.sh

#       --> required environment variables are validated for existence
. ./.scripts/module-example-v1/lib/--env-vars-validator.sh

#       --> available functions are imported/exported
. ./.scripts/module-example-v1/lib/--index-api.sh

Rules:

  • Exactly these three sources, in exactly this order.
  • This is the module's own entrypoint, sourced by its api/ wrappers. Other modules never source it — they take --index-api.sh.
  • A module with no required inputs may omit the validator, but keeping an empty one makes the next required variable a one-line change.

5. Implementation functions — lib/-<command>.sh

#!/bin/bash

. ./.scripts/loggers/lib/--index-api.sh

_moduleExampleV1_initializeOne() {
  local FUNCTION_NAME="_moduleExampleV1_initializeOne"

  _loggers_info "${FUNCTION_NAME}" "Initializing module-example-v1"
  _loggers_debug "${FUNCTION_NAME}" "Output directory: ${MODULE_EXAMPLE_V1_OUTPUT_DIR}"

  if [ "${MODULE_EXAMPLE_V1_DRY_RUN}" = "true" ]; then
    _loggers_info "${FUNCTION_NAME}" "Dry run enabled; no files will be changed."
    return 0
  fi

  mkdir -p "${MODULE_EXAMPLE_V1_OUTPUT_DIR}"
  _loggers_info "${FUNCTION_NAME}" "Initialization complete."
}

Rules:

  • Open with local FUNCTION_NAME="_<module>_<function>" — every logger call takes it as the first argument, which is what makes log output traceable to a specific function.
  • One file, one primary function. Small private helpers may live alongside it.
  • Use _loggers_* helpers, never raw echo, for status and errors.
  • return non-zero on failure; never exit. The file is sourced, so exit terminates the caller's shell — including an interactive one.
  • Read configuration from the module's environment variables. Never re-parse $@ here.
  • Quote every expansion unless word splitting is deliberate.
  • Never expose secret values in a log line.

6. api/ wrappers

#!/bin/bash

. ./.scripts/module-example-v1/lib/--index.sh

_moduleExampleV1_initializeOne

That is the whole file. Rules:

  • Source exactly lib/--index.sh, then call exactly one function.
  • No implementation, no flag parsing, no validation, no argument forwarding.
  • Arguments reach the reader automatically: sourcing happens while the wrapper's own $@ is still in scope, so --env-vars-reader.sh sees them without being passed anything.
  • Create a wrapper only for a genuinely user-facing command. An internal helper stays in lib/ — every file in api/ is a public interface someone may come to depend on.

7. Diagnosing a broken module

Symptom Cause
command not found for a function that exists on disk Its file is missing from lib/--index-api.sh
No log output at all, exit code 0 Logger level flags never exported — see references/shared-modules.md §3
A flag is ignored Not handled in --env-vars-reader.sh, or handled after the set -- restore
Later sources see no arguments Reader is missing the ALL_ARGS save/set -- restore
No such file or directory for a lib/ path Run from inside the module instead of the repository root
.env loaded unexpectedly Something sourced base/lib/--index.sh instead of base/lib/--index-api.sh
The shell exits on a handled error exit used inside a sourced implementation function