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."
The concern
The current module system (
core_modules.sh+modules/module_*.sh) works by havingeach module register three plain bash functions (
plan,apply, optionaldoctor)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:
bugs where
plandidn't match whatapplyactually does — see the double-sourcingfix, where
db_module_zsh_planinitially didn't know about a new skip conditionthat
db_module_zsh_applyhad).description — no way to statically analyze, diff, or verify a module's behavior
without executing it.
(
if [[ "${DB_DRY_RUN:-false}" == "true" ]] ... else ...) by hand.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:
do" derived from the same spec, not hand-written separately).
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-authoringdocs 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
shared executor interprets, vs. something else entirely?
this structure?
module_mise'snpm-globals migration prompt,
module_pkg's per-OS package name mapping) — the DSLcan'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."