A package and project manager for let-go: git-based dependency manager, runner, build tool, test runner, scaffolder, and task runner, in one binary.
lgx new myapp # scaffold a project
cd myapp
lgx install # install deps from lgx.edn
lgx run # fetch deps, run :main
lgx nrepl # run nrepl
lgx build # bundle a standalone binary
lgx test # run tests under test/
lgx <task> # run a custom task from lgx.ednlg>=1.11.0onPATH(or pointed to byLGX_LG). Install it withbrew install nooga/tap/let-go.gitonPATH. (lgx uses it to clone, fetch, and check out deps)
Prebuilt binaries for linux_amd64, linux_arm64, darwin_amd64, and
darwin_arm64 are attached to each GitHub Release.
There are a few ways to install lgx.
Works on macOS and Linux:
brew install abogoyavlensky/tap/lgxThis installs lgx only. Install let-go (lg) separately with
brew install nooga/tap/let-go.
With mise
mise use -g github:abogoyavlensky/lgx@latestOr pin per project in .mise.toml:
[tools]
"github:nooga/let-go" = "latest"
"github:abogoyavlensky/lgx" = "latest"Installs the latest release to ~/.local/bin/lgx:
curl -fsSL https://raw.githubusercontent.com/abogoyavlensky/lgx/master/scripts/install.sh | bashSee the script's README for options.
Create a new project and run it:
lgx new hello
cd hello
lgx runlgx new scaffolds from the base template;
-t/--template selects another (see lgx new templates).
lgx run resolves :main from lgx.edn, fetches any deps under
$LGX_HOME/gitlibs/, then execs lg.
| Command | What it does |
|---|---|
lgx new <name> [-t <tpl>] |
Scaffold a new let-go project into ./<name> from a built-in template (base, cli, lib) or a git URL. |
lgx install |
Fetch deps from :deps into the gitlibs cache. Idempotent. Useful for editor navigation. |
lgx run [args...] |
Run :main through lg with deps on the source path. Put a script or lg flags before -- to drive lg yourself; program args go after --. With no :main and no script, errors (use lgx repl for a REPL). |
lgx repl |
Start lg's built-in REPL with the project's deps on the source path. Auto-applies the :dev and :test contexts when defined. |
lgx nrepl [--port N] |
Start a REPL with an nREPL server on a free OS-assigned port (or N). Writes .nrepl-port. Auto-applies the :dev and :test contexts when defined. |
lgx build [args...] |
Bundle :main into :targets/:bin/:out in lgx.edn via lg -b. |
lgx test [file] |
Run *_test.lg / *_test.cljc / *_test.clj files under test/. With <file>, run just that file. |
lgx <task> [args...] |
Run a custom task defined under :tasks in lgx.edn, binding any declared positional :args. |
lgx or lgx help |
Show usage, including project tasks if an lgx.edn is found. |
lgx version |
Print version. |
Options:
--with <a,b,...>applies one or more named contexts to the command. Works withrun,repl,nrepl,build,test,install, and user tasks; on a task it unions with the task's own:with.--verboseprints the resolvedlginvocation before running (applies torun,repl,nrepl,build,test, and user tasks). It first prints a+ lg <version> (<path>)line naming thelgit resolved - the version fromlg -vand the full binary path, which reflects anLGX_LGoverride. It also prints a+ env …line listing the env vars lgx sets.
Both options go before the subcommand: lgx --with dev,test run.
lgx run, repl, nrepl, build, test, and tasks find the nearest
lgx.edn by walking up from the current directory.
lgx new takes -t/--template with a built-in template name or a git URL:
lgx new myapp # base template (the default)
lgx new myapp -t cli # built-in template by name
lgx new myapp -t https://github.com/user/my-templateBuilt-in templates are pinned to a latest revision:
| Name | Repo | Purpose |
|---|---|---|
base |
lgx-template-base | Minimal let-go app. |
cli |
lgx-template-cli | Command-line app skeleton. |
lib |
lgx-template-lib | Library project skeleton. |
A URL template uses the repo's latest default-branch HEAD and caches the checkout by sha under
$LGX_HOME/templates/.
Template URLs must be https://host/owner/repo (or file:///path/to/repo
for local development); SSH forms like git@host:owner/repo are not
supported. To make a repo a template, put the literal token projectname
in paths and file contents wherever the project name belongs; lgx new
replaces it with the underscored name (my_app) in paths and the
hyphenated name (my-app) in contents.
lgx run runs :main. Put a script or lg flags before -- and you drive
lg yourself (:main isn't added); your program's args go after -- and
arrive in *command-line-args* (requires lg >= 1.11.0). With no :main and
no script it errors: set :main, name a script, or start
lgx repl.
It also sets LGX_RUN=1 in the spawned process, so a tool can tell it runs
under lgx run (dev) vs. as a bundled binary. The spawned lg inherits lgx's
stdio, so live output and interactive programs (REPL, prompts) work.
Argument forms
lgx run->lg <paths> <main>(*command-line-args*isnil).lgx run -- foo bar->lg <paths> <main> foo bar->*command-line-args*is("foo" "bar").lgx run other.lg->lg <paths> other.lg(explicit script, no:main).lgx run other.lg -- bar->lg <paths> other.lg bar->*command-line-args*is("bar").lgx run -e '(...)'->lg <paths> -e '(...)'(no:main).lgx run -- a -- b->*command-line-args*is("a" "--" "b")(only the first--is the separator).
lgx repl opens lg's built-in REPL with the project's deps and :paths on
the source path, no script and no :main. It auto-applies the :dev and
:test contexts, exactly like lgx nrepl; the difference is that repl binds
no port and writes no .nrepl-port, so it is the zero-footprint choice for a
quick session. Reach for lgx nrepl when an editor needs to connect.
It takes no arguments of its own (--with/--verbose are the usual leading
options). For a scratch script or lg flags with your deps on the path, use
lgx run <script> or lgx run -e '(...)'.
lgx build is shortcut for lg <paths> [extra-args...] -b <:out> <:main>.
Extra args go before -b, so cross-OS bundling works as:
lgx build -bundle-base /path/to/lgBoth :main and :targets/:bin are required.
lgx test walks test/ for *_test.lg / *_test.cljc / *_test.clj files, generates
a one-shot harness under $LGX_HOME/test-runner/, and runs every deftest
against the project's resolved -source-paths. Prints summary results.
lgx.edn lives at the project root. The smallest valid file:
{}Every key is optional. The annotated reference below shows all of them with their possible values; the sections that follow spell out each key's rules in detail.
{; Source dirs, relative to the project root. Prepended to dependency paths.
:paths ["src"]
; Resource roots for (io/resource "..."): on the path for run/test,
; embedded into the binary by build.
:resource-paths ["resources"]
; Default entrypoint: `lgx run` runs it, `lgx build` bundles it. Does not have to be in the `:paths`
:main "main.lg"
; The let-go version this project targets. When set, run/build/test check it
; against the lg on PATH and warn. lgx does not install lg itself.
; If `LGX_FETCH_LET_GO_SOURCE=1` env var set, `lgx install` fetches
; the matching let-go source for editor navigation.
:lg-version "1.11.0"
; Git or local deps. A dep's own :deps are resolved too (first-wins).
:deps
{some-user/let-go-async {:git/url "https://github.com/some-user/let-go-async"
:git/tag "v0.2.0"} ; pin by tag...
org.clojure/tools.cli {:git/url "https://github.com/clojure/tools.cli"
:git/sha "0123456789abcdef0123456789abcdef01234567" ; ...or by sha
:deps/root "src/main/clojure"} ; source subdir (default "src")
my/lib {:local/root "../my-lib"}} ; local dir, no gitlibs cache
; Build output for `lgx build`. :bin is the only target; :out is relative to
; the project root (lgx creates the parent dir if missing).
:targets {:bin {:out "bin/myapp"}}
; Named overlays of extra paths/deps. Apply with `lgx --with dev,test <cmd>`
; or a task's :with; :dev auto-applies to run/nrepl, :test to nrepl and
; `lgx test`.
:contexts
{:dev {:extra-paths ["dev"] ; appended after :paths
:extra-resource-paths ["dev-resources"] ; appended after :resource-paths
:extra-deps {nrepl {:git/url "https://github.com/x/nrepl"
:git/tag "v1"}}} ; same grammar as :deps
:test {:extra-paths ["test-support"]}}
; Custom commands: `lgx <task> [args...]`. A step is {:sh ...} (shell)
; or {:run ...} (like `lgx run ...`); a string value splits on whitespace.
:tasks
{fmt {:doc "Lint the project" ; :doc shows up in `lgx help`
:do {:sh "cljfmt fix"}} ; single step: bare map
ci {:doc "Lint, then test" ; multi-step: vector,
:do [{:sh "cljfmt check"} ; stops at first failure
{:run "scripts/check.lg"}]}
greet {:doc "Run main with a fixed arg"
:do [{:run ["main.lg" "--" "world"]}]} ; vector form: explicit argv
deploy {:doc "Deploy the app"
:args [{:name :env ; typed positional CLI args
:type [:enum "prod" "staging"]} ; :string (default), :int, [:enum ...]
{:name :version
:type :string
:default "latest"}] ; :default makes an arg optional
:do [{:sh ["./deploy.sh" :arg/env :arg/version]} ; :arg/<name> fills
{:run ["notify.lg" :arg/env]} ; in declared args
{:sh "echo deploying v{{version}}"}]} ; {{name}} expands in strings
console {:doc "Custom REPL entrypoint with dev tooling"
:with [:dev] ; always apply these contexts
:extra-paths ["repl"] ; task-private extras:
:extra-resource-paths ["repl-resources"] ; same shape as a context,
:extra-deps {seme-extra-dep {:git/url "https://github.com/some-extra/dep"
:git/tag "v1"}}
:do [{:run "dev/repl.lg"}]}}}:pathsprepends project source dirs to dependency paths, so project namespaces shadow lib namespaces of the same name.:mainis substituted bylgx runwhen no script is given, and bundled bylgx build. It need not live under:paths.:resource-pathsare passed tolgas-resource-pathsfor run/test and embedded into the binary bylgx build, so(io/resource "…")keeps working with no files beside the executable. Unlike source paths, resource roots come only from your project, never from dependencies.- Missing
:paths/:resource-pathsentries print a warning.
lgx never installs or manages lg (use mise/brew for that), but when
:lg-version is set it checks the lg on PATH: a mismatch warns on
run/nrepl and fails on build/test, where a wrong-runtime artifact or
test verdict matters. A dev or unparseable installed version is skipped, and
LGX_SKIP_VERSION_CHECK bypasses the check. With LGX_FETCH_LET_GO_SOURCE set,
lgx install also fetches the matching let-go source (not the binary) into
$LGX_HOME/let-go/source/<version>/ for editor navigation.
Each coord is either a git source (:git/url plus one of :git/sha/:git/tag,
HTTPS only) or a :local/root dir - never both. Local deps bypass the gitlibs
cache. :deps/root names the source subdir inside the dep (defaults to src if
present, else the repo root; matches tools.deps).
Transitive deps. After fetching a dep, lgx reads that dep's own lgx.edn
(if any) and resolves its :deps recursively - only :deps, never a dep's
:paths/:main/:tasks. Resolution is breadth-first and first-wins: the
first coord seen for a lib name is kept, and a later differing coord is skipped
with a warning. A coord you list directly overrides the same lib pulled in
transitively.
Tasks replace ad-hoc Makefile/Taskfile recipes. A task is a step or a vector of
steps; each step is :sh (shell) or :run (an explicit argv to lg with the
project basis). A :run step names its own script - it never substitutes
:main - but, like lgx run, drops the first --. The first non-zero exit
stops the chain; output is buffered and replayed after each step. A single-step
:do may be a step map instead of a vector.
Run a task with lgx <name>; lgx help lists the project's tasks. Names are
symbols (context names stay keywords) and can't shadow built-ins (install,
run, nrepl, build, test, new, help, version, plus reserved add,
update, tasks). A task accepts only :doc, :args, :do, :with,
:extra-paths, :extra-resource-paths, and :extra-deps; any other key is
rejected (so a typo like :extra-dep fails loudly).
A task may declare typed positional args and reference them in steps, like the
deploy task above. Each arg is a map:
:name- required, an unqualified keyword; the placeholder is:arg/<name>.:type-:string(default),:int, or[:enum "v1" "v2" ...].:default- optional; must match the type. Args without a default are required and come first; once one arg has a default, every later arg needs one too (CLI values fill positions left to right).
Arity is strict: a missing required arg, a wrong type, or a surplus arg prints a
usage line and exits 1. Reference args two ways: as :arg/<name> keyword
items in vector-form steps (single-quoted in :sh so shell-safe, verbatim in
:run), or as {{name}} templates in any step string (spliced raw - quote
it yourself when it may contain spaces). Unknown tokens are left untouched, and a
bound value is never re-expanded.
A task may carry its own extras (same rules and grammar as the top-level
:paths/:resource-paths/:deps), appended after the project's and applied to
that task's :run steps only - :sh steps are plain shell and unaffected, like
the console task above. An :extra-deps coord that names a project dep wins
for that task only. These are the anonymous form of a context; use
named :contexts + :with when an overlay is shared across tasks.
A context is a named, reusable overlay of :extra-paths,
:extra-resource-paths, and :extra-deps - the per-task extras lifted to the
project top level so any command or task can apply them (the :dev/:test
contexts applied by the console task's :with above). Apply two ways:
lgx --with dev,test <command>- a global flag applied torun,build,test,install, or a task (installpre-fetches the contexts' deps).:with [:dev]on a task - always applied; a global--withunions on top.
Default contexts. By convention :dev auto-applies to lgx run, :test to
lgx test, and lgx nrepl applies both - no --with needed. build and
install never auto-apply, so dev/test deps stay out of binaries; task :run
steps don't inherit them either (use the task's :with). On a lib-name
collision the more specific layer wins. Referencing an undefined context fails
loudly (a :with at load time, an unknown --with at runtime).
Layering. When a lib name appears in more than one layer, the most specific wins (lowest to highest):
project :deps / :paths / :resource-paths
→ auto context (:dev for run, :test for test, both for nrepl)
→ task :with contexts (in order)
→ CLI --with contexts (in order)
→ task inline :extra-deps / :extra-paths / :extra-resource-paths (highest)
Source and resource paths concatenate project-first (so project namespaces still shadow libs) and de-duplicate; resource paths never pick up dependency dirs.
lgx completion <shell> prints a completion script for bash, zsh, or
fish. TAB then completes the built-in commands and the current
project's tasks from lgx.edn. For a task arg typed as [:enum ...],
TAB at that argument completes the declared values (those made of shell-safe
characters; a value containing spaces or shell metacharacters is skipped, but
still works when typed by hand).
Bash (add to ~/.bashrc):
source <(lgx completion bash)Zsh, either sourced (add to ~/.zshrc):
source <(lgx completion zsh)or saved on your fpath (run once; assumes ~/.zfunc is on fpath):
lgx completion zsh > ~/.zfunc/_lgxFish (run once):
lgx completion fish > ~/.config/fish/completions/lgx.fish| Variable | Default | Purpose |
|---|---|---|
LGX_LG |
lg on PATH |
Path to the lg binary lgx invokes. Useful when testing an unreleased build. |
LGX_RUN |
(set by lgx) | Set to 1 in the process spawned by lgx run. Read it to detect dev (lgx run) vs. bundled-binary mode - e.g. to enable dev-only behavior. Not needed for argument parsing; read *command-line-args* for that (see lgx run details). |
LGX_HOME |
~/.lgx |
State root for the gitlibs cache, the let-go source cache, the template cache, and the test-runner harness dir. |
LGX_SKIP_VERSION_CHECK |
(unset) | Set to any non-empty value to bypass the :lg-version compatibility check on run/nrepl/build/test. |
LGX_FETCH_LET_GO_SOURCE |
(unset) | Set to any non-empty value to make lgx install fetch the let-go source matching :lg-version into $LGX_HOME/let-go/source/<version>/. The source feeds editor diagnostics - an LSP server navigating into let-go's core/stdlib. Off by default, since most users don't run such tooling and wouldn't expect the extra clone. |
LGX_NO_COLOR |
(unset) | Set to any non-empty value to disable colored status headers. lgx prints a green => header before install/build/test/new and a purple => Running task <name>... header before custom tasks, on stderr. lgx run prints no header, so it mirrors the built binary. |
LGX_TEMPLATE_BASE_URL |
template repo URL | Override the source repo of the built-in base template. |
LGX_TEMPLATE_BASE_SHA |
pinned sha | Override the revision of the built-in base template. |
$LGX_HOME/
gitlibs/<host>/<owner>/<repo>/<ref>/
let-go/source/<version>/
templates/<host>/<owner>/<repo>/<sha>/
test-runner/lgx-test-<version>.lg
<ref> is the sha for :git/sha coords, or the tag with / replaced
by _ for :git/tag coords. lgx test rewrites the version-stamped
harness on each run. lgx new reuses the template cache after the first
clone.
examples/hello/- no-deps script.examples/server/- simple HTTP server with a ruuter lib.examples/local-dep/- project plus sibling library using:local/root.examples/clojure-libs/- survey of real Clojure libraries on let-go.
- tiny-cli - a CLI parser library for let-go, distributed as a git dep.
- tiny-tui - a minimal TUI lib for let-go
- wtr - a git worktree CLI built with let-go and lgx, using tiny-cli for argument parsing
- skl - a minimal interactive agent skills installer
- rite - a project task runner with built-in let-go
- frame - a declarative project templater
- tiny-cli - cli parsing
- tiny-tui - TUI widgets
- ruuter - simple router
- medley - various useful helpers
- bond - spying/stubbing for tests
- integrant - micro-framework for data-driven architecture
- dev.weavejester/dependency - A data structure for graphs
- org.clojure/tools.cli - cli parsing
- metosin/malli - data-driven schema validation and coercion
Shipped so far: source paths, per-coord :deps/root/:local/root, :tasks,
lgx build/test/new, transitive deps, lgx repl/nrepl, :contexts with
--with/:with, and let-go-side resources. Next:
- Install transitive dependencies from Clojure libs that use
deps.ednorproject.clj -
lgx install --all- fetch deps from all contexts and tasks in lgx.edn. -
lgx deps- print the dependency tree. -
lgx update/lgx update --check- check and update outdated deps.
make build # produces bin/lgx, a bundled standalone binary
make dev-install # runs `lg lgx.lg install` from the lgx project root
make dev-run # runs examples/hello/main.lg through dev `lg lgx.lg ...`
make test # runs all tests through dev `lg lgx.lg ...`
make clean # remove bin/lgx and all build artifacts
Run from the lgx project root during dev so the resolver finds
lgx/*.lg. Once built, bin/lgx works from any directory. Point at a
non-default lg binary with LGX_LG=/path/to/lg.
MIT License. Copyright (c) 2026 Andrey Bogoyavlenskiy.