weir
On this page

Tooling

This page covers the tools around the language: the weir command, command signatures, YAML schemas, remote modules, the .weir/ directory and configuration. Read it when you set up a project or a CI pipeline; for the language itself, see the reference. Editors and the REPL have their own pages.

The CLI

weir is a single binary with a handful of subcommands (CI checks this block matches weir --help exactly):

usage: weir                                    the REPL
       weir <script> [args...]                 run a script
       weir -e <program>                       evaluate a program; the result is its last expression
       weir check [--json] <script>            diagnostics only (no evaluation)
       weir check --can [--strict] [--json] <script>  the static capability report
       weir fmt [--check] <script>             canonical formatter
       weir lsp                                language server (stdio)
       weir add sig <tool>                     generate a command signature from the installed binary
       weir add schema <url> --as <name>       fetch an external contract, lock it
       weir add module <src>//<file>@<ref> --as <name>  vendor a remote module, lock it
       weir gen types --schema <name>          generate weir types from a locked schema
       weir restore                            re-materialize the lock's artifacts
       weir verify                             vendored contracts vs the lock
       weir --version                          the build stamp

Running

Checking

The project subcommands

These maintain the .weir/ tree:

Conventions

Command signatures

weir checks that a command exists before running a script. A signature goes one step further and checks the flags you pass it. With a signature for bicep, the typo in bicep build --outfil x is an error from weir check that points at the line, instead of a failure at 3am. (A generated signature is partial, so there it is a warning.)

Creating a signature

Generate a signature from the installed tool:

weir add sig bicep      # probes the installed binary, writes .weir/sigs/bicep.weir + a lock entry

Then declare it in each script that should be checked against it. Scripts without the #sig line aren’t affected:

#sig bicep
bicep build --outfile x.json

The signature records the tool’s version: the first line of its --version output, with runs of whitespace collapsed. (Only the first line, because some tools, like az, print pages of environment details.) weir verify compares it with the installed tool’s version and reports any difference at all. When the tool updates, run weir add sig again. If the regenerated file shows no diff, nothing that matters to your scripts changed.

Partial by default

A generated signature is partial: an unknown flag is a warning, not an error, because the flags read from a tool’s help may be incomplete. Once you have checked the flag list by hand, add let exhaustive = true to the signature file, and unknown flags become errors.

weir add sig looks for flags in this order: the tool’s fish completions, fish completion files shipped with the tool, then its --help. If the help lists subcommands, it reads each subcommand’s help too, up to four levels deep (enough for kustomize edit add resource).

For a tool with subcommands, the signature lists flags per subcommand:

A tool without subcommands gets one flat list of flags. If no flags are found at all, weir add sig says so. You can also write .weir/sigs/<tool>.weir by hand, in either form; it’s an ordinary weir file.

Checking in CI

weir check never runs the tool and never downloads anything. A signature is a checked-in file that only matters at check time (project layout). So you can check scripts for tools that are only installed in CI, and CI can check them offline. Commit the signature file: if it is in the lock but missing on disk, weir restore can’t recreate it, and tells you to regenerate it with weir add sig.

For why you would want a signature, see the short section in the guide.

YAML schemas

A yaml block can name a JSON schema on its first line, and weir check then validates the block’s structure against it before anything runs:

let svc = yaml schema=k8s-service
    apiVersion: v1
    kind: Service
    ...

Vendoring

weir add schema <url> --as <name>    # fetches into .weir/schemas/<name>.json, locks it

The schema file is checked in, recorded in the lock with its hash, and only matters at check time (project layout). weir check never downloads anything, so checking works offline and in CI. If a schema is in the lock but missing on disk, weir restore recreates it and verifies its hash. weir verify reports missing or modified schemas.

For Kubernetes, use the schema files whose names end in …-standalone-strict.json. Their additionalProperties: false is what makes an unknown field an error; the plain variants accept any key.

What the schema check covers

A passing check doesn’t mean every value was validated:

In short, the schema checks what weir can see before the script runs. Templates, block scalars and splices are explained in the guide.

Types from a schema

The same locked schema can generate the from json/from yaml types for you. This is the more reliable counterpart of the REPL’s #infer:

weir gen types --schema pod [--as Pod] [--out <path>|-]

This reads the vendored .weir/schemas/pod.json (never the network), writes a weir module containing only type declarations to .weir/types/pod.weir, and prints the line to import it. weir:pod then names that file; if a vendored module .weir/modules/pod.weir also exists, the import is an error — rename one.

#infer drafts types from a sample, so it only knows what that sample contained: if the next run returns a container without env, the drafted type breaks. The schema knows what no sample can:

More of the mapping:

Where the schema leaves a question open, the file has a // note: line saying what was chosen, instead of a silent guess.

The generated file is yours: edit it freely. Nothing regenerates it behind your back (signatures work the same way). Running weir gen types again regenerates it and overwrites your edits. It is deliberately not in the lock: the lock records the files that add downloaded, and this file belongs to you once it’s generated. Instead, a header in the file records the schema name and the lock hash it came from.

The same locked schema always produces the same output. Before writing, weir gen types checks the module with the normal checker; if the schema can’t be expressed in weir, the command fails with the reason and writes nothing.

Import it anywhere in the project. The weir: prefix looks in vendored modules first, then in generated types:

import "weir:pod" as Pod
let p = kubectl get pod web -o json |> from json Pod

(An imported type can be named without its module prefix here, so from json takes Pod, not Pod.Pod.)

Remote modules

To share code across repos, you vendor it: weir downloads a copy into your repo. This is not a package manager. There is no registry, no dependency resolution and no version ranges.

weir add module github.com/org/repo//lib/retry.weir@v1.2.0 --as retry

The source starts with the host. // separates the repo from the path of the file inside it. (GitLab groups can nest, so weir can’t guess where the repo name ends. Marking it explicitly also means one syntax works for every host.)

The @ref is required: a tag, branch or commit sha. weir resolves it to the full commit sha and records that in the URL stored in the lock, so restore downloads exactly what you reviewed. This short form works for github.com and gitlab.com. For any other host, give the full raw URL:

weir add module https://raw.githubusercontent.com/org/repo/<sha>/lib/retry.weir --as retry

Before writing anything, add checks that the file:

If any check fails, nothing is written to .weir/.

Then import the module by name anywhere in the project. weir: finds it by searching upward for .weir/, the same way #sig does, stopping at a .git directory or file or at the filesystem root (see project layout). So the import line is the same at any directory depth, and it works the same way for a module vendored outside any git repo:

import "weir:retry" as Retry

To update, run add again. It overwrites the file and prints the old and new sha, and you review the change in the vendored file’s diff. If the vendored copy gets modified, verify reports it and restore downloads it again.

check --can follows imports, so a vendored module’s commands, file writes and network access appear in your report, each at the module’s own file:line. You can see what a dependency is able to do before running anything.

For a private repo, set WEIR_TOKEN_GITHUB_COM or WEIR_TOKEN_GITLAB_COM. Only add and restore need the token; using the committed file doesn’t. Tokens are read from the environment and never stored. GitHub answers 404 for a private repo when no token is given, so weir’s not-found error mentions the token.

Modules aren’t signed. You decide to trust a module by reviewing its code. From then on, the lock holds its hash, and your repo’s history records every change to it.

Project layout: .weir/

A project that uses signatures, schemas or remote modules grows one directory:

.weir/
  lock.json          # the lock: exact identity + hash for every vendored artifact
  sigs/<tool>.weir   # command signatures (weir add sig)
  schemas/<name>.json# JSON schemas (weir add schema)
  modules/<name>.weir# vendored modules (weir add module)
  types/<name>.weir  # generated, user-owned type modules (weir gen types — not locked)

A script finds its .weir/ by searching upward from its own directory and using the first one found. The search stops at a .git directory or file (so git worktrees work) or at the filesystem root, and if nothing is found, the error says what was looked for and where the search stopped. One .weir/ at the repo root serves every script below it.

Commit all of it

Check in the whole directory, including the lock, signatures and schemas. That is the point of vendoring: weir check never downloads anything, so CI checks against exactly what you committed. If a fresh clone is missing a downloaded file, weir restore gets it back and verifies its hash, but with .weir/ committed you never need to.

How it behaves

Configuration

weir has very little configuration: one environment variable for logging and one JSON file that only the REPL reads. Neither changes what a script does, so a script behaves the same wherever it runs; only its own file decides what it does.

WEIR_LOG

Log.trace, Log.debug, Log.info and Log.warn write log lines to stderr. By default you see info and warn; WEIR_LOG sets the level for one run (trace, debug, info, warn or off):

WEIR_LOG=debug weir script.weir    # turn the detail on
WEIR_LOG=off weir script.weir      # silence the log

printerr and fail always print, whatever the level. There is deliberately no Log.error, because an environment variable should never be able to hide an error message. Logging never writes to stdout, so stdout is the same at every level.

The REPL config

The REPL reads its settings from $XDG_CONFIG_HOME/weir/config.json (~/.config/weir/config.json if XDG_CONFIG_HOME isn’t set; %APPDATA%\weir\config.json on Windows). Nothing else reads this file.

keydefaultmeaning
historySize5000entries kept
historyDeduptrueremove duplicates and keep the latest, so each entry appears once, most recent last
historyPath<state>/weir/historywhere history lives ($XDG_STATE_HOME, ~/.local/state, or %LOCALAPPDATA%)
finderFlags["--height", "40%", "--reverse"]extra arguments for the Ctrl+R fzf search
echoElems100how many elements of a lazy seq the REPL prints (the REPL)

An unknown key gets a warning suggesting the closest valid key, so a typo doesn’t silently do nothing. If the file is missing or can’t be parsed, the defaults apply.

The REPL also loads an optional init file from the same directory. It holds your own declarations (functions, a custom prompt), #alias lines, and a #session block for settings.

weir also honors NO_COLOR everywhere: it turns off color in the REPL and the checker. Piped output is always plain.