The config and its modules
Structure comes from the file layout, not from an API. What each directory is for, why modules are also groups, and why there is no profile system.
The config is a directory of files, and the layout is the only structure there is. No manifest declares what a module is, and no API call groups resources. Where a file sits is what it means.
The layout
niwa init does not hand you an empty file. It scans the machine, then
writes a config that already describes it, with the sections you have
not filled in yet labelled and commented. The whole skeleton exists from
the first run, hosts/ included, because splitting that out later means
rewriting files that already work.
The structure below is the example config the tool's own tests run
against. state/ is the one directory init does not write; the first
apply writes the first stamp.
~/.config/niwa
| Entry | What it is for |
|---|---|
init.luau | The entry point. It requires each module in turn, and the order of those lines is execution order. |
.luaurc | The editor and analyzer settings, generated by init. |
niwa.lock | What every version-resolved declaration resolved to. Committed. |
modules/ | The declarations, split by subject. Each file is also a group. |
hosts/ | One file per machine, loaded last, for the differences between them. |
files/ | Sources for niwa.file and niwa.link, referenced as @self/files/…. |
secrets/ | Sealed secrets, and the escrowed sealing key. Ciphertext only. |
state/ | One committed stamp per machine: what it applied, and when. |
The starter config writes eight modules: cli, apps, shell, dev,
desktop, system, services, and inbox. The names are a starting
point, not a schema. Rename them, split them, or add your own, and the
only line that has to know is the require in init.luau.
inbox.luau is worth naming. An accepted proposal lands there when no
other module is a clear match, and it is a permanent home rather than a
queue. Moving a line out of it later is optional.
The entry point, and why niwa.host() exists
init.luau requires each module by a literal path, so the analyzer can
resolve every one of them before anything runs. That is also why the
last line is a call and not another require:
niwa.host() -- hosts/<this machine>.luau, if it exists, loaded lastA machine's own file has a name known only at run time, which a static
require cannot express. niwa.host() performs that lookup through
niwa's own resolver, so the dynamic case stays typed and the static case
stays analysable.
Modules are also groups
Because a module is a file, the tool can talk about it. On a converged
machine niwa plan -v prints one count per module, and -vv lists
every resource under the module that declared it.
niwa apply --only desktop runs one module and leaves the rest as they
stand. A module is a unit of reading, a unit of reporting, and a unit of
running, and you got all three by naming a file.
Modules declare, hosts override
A host file overriding a module is the supported mechanism for per-machine variance: later declaration wins, merged per key. Two modules setting one key to different values is a lint error with both source locations, because there the order is an accident. Many machines has the worked pair.
Why there is no profile system
The config is a program, so the composition story is already answered.
local function tool(name: string, config: string?)
local formula = niwa.brew.formula(name)
if config then
niwa.file(`~/.config/{name}/{config}`, { source = `@self/files/{config}` })
end
return formula
end
tool("starship", "starship.toml")
tool("btop")That function is a bundle. A module is a role. A shared recipe is a file that returns a function. A bundle primitive, a profile system, and a recipe format would be three concepts covering what one already covers, each with its own syntax, its own error messages, and its own page here.
Results pass through untouched, so tool("neovim").changed branches
exactly like the call it wraps. Sugar you write and sugar niwa ships are
the same kind of thing, which is why the built-in surface stays small.
Next
- The config language — what the files in this layout are written in
- Share a module — the same structure, published for another repo to load
