Skip to content

Reconsider module system architecture — possibly a declarative DSL #4

Description

@rolfsormo

The concern

The current module system (core_modules.sh + modules/module_*.sh) works by having
each module register three plain bash functions (plan, apply, optional doctor)
that get called by name through a bash-3-compatible pseudo-associative-array registry
(db_register_module / _db_module_get / _db_module_set).

This has gotten harder to reason about as the number of modules has grown:

  • Plan/apply pairs are hand-written twice per module and can drift (we've already hit
    bugs where plan didn't match what apply actually does — see the double-sourcing
    fix, where db_module_zsh_plan initially didn't know about a new skip condition
    that db_module_zsh_apply had).
  • "What will actually happen" requires reading arbitrary bash, not a declarative
    description — no way to statically analyze, diff, or verify a module's behavior
    without executing it.
  • Every module re-implements the same idempotency/dry-run boilerplate
    (if [[ "${DB_DRY_RUN:-false}" == "true" ]] ... else ...) by hand.
  • Modules end up longer than the actual "what changes" logic requires, because so
    much of each file is control flow rather than intent.

Proposal to explore

Design a small, declarative DSL (JSON, YAML, or a constrained bash-embedded config —
open question) that describes what a module does (files to write, blocks to
upsert, packages to ensure installed, commands to run) rather than how to do it
imperatively. A shared interpreter/executor would then:

  • Guarantee plan/apply consistency by construction (plan = "describe what apply would
    do" derived from the same spec, not hand-written separately).
  • Make idempotency and dry-run automatic, not something each module re-implements.
  • Potentially allow static validation of a module before it's ever run.
  • Make writing a new module shorter — ideally closer to declaring "these files, these
    packages, these config blocks" than writing full bash control flow.

Scope / risk

This is a genuine architecture change, not a quick refactor — it touches every
existing module (module_pkg, module_znap, module_zsh, module_starship,
module_tmux, module_mise, module_corepack, module_direnv, module_git,
module_services, module_security), the build process, and the module-authoring
docs in AGENTS.md/ARCHITECTURE.md. Needs a deliberate design pass (what the DSL
schema looks like, what the executor can/can't express, migration plan for existing
modules) before any code — not something to start mid-flight on an unrelated task.

Open questions

  • JSON/YAML data file per module, vs. a constrained subset of bash/functions that a
    shared executor interprets, vs. something else entirely?
  • How to keep the "trivially easy to add a module" bar from AGENTS.md while adding
    this structure?
  • Escape hatch for modules that need genuinely imperative logic (e.g. module_mise's
    npm-globals migration prompt, module_pkg's per-OS package name mapping) — the DSL
    can't reasonably express everything; need a clean way to drop to raw functions when
    needed without the whole system becoming "everything drops to raw functions anyway."

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions