Every command carries its own documentation.

npm scripts, Makefiles and half-remembered dotnet run incantations tell you that a command exists. They never tell you what it does, what arguments it takes, or what it produces. noodge keeps the answer next to the command, where it cannot drift.

noodge  ~/dev/noodge/noodge.yaml

> build              build
  test              
  test:race          Compiles every package.
  check             
  lint               The fastest check that a change has not broken anything
  gen-schema         structurally. Does not produce a binary; use install for
  install            that.
                    
                     Output
                     Nothing on success. Compiler errors on failure.
                    
                     Steps
                     1. go build ./... 

up/down move   enter run   / filter   q quit

That is noodge, run with no arguments, in its own repository. Arrow through the commands, read what each one does, press enter.

The problem

You already have these commands.

You just cannot read them. Here is an ordinary package.json. Every name is a guess, and the only way to learn what gen produces is to run it and find out.

What you have

"scripts": {
  "dev":       "concurrently \"npm:dev:*\"",
  "dev:api":   "tsx watch src/server.ts",
  "gen":       "openapi-gen -i ./spec.yaml -o ./src/api",
  "db:reset":  "docker compose down -v && prisma migrate reset --force",
  "predeploy": "npm run build && node ./scripts/check-env.mjs"
}

Which one do I run first? Does db:reset touch the shared database or the local one? Is gen safe to run on a dirty tree? Nobody knows, so someone asks in Slack, and the answer lives in that thread until it scrolls away.

What noodge gives you

my-api
~/dev/my-api/noodge.yaml

  dev          Runs the whole stack locally: api, worker and web.
  dev:api      Restarts one service without touching the others.
  start:local  Starts the API behind a local HTTPS listener.
  gen          Regenerates the typed API client from spec.yaml.
  db:reset     Drops and recreates the local database, then reapplies every migration.
  db:stop      Stops the local Postgres container, keeping its data.
  deploy       Deploys the current commit to production.

Same commands, same project. The documentation lives in the config next to the thing it describes, so it cannot drift — and it shows up everywhere you might look: the browser, --help, noodge list, and your shell's tab completion.

Get it

Install it in ten seconds.

One static binary. No runtime to install first, nothing to add to your project. The scripts verify the download's SHA-256 against the published checksums before installing anything, and neither needs administrator rights.

$ curl -fsSL https://noodge.dev/install.sh | sh
1
noodge init writes a starter noodge.yaml with two working commands in it.
2
Open it and describe what your commands actually do, in the description and output fields. That is the whole trick.
3
Run noodge. From here on, that is your project's command list.
# yaml-language-server: $schema=https://wimhaanstra.github.io/noodge/schema/v1/noodge.schema.json
version: 1
name: starter

commands:
  hello:
    description: |
      A placeholder so you can check noodge works.

      Replace this with something your project actually needs, and write the
      description for whoever joins next month.
    steps:
      - echo Hello from noodge
    output: One line on stdout.

Already have it? noodge upgrade replaces the binary in place — and refuses politely when a package manager put it there, telling you the right command to use instead. No telemetry: noodge makes no network requests other than an update check you can turn off.

Browse, don't remember

A browser that teaches the command line.

Commands group into families by the part of the name before the colon, so the list stays navigable at forty commands. Give a family a title and a line of explanation and the browser shows those too.

fleetgo-exact-gateway  ~/dev/gateway/noodge.yaml

  Running it            Database
    dev                
    dev:api             The local Postgres the gateway develops against.
    dev:worker         
> Database             2 commands in this group.
    db:reset           
    db:stop            

up/down move   enter run   / filter   q quit
Confirming prints the equivalent command line before running it, so the browser teaches the command line rather than replacing it.
$ noodge start:local --certificate dev.pfx --host localhost --verbose

The browser never runs anything itself. It hands back a command name and its arguments, so the command inherits the real terminal — it keeps its colours, it can prompt for input, and it reports its own exit code. In a pipe, in CI, or anywhere without a terminal, noodge lists instead. That is the correct answer rather than a fallback.

Write it once

The config is the help text.

There is no second place to update, so nothing can fall out of step. What you write on the left is exactly what the next person reads on the right.

  test:
    description: |
      Runs the whole test suite.

      Extra arguments are passed straight
      through to `go test`, so
      `noodge test -- -run TestDiscover -v`
      works without editing this file.
    steps:
      - go test ./...
    output: One line per package, then
      ok or FAIL.
Runs the whole test suite.

Extra arguments are passed straight
through to `go test`, so `noodge test
-- -run TestDiscover -v` works without
editing this file.

Output:
  One line per package, then ok or FAIL.

Steps:
  go test ./...

Arguments after -- are appended to the
last step.

The same description reaches the browser, noodge list, and your shell's tab completion — which is per-directory, so tabbing in one project never offers you another project's commands.

Nothing runs until it is valid

Parameters are declared, not parsed by hand.

Give a parameter a type and noodge checks it before a single process starts. A bad value is a clean refusal with exit code 2, not a stack trace from whatever you wrapped.

stringintnumber boolpathenum requireddefaultpattern
    params:
      - name: certificate
        flag: --certificate
        short: -c
        type: path
        required: true
        description: Path to the .pfx used for the
          local HTTPS listener.
    steps:
      - node serve.ts {{flag host}} {{flag certificate}}
Flags:
  -c, --certificate string   Path to the .pfx used
                             for the local HTTPS
                             listener. (required)
      --host string          Hostname the server
                             binds to.
                             (default: localhost)
  -v, --verbose              Enables per-request
                             logging.
$ noodge start:local --dry-run
noodge: required flag "--certificate" is not set

$ noodge start:local -c nope.pfx --dry-run
noodge: --certificate: nope.pfx does not exist

$ noodge start:local -c dev.pfx --verbose --dry-run
start:local would run in ~/dev/my-api:
  1. node serve.ts --host "localhost" --certificate "dev.pfx" --verbose

with environment:
  NOODGE_COMMAND=start:local
  NOODGE_PARAM_CERTIFICATE=dev.pfx
  NOODGE_PARAM_HOST=localhost
  NOODGE_PARAM_VERBOSE=true

Every refusal happens before any process starts, and every value reaches the wrapped tool shell-quoted. noodge deploy --host 'a && shutdown /s' arrives as one literal argument. It is data, and it stays data. Use --dry-run whenever you want to see the exact command lines without running them.

How each placeholder expands
PlaceholderWhen the parameter isExpands to
{{flag host}}set, or defaulted--host localhost
{{flag host}}optional and unsetnothing at all
{{flag verbose}}a bool, and true--verbose
{{flag verbose}}a bool, and falsenothing at all
{{host}}setlocalhost
{{args}}whatever you typed after --

Steps that do more than run in order

Start things together. Stop them together.

Steps run one after another by default. Work that does not depend on anything else goes in a parallel: group, and each line of output is labelled with who said it.

  dev:
    description: Runs the whole stack locally.
    steps:
      - npm install          # sequential, as usual
      - parallel:
          api: npm run dev:api
          worker: npm run dev:worker
          web: npm run dev:web
$ noodge dev

added 214 packages in 3s

api    | listening on :3000
worker | polling for jobs
web    | vite ready in 412 ms 

Everything a group starts goes into one process tree, so stopping it also stops what those processes started. This is the part that is easy to get wrong on Windows, which has no process groups: killing npm there leaves node holding the port. noodge uses a Job Object with kill-on-close, so the whole tree goes.

And it asks before anything you cannot undo.

  db:reset:
    description: Drops and recreates the local database.
    confirm: true                  # the default prompt
    steps:
      - docker compose down -v

  deploy:
    description: Deploys to production.
    confirm: This deploys to PRODUCTION. Continue?
    steps:
      - ./scripts/deploy.sh --env prod
$ noodge db:reset
Really run "db:reset"? [y/N]  

$ noodge deploy
This deploys to PRODUCTION. Continue? [y/N] n

$ noodge deploy --yes
# no prompt — for CI and scripts

Declining exits 2, so a script that says no fails rather than carries on quietly. Step exit codes otherwise pass through untouched — noodge never invents one.

When you get it wrong

It tells you where, and what to do about it.

Every problem in one pass, each with a line, a column and a hint. Not the first one, then a rerun, then the next one.

noodge.yaml:8:15: error: flag "-host" must start with two dashes
  hint: write it as --host. This is only how you type it to noodge; a step is
        still free to write -host {{host}} to pass it on with a single dash
noodge.yaml:10:13: error: parameter "mode" is an enum but lists no values
  hint: add a values: list of the allowed values
noodge.yaml:18:9: error: step 1 refers to {{nosuch}}, but command "bad" declares no
  parameter called "nosuch"
  hint: add it under params:, or correct the name
noodge.yaml:14:13: error: parameter "unused" is declared but no step uses it
  hint: add {{flag unused}} to a step, or remove the parameter
noodge.yaml:20:3: error: command "nosteps" has no steps, so there is nothing to run
  hint: add a steps: list

noodge.yaml: 5 errors, 0 warnings

The JSON Schema ships with the binary, and noodge init puts a modeline at the top of the file pointing at it — so your editor catches most of this before you save. Run noodge schema to print it.

The rest of it

Small things you notice on day two.

None of these are why you would install it. All of them are why you would keep it.

Found, not configured

Walks up from your working directory to the nearest config, the way git finds .git, and stops at a repository boundary. Commands run in the directory holding the config, not wherever you happened to be standing.

noodge build   # from any subdirectory

Correct without a terminal

In a pipe, in CI, or anywhere there is no TTY, noodge lists instead of opening the browser. There is a machine-readable form for editors and scripts.

noodge list --json

Values stay data

Every parameter value is shell-quoted on the way out, so a value that looks like a shell operator reaches the wrapped tool as one literal argument. There is no injection surface.

--host "a && shutdown /s"

Completion that knows the project

Tab through this directory's commands, descriptions included. bash, zsh, fish, pwsh and Windows PowerShell — and it will set itself up for you.

noodge completion install zsh

Windows done properly

Two quoting parsers reconciled, because cmd.exe escapes an embedded quote as "" while the C runtime expects \". A Job Object with kill-on-close so nothing keeps the port. Each step its own process, so no && is needed.

windows · macos · linux · amd64 · arm64

One static binary

No runtime, no cgo, nothing added to your project but a YAML file. Every glyph in the interface is ASCII, and the colours are legible on a light and a dark background alike.

noodge version

No telemetry. noodge collects nothing and phones home to nobody. The only network request it ever makes is an update check, and NOODGE_NO_UPDATE_CHECK=1 turns that off.