Configuration
The options you would otherwise retype on every invocation — which runners to
skip, what order to show them in, whether execution is allowed, how files are
discovered — live in a settings file instead. The file and the config
command that reads and writes it come from @sksizer/detect-runners and
are shared between it and waypost: one file, one set of rules,
both CLIs. The file is named waypost.config.json even though the library is
detect-runners, because way is the name a
person actually types.
Layers
Section titled “Layers”Lowest precedence first:
| Layer | Where |
| ---------------- | ---------------------------------------------------------------------------------------------------------- |
| defaults | built in |
| user | $XDG_CONFIG_HOME/waypost/config.json, or ~/.config/waypost/config.json when XDG_CONFIG_HOME is unset |
| WAYPOST_CONFIG | replaces the user layer’s location — the named file is read instead of the XDG path |
| project | waypost.config.json at the project root the directory resolves upward to |
| flags | the command line |
XDG is used on every platform, macOS included — a developer with a dotfiles repo expects to find this beside their other tool configuration rather than in a directory they only see in Finder.
WAYPOST_CONFIG=/path/to/file only ever replaces the user layer’s location. The
project layer is still read from the project root, still merges on top, and
cannot be turned off this way. A relative path resolves against the project
root. Naming a file that does not exist is an error rather than a silent
empty layer — an env var pointing at nothing is a typo, not a choice.
Merging
Section titled “Merging”Objects merge key-wise. Everything else — scalars and arrays alike — replaces wholesale:
user { runners: { disabled: ["cargo"], order: ["moon"] } }project { runners: { order: ["just"] } }result { runners: { disabled: ["cargo"], order: ["just"] } }runners merged key-wise, so disabled survived from the user layer;
order was replaced entire rather than concatenated to
["moon", "just"]. Arrays replace because both settings answer “which
ones, in what order” — a project that says order: ["just"] means that
list. --exclude on the command line replaces runners.disabled for the
same reason, and --exclude-path replaces excludePaths.
The file
Section titled “The file”Schema v1, every key optional:
{ "runners": { // Runner ids to skip entirely. "disabled": ["cargo"], // Preferred display order. Runners not named here follow alphabetically. "order": ["moon", "just"], }, // Default for the exec trust gate. --exec / --no-exec still wins. "allowExec": false, // git | walk | auto — how files are found. "discovery": "auto", // Paths that are not project structure, on top of the built-in list. "excludePaths": ["crates/manifest-graph/tests/mini-repo"], // USER LAYER ONLY — a project-layer trust block is rejected the same way a // project-layer allowExec: true is. Pre-authorizes a class of repositories // with no per-repository trust marker. See "trust.paths and trust.owners" // below and [Trust](../trust/). "trust": { "paths": ["/home/me/work/**"], "owners": ["my-org"], },}runners.order reorders the reports behind --json and --list (and the
library’s own --table), and the terminal UI’s tab row follows it too. The
web view does not: its tab order is a separate, per-browser setting saved by
dragging a tab, and that saved order always wins over whatever the server
sends (see The web view).
excludePaths, and what it is not
Section titled “excludePaths, and what it is not”runners.disabled turns a runner off — it is never probed anywhere in
this project. excludePaths leaves every runner on and narrows where
they look. They answer different questions, and the second is the one a
repository usually wants.
The case it exists for is a tree that holds real manifests belonging to nobody: a test-fixture repository, a vendored sample, a scaffolding template. A detector is right to find those and right to report a broken one — they are simply not tasks anyone can run. Disabling the runner would hide them, and would hide the tasks you do have along with them.
{ "excludePaths": [ "crates/manifest-graph/tests/mini-repo", // a fixture repo, not a project "testdata", // any directory of that name ],}Two forms, and deliberately not glob syntax:
- A bare name matches any single path segment, at any depth. This is the
only useful reading for something like
node_modules. - A value containing a slash is matched from the root, segment by
segment.
src/apptherefore does not also takesrc/application, and naming one fixture tree does not exclude every directory in the repository namedtests/.
Changing this stales the cache. The setting lives outside the tree, so
nothing about hashing the repository would notice that it moved; each stored
entry records the exclusions it was probed under, and a mismatch re-probes
with reason: "excludes".
allowExec and its default
Section titled “allowExec and its default”The library’s own default for allowExec is false — it may be pointed at
a repository you did not write, and reading moon means running that
repository’s own toolchain configuration. waypost sits in front
of it with its own default of true, since running it means standing in
your own repository. That default sits above the library’s default but
below any configuration file.
allowExec is one exception to the layer order above. waypost.config.json
lives inside the repository being scanned, so it may only lower
allowExec to false. It can never raise it to true: a repository cannot
grant itself permission to run its own code. An allowExec: true in a
project file is stripped before the layers merge — the value is not applied,
is not attributed to the project layer by config list, and a warning names
the file and points at way trust. Only the
user file and the command line can turn execution on.
Highest precedence first:
--execor--no-execon the command lineallowExecin the user file; andallowExecinwaypost.config.jsononly where it lowers the value tofalse- waypost‘s own default,
true
The library’s allowExec: false default never applies to waypost;
its own true answers only where no file answered at all.
way config list and
way config get allowExec report
waypost‘s own default rather than the library’s, still labelled (default), because the
value actually in effect is what those two commands exist to tell you.
This whole list settles the configured value, which is not yet permission
to run anything. The configured value is combined with per-repository trust:
execution happens only when it is on and the repository has been trusted,
or --exec was given for this one run. A true in the user file on its own
runs nothing. See Trust.
trust.paths and trust.owners
Section titled “trust.paths and trust.owners”The same USER-LAYER-ONLY rule as allowExec, but stricter: trust has no
“lower” state a project file could honour, so a project-layer trust block
is dropped before the layers merge, entirely, with a warning — never partially
applied.
{ "trust": { "paths": ["/home/me/work/**"], // a glob over the resolved repo root "owners": ["my-org"], // case-insensitive origin remote owners },}This pre-authorizes a whole class of repositories — everything under a
directory, or everything cloned from an organization — without a
per-repository trust marker for each one. See Trust
for the precedence between a marker and this policy, the glob dialect, and
the threat model behind matching on trust.owners.
The config command
Section titled “The config command”config list [--json] every setting, and the layer it came fromconfig get <key> one setting's effective value, as JSONconfig set <key> <value> [--user|--project] write a setting (default --project)config unset <key> [--user|--project] remove a setting (default --project)config path where each layer's file livesconfig --help this listKeys are dotted: runners.disabled, runners.order, allowExec,
discovery, excludePaths, trust.paths, trust.owners. A value is read
as JSON and falls back to the bare string when
that fails, so set discovery git and
set runners.order '["moon","just"]' both work and a plain word needs no
quoting.
list annotates each value with the layer that supplied it — the question
you ask when a setting is not what you expected. The settings themselves go
to stdout and the file locations and any warnings go to stderr, so --json
stays machine-readable and the plain listing stays greppable. get splits
the same way: the value alone on stdout, any unknown-key warning on stderr.
-C (or --dir) reaches config too, so
way config path -C /elsewhere reports that directory’s files
rather than the current one’s.
An unknown key in the file is a warning naming the file and the key, and the rest of the file still loads. A file that is malformed, or holds a wrong-typed value, is an error naming the file and the problem — there is no silent fallback, because a setting that quietly did not apply is worse than one that refused to load.
Exit codes
Section titled “Exit codes”2 means the command as typed cannot run at all — an unknown subcommand,
option or setting name, or a value of the wrong type for its setting — every
one of those decided from the arguments and the schema alone, with no file
read. 1 means a well-formed command that failed against the state it
found: a file that will not parse, an unset on a key that was never set, a
get with no value anywhere. A script can act on the split: 2 means fix the
command, 1 means look at the state.