elmq
A CLI for querying and editing Elm files — like jq for Elm.
Designed as a next-gen LSP for agents and scripts, not editors. Optimized for token efficiency and structured output.
Status: Active development. Supports reading and writing Elm declarations, imports, and module lines, plus project-wide operations (rename, move, extract, add/remove variant). See ROADMAP.md for what's planned.
[!TIP]
Curious why elmq exists and what it's trying to prove? Read HYPOTHESIS.md for the claim, the experimental design, and the threats to validity.
Install
Homebrew
brew install caseyWebb/tap/elmq
npm
npm install -g @caseywebb/elmq
Or run without installing:
npx @caseywebb/elmq <command>
From source
Requires Rust:
cargo install --path .
Usage
Write-safety precondition
Every elmq command that mutates a .elm file — set decl, patch, rm decl, add/rm import, expose/unexpose, mv, rename decl, move-decl, add/rm variant, set let, set case, rm let, rm case, rm arg, rename let, rename arg, add arg — refuses to operate on a file that has pre-existing tree-sitter parse errors, and refuses to produce an output buffer that would not parse. If either check fires, elmq exits non-zero with a refusing to edit … or rejected '<op>' write to … message naming the file and the first error location, and the file on disk is left unchanged. Fix the file by hand (or with your editor) and retry. All write commands confirm success with ok output (sole exception: rm variant emits an actionable references_not_rewritten advisory section when the removed constructor appears in non-cleanly-rewritable positions). Read commands (list, get, grep, refs, variant cases) keep their existing tolerant behavior — they print a warning and continue so you can still inspect broken files.
File summary
elmq list src/Main.elm
module Main exposing (Model, Msg(..), update, view) (38 lines)
imports:
Html exposing (Html, div, text)
Html.Attributes as Attr
type aliases:
Model L4-8
types:
Msg L11-15
functions:
update Msg -> Model -> Model L18-28
view Model -> Html Msg L31-34
helper L37-38
With doc comments
elmq list src/Main.elm --docs
type aliases:
Model L4-8
The model for our app
types:
Msg L11-15
Messages for the update function
...
Extract a declaration
elmq get src/Main.elm update
update : Msg -> Model -> Model
update msg model =
case msg of
Increment ->
{ model | count = model.count + 1 }
Decrement ->
{ model | count = model.count - 1 }
Reset ->
{ model | count = 0 }
Includes doc comments and type annotations when present. Returns non-zero exit code if the declaration is not found.
Read across multiple files in one call with -f:
elmq get -f src/Page/Home.elm update view -f src/Update.elm main
Each -f group is a file followed by one or more names. Output is framed as ## Module.decl blocks (falls back to ## file:decl without elm.json).
Upsert a declaration
echo 'helper x =
x + 42' | elmq set decl src/Main.elm
Reads a full declaration from stdin, parses the name, and replaces the existing declaration (or appends if new). Use --content to pass the declaration inline instead of stdin:
elmq set decl src/Main.elm --content 'helper x = x + 1'
A name mismatch between the declaration source and the target is an error; use rename decl to rename instead of upsert.
Patch a declaration
elmq patch src/Main.elm update --old "model.count + 1" --new "model.count + 2"
Surgical find-and-replace scoped to a single declaration. The --old string must match exactly once.
Remove a declaration
elmq rm decl src/Main.elm helper
Removes the declaration, its type annotation, and doc comment. Cleans up excess blank lines.
Manage imports
elmq add import src/Main.elm "Browser exposing (element)"
elmq rm import src/Main.elm Html
add import inserts in alphabetical order or replaces an existing import with the same module name.
Manage exposing list
elmq expose src/Main.elm update
elmq expose src/Main.elm "Msg(..)"
elmq unexpose src/Main.elm helper
Granularly add or remove items from the module's exposing list. If the module has exposing (..), unexpose auto-expands to an explicit list then removes the target. expose is a no-op when exposing (..). Neither command ever produces exposing (..).
Rename/move a module
elmq mv src/Foo/Bar.elm src/Foo/Baz.elm
renamed src/Foo/Bar.elm -> src/Foo/Baz.elm
updated src/Main.elm
updated src/Page/Home.elm