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.10.0onPATH(or pointed to byLGX_LG). Install withbrew tap nooga/let-go https://github.com/nooga/let-go && brew install 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 few options to install lgx.
Works on macOS and Linux:
brew install abogoyavlensky/tap/lgxThis installs lgx only; lg still needs to be on PATH (see
Requirements).
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) or a git URL. |
lgx install |
Fetch deps declared in :deps key of lgx.edn. Idempotent. |
lgx run [args...] |
Run :main (or an explicit script) through lg with deps on the source path. Without a script and :main, opens lg's REPL. |
lgx nrepl [--port N] |
Start a REPL with an nREPL server on a random port (or N). Writes .nrepl-port. |
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 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 (reusable:extra-deps/:extra-pathsoverlays) to the command. Applies torun,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,nrepl,build,test, and user tasks). It also prints a+ env …line listing the env vars lgx sets:LG_READ_CLJ=1andLG_SUPPRESS_SOURCE_PATHS_WARNING=1for everylginvocation (the latter silences lg's source-paths transition notice, since lgx always passes an explicit-source-paths), plusLGX_RUN=1onrunpaths.
Both options go before the subcommand: lgx --with dev,test run.
lgx run, 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. |
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.
With no arguments, lgx run execs lg <paths> :main --, injecting a
trailing -- marker so a script can find where its CLI args begin.
lgx run also sets LGX_RUN=1 in the spawned process, so a tool can
tell it is running under lgx run (dev) vs. as a bundled binary.
Prefer keying off LGX_RUN rather than sniffing for --. The ---only
idiom is wrong for a bundled binary, where there is no injected marker and
a -- may legitimately appear inside the user's command (e.g.
myapp run wt git checkout -- file):
(defn- cli-argv [argv]
"Application args, in both dev (lgx run) and bundled-binary modes."
(if (str/blank? (os/getenv "LGX_RUN"))
(rest argv) ; ./bin/myapp <args>
(rest (drop-while #(not= "--" %) argv)))) ; lgx run -- <args>Forms:
lgx run->lg <paths> :main --.lgx run -- foo bar->lg <paths> :main -- foo bar(requires:main).lgx run -r -- foo->lg <paths> -r :main -- foo(lg flags before--).lgx run foo.lg->lg <paths> foo.lg(explicit script, pass-through).lgx run foo.lg -- bar->lg <paths> foo.lg -- bar.lgx run -e '(...)'-> pass-through.lgx runwith no:main->lg <paths>: lg's interactive REPL with the project's deps on the source path.
The spawned lg inherits lgx's stdin/stdout/stderr, so output streams
live and interactive programs (REPL, prompts) work.
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/tmp/, and runs every deftest
against the project's resolved -source-paths. Prints summary results.
A test file may contain deftest forms, fixtures and some helpers.
(ns myapp.list-test
(:require [test :refer [deftest is testing]]
[myapp.format :as fmt]))
(deftest render-list-empty
(testing "empty list"
(is (= "(empty)" (fmt/render-list [])))))The path-to-namespace rule mirrors let-go's resolver:
test/myapp/config_test.lg resolves to myapp.config-test. Underscores
become hyphens; / becomes ..
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, so project namespaces shadow lib namespaces.
: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"
;; 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.
: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 `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
repl {:doc "REPL 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"}]}}}:pathslists project source paths relative to the project root.lgx runprepends them to dependency paths so project namespaces shadow lib namespaces. Missing entries print a warning.:mainnames the default entrypoint script.lgx runsubstitutes it when no script is given;lgx buildbundles it.
:resource-pathslists project-relative directories that hold resources (templates, data files, static assets) reachable from(io/resource "…"). lgx passes them tolgas-resource-paths.lgx runandlgx testmake the resources resolvable at runtime;lgx buildembeds every resource under these roots into the bundled binary, soio/resourcekeeps working with no files alongside the executable.- Missing entries print a warning, same as
:paths. Unlike source paths, resource roots come only from your project (its top level plus any applied contexts/tasks) - dependencies never contribute resource roots.
Each coord uses either a git source or :local/root, never both.
- Git coord.
:git/urlplus one of:git/shaor:git/tag. Tag-pinned coords cache under the tag name itself. HTTPS URLs only (no SSH). - Local coord.
:local/rootpoints at a directory on disk, relative to the project root or absolute. Local deps bypass the gitlibs cache. :deps/root(optional). The subdirectory inside the dep that holds the source. Defaults tosrcif that directory exists, else the repo root. Matches tools.deps':deps/root.
Transitive dependencies. lgx follows transitive deps: after
fetching a dep, it reads that dep's own
lgx.edn (if it ships one) and resolves its :deps too, recursively. Only
a dep's :deps is consulted - its :paths, :main, :tasks, and
:targets describe how to build that project, not how to consume it.
Resolution is breadth-first from your project, and conflicts are first-wins: the first coord seen for a given lib name is kept, and a later, differing coord for the same lib is skipped with a warning on stderr. A coord you list directly therefore overrides the same lib pulled in transitively.
Currently, supports the :bin target only. :out is the output path
relative to the project root; lgx creates the parent directory if
missing.
Tasks replace ad-hoc Makefile or Taskfile recipes for let-go projects.
A task is a sequence of steps; each step is either :sh (shell command)
or :run (invoked like lgx run ... with the project basis). The first
non-zero exit code stops the chain. The lint, ci, and greet tasks
in the reference above show the common forms.
Run a task with lgx <name> (for example, lgx ci). lgx help lists
tasks defined in the current project. Task names are symbols, matching
how they are typed on the command line (context names stay keywords -
see Contexts); they cannot shadow built-in
commands (install, run, nrepl, build, test, new, help,
version, plus reserved add, update, tasks).
When a task has a single step, :do may be written as a step map
instead of a vector. Multi-step tasks use a vector. Step values may be
a string (split on whitespace) or a vector of strings and
:arg/<name> placeholders (see Positional args).
Output is buffered and replayed after each step completes.
A task may contain only :doc, :args, :do, :extra-paths,
:extra-resource-paths, :extra-deps, and :with; any other key is
rejected (so a typo like :extra-dep fails loudly).
A task may declare typed positional CLI args and reference them in
vector-form step values as :arg/<name> keywords or embed them in step
strings as {{<name>}} templates, like the deploy task in the
reference above. lgx deploy prod runs ./deploy.sh 'prod' 'latest',
then lgx run notify.lg prod, then echo deploying vlatest.
Each arg is a map:
:name- required; an unqualified keyword. The placeholder is the matching:arg/<name>keyword.:type- optional, defaults to:string. One of:int,:string, or[:enum "v1" "v2" ...](at least two distinct non-blank strings; CLI values arrive as strings, so enums are string-only).:default- optional; its value must match the type. Args without:defaultare required and must come first; once an arg has:default, every later arg needs one too (CLI values fill positions left to right, so only trailing args can be omitted).
Arity is strict: a missing required arg, a value that fails its type,
or a surplus arg prints the error plus a usage line
(usage: lgx deploy <env> [version]) and exits 1. A task that declares
no :args rejects any CLI args the same way. lgx help shows each
task's signature after its name.
Args can be referenced two ways:
:arg/<name>keyword items in vector-form step values. Each must name a declared arg (checked whenlgx.ednloads). In:shsteps the substituted value is single-quoted, so it always reaches the shell as one word and is never interpreted (lgx greet 'a; echo pwned'echoes the literal text). In:runsteps each vector item is already one argument, so values pass through verbatim.{{<name>}}templates inside any step string - a whole-string command or a string item in a vector. The value is spliced in raw with no quoting added; quote it yourself when it may contain spaces or shell syntax ({:sh "git tag 'v{{version}}'"}). A token that does not name a declared arg is left untouched (which also lets a command carry literal{{...}}text), and a bound value is never re-expanded. A string-form:runvalue is expanded first and then whitespace-split, so a value with spaces becomes several arguments - use the vector form ({:run ["notify.lg" "{{env}}"]}) when it must stay one.
A task may declare extra source paths, resource roots, and dependencies
that apply to that task's :run steps only, like the repl task in
the reference above:
:extra-paths- extra project-root-relative source dirs, same rules as top-level:paths. Appended after the project's:paths.:extra-resource-paths- extra project-root-relative resource roots, same rules as top-level:resource-paths. Appended after the project's:resource-paths.:extra-deps- extra coords, same grammar as top-level:deps(git,:local/root,:deps/root). Fetched on first run like any dep.
These augment the -source-paths and -resource-paths for the task's :run
steps. :sh steps are plain shell and are unaffected. When an :extra-deps
coord names a lib already in the project's top-level :deps, the extra coord
wins for that task only (a silent override) - other commands still use the
project coord.
These per-task extras are the task-private, anonymous form of a
context: use them for one-off extras, and named
:contexts + :with when an overlay is shared across tasks or commands.
A context is a named, reusable overlay of :extra-paths,
:extra-resource-paths, and :extra-deps - the same shape as per-task extras,
lifted to the project top level so it can be applied to any command or shared
across tasks (the :dev and :test contexts in the reference above,
applied by the repl task's :with).
A context map may contain only :extra-paths, :extra-resource-paths, and
:extra-deps, validated by the same rules as the top-level
:paths/:resource-paths/:deps. Apply contexts two ways:
lgx --with dev,test <command>- a global, comma-separated flag that applies the named contexts torun,build,test,install, or a task.installpre-fetches the contexts' deps.:with [:dev]on a task - that task always runs with the named contexts. A global--withon the same invocation is unioned on top.
Default contexts. Two context names are conventions: when defined, :dev
auto-applies to lgx run and lgx nrepl, and :test auto-applies to
lgx test - no --with needed. That is the natural home for nREPL tooling
and dev-only source dirs (:dev) and test helpers (:test), as in the
reference above. build and install never auto-apply contexts, so dev and
test deps stay out of built binaries; task :run steps don't inherit them
either (use the task's :with). An explicit --with layers on top, and
--verbose prints the applied name (+ auto context :dev).
Referencing a context that isn't defined fails loudly: a task's :with is
checked when lgx.edn loads; an unknown --with name errors at runtime,
listing the defined contexts.
Layering. When the same lib name appears in more than one place, the more specific layer wins (last-wins). Lowest → highest precedence:
project :deps / :paths / :resource-paths
→ auto context (:dev for run/nrepl, :test for test; built-in commands only)
→ 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 in the same order with the project's own
first (so project namespaces still shadow lib namespaces) and are
de-duplicated. Resource paths layer identically but, unlike source paths, never
pick up dependency dirs. Like per-task extras, contexts augment only the
-source-paths/-resource-paths for :run steps and the basis commands;
:sh steps are unaffected.
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-vs-bundled mode (see lgx run details). |
LGX_HOME |
~/.lgx |
State root for the gitlibs cache, template cache, and test harness tmp dir. |
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>/
templates/<host>/<owner>/<repo>/<sha>/
tmp/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/with-lib/- fetch-and-require flow using let-go's own repo as the dep.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.
- wtr - a git worktree CLI built with let-go and lgx, using tiny-cli for argument parsing.
In no particular order:
-
:pathssource paths. - Per-coord
:deps/root. - Per-coord
:local/root. -
:tasks- named command shortcuts. -
lgx build- build project binary. -
lgx test- test runner. -
lgx new- project scaffolding. - Transitive dependencies. Follow
lgx.ednfiles inside fetched libs and resolve the union, with first-wins on conflicts. - REPL: bare
lgx runopenslg's REPL;lgx nrepladds an nREPL server on a random or--port-chosen port. -
:extra-deps/:extra-paths- ad-hoc overrides for custom tasks. -
:contexts- environment-specific:extra-pathsand:extra-depsconfigurations. -
--with/:with- ability to extend tasks with contexts. - Non-source resources (let-go-side).
lg's resolver finds.lgand.cljconly; libs that ship templates, JSON, or other assets have no resolution story. Likely needs an upstream change. -
lgx deps- print dependency tree. -
lgx update/lgx update --check- check and update outdated deps. -
lgx clean- clean build artifacts from:targets. -
lgx fmt/lgx lint.
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.