Template
The cyber-mux/template subpath is the template layer as a
library: the schema types, a validator that reports every error at once, the flat-N desugarer
that turns a pane pool into a split tree, and the filesystem-backed resolution seam. The schema and
desugaring are pure — no Exec, no filesystem — which is what makes them testable with no mock.
import { validateTemplate, parseTemplate, resolveTree, resolveTemplate, listTemplates, nodeTemplateStore, type Template,} from 'cyber-mux/template'The schema
Section titled “The schema”A Template names a pane layout in one of three spellings — exactly one of root, panes, or
tabs:
root— an explicitTemplateNodetree ofPaneNodeleaves and binarySplitNodes (direction: 'right' | 'down', optionalratio).panes— the flat sugar: a pool ofFlatPanes plus anarrange('tiled' | 'even-horizontal' | 'even-vertical'), desugared into a tree.tabs— the two-level form: a workspace ofTabNodes, each its own tree.
The rule the whole capability enforces: cwd is not in the schema. A template carrying one
fails validation — an ignored key would quietly make a template non-reusable, the one thing a
template must be. Use a pane’s relative dir for a subdirectory under the apply-time target.
Validating and parsing
Section titled “Validating and parsing”validateTemplate(template, stem?) → string[]
Section titled “validateTemplate(template, stem?) → string[]”Every validation error, not the first — an empty array means valid. Each error names its own JSON
path (root.second.first.cwd), so it points at a place in the file. Pass stem (the filename’s
stem) to enforce that the name field matches it, so a copied template that kept its old name fails
loudly.
const errors = validateTemplate(parseTemplate(raw), 'dev')if (errors.length) throw new Error(errors.join('\n'))parseTemplate(raw)— parse the bytes; throws on malformed JSON (schema validity isvalidateTemplate’s job).isValidTemplateName(name)/assertTemplateName(name)— a name is[a-z0-9][a-z0-9-]*and must be a plain filename stem, checked before any file read so a name can never traverse out of the templates directory.
Desugaring
Section titled “Desugaring”resolveTree(tree) → TemplateNode
Section titled “resolveTree(tree) → TemplateNode”The one place panes/arrange becomes a tree, so template show --desugar and the apply walk can
never disagree about what a flat template means. Takes either carrier of a tree (a Template or a
TabNode), so the tab tier and the template tier resolve through the same desugarer.
Also exported: desugar(panes, arrange), collectPanes(node) (every pane in template
order), and firstPane(node) (the pane that lands on a subtree’s existing region).
Bound facade — templateApi
Section titled “Bound facade — templateApi”The ergonomic surface over resolution: templateApi(env, deps?) binds env + Exec + TemplateStore
once (defaulting to nodeExec / nodeTemplateStore), returning a TemplateApi whose methods drop
them. env is bound like resolveMux(env) because the searched
directories read $XDG_CONFIG_HOME / $HOME.
import { templateApi } from 'cyber-mux/template'
const tpl = templateApi(process.env) // or templateApi(env, { exec, store }) to injectconst { raw } = tpl.resolve({ name: 'dev' })for (const t of tpl.list()) { /* ... */ } // list() defaults its dirs to dirs()| method | binds + wraps |
|---|---|
tpl.dirs() |
templateDirs |
tpl.resolve({ name?, file? }) |
resolveTemplate |
tpl.list(dirs?) |
listTemplates — dirs defaults to dirs() |
deps is TemplateDeps { exec?, store? }. The functions below are the raw seam it binds; the pure
schema functions above (validateTemplate, resolveTree, …) never took a seam at all.
Resolution
Section titled “Resolution”The filesystem-backed seam that finds a template’s bytes across the searched directories.
resolveTemplate(opts) → ResolvedTemplate
Section titled “resolveTemplate(opts) → ResolvedTemplate”Resolve a template to its bytes: --file (explicit path, skips search), then the repo directory,
then the user directory. Repo beats user — a project shipping a template should not be silently
shadowed by a personal one of the same name. Throws when nothing resolves, naming both directories
searched.
templateDirs(exec, env, homedir?)→{ repo, user }— the two searched directories. The repo location resolves throughresolvePrimaryRoot, so every worktree of a project sees one canonical answer.listTemplates(store, dirs)→TemplateListing[]— every resolvable template, repo first, with a shadowed user template reported (shadowed: true) rather than omitted.
store is a TemplateStore seam (list / read / dirExists / write); nodeTemplateStore is
the default over the filesystem.