CLI Reference
The nudo CLI runs type inference on .js, .mjs, and .ts files. Install globally or run via npx:
pnpm add -g @nudojs/cli
# or
npx @nudojs/cli infer ./src/utils.js
Commands
| Command | Purpose |
|---|---|
nudo infer | Infer types from files or directories |
nudo check | Check a file or directory for type errors (error-level diagnostics exit 1) |
nudo interface | Print/emit/draft each function's effective interface — [handwritten] / [generated] / [implicit] layers; --draft for code-first reviewable contracts (alias nudo refine) |
nudo types | Type-as-computation view: term + constraints from Abs algebra |
nudo test | Run @nudo:case directives as assertions (exit 1 on failure) |
nudo doctor | Health-check files: call-site solidification drift, analysis errors, uncovered functions |
nudo generate | Generate runtime validators from inferred types |
nudo emit | Emit .d.ts declarations (npm compatibility exit) |
nudo guard | Generate runtime type-guard functions |
nudo watch | Watch a file or directory and re-run inference on changes |
nudo harvest | Convert @types/<pkg> declarations into a Nudo env file; --auto reports analysis-path auto-harvest |
nudo infer
Infer types from a single file or from every inference target under a directory.
nudo infer <file> [options]
Arguments:
| Argument | Description |
|---|---|
<file> | Path to a .js, .mjs, or .ts file (relative or absolute). Directories are also accepted — recursively scanned for inference targets (.js/.mjs/.ts, excluding .d.ts and .tsx). TypeScript type annotations are stripped at the parser layer and the file is inferred with JS semantics. |
Options:
| Option | Description |
|---|---|
--dts | Generate a .d.ts declaration file next to the source file |
--loc | Show source locations (file:line:column) in the output |
--json | Output results as structured JSON — requires a single file; a directory target is an error |
--callsites <paths...> | Usage-site files or directories (tests/apps) to harvest real call shapes from; their calls to this file's exports become synthesized call@L cases — see Call-Site Discovery |
--emit-cases [mode] | Debug only — write the synthesized call-site cases back into the analyzed file as @nudo:case directives (reserved call@ name prefix). Not the contract product (that is *.nudo.js / nudo interface). Omit the value for add (only fills in functions that have no case directives yet) or pass =update to re-synchronize previously generated directives — see Persisting cases as directives |
--dry-run | With --emit-cases: print a unified diff instead of writing to disk |
--exit-on-diff | With --dry-run: exit with code 1 when the diff is non-empty — a CI gate for usage-site drift |
Output format:
- One section per function (
=== name ===); functions from imported modules are shown under a--- path (imported) ---header - Observed call sites:
call@L<line>: (arg1, arg2, ...) => result;@nudo:casedebug witnesses print asdebug "name": (…) => … - Functions without call sites still get an
entry@Lobservation withunknownparameters plus a# no call sites foundnote - Optional
throws typewhen the case may throw - If multiple cases: observed type printed as
Observed: type, simplified by absorption — a literal whose base type is already in the union is absorbed (e.g.2 | -9 | numbercollapses tonumber); pure-literal unions keep all members - Diagnostics, if any, are printed in a trailing
Diagnostics:section as[severity] path:line:column message (code) - With
--dts: writes<basename>.d.tsin the same directory and printsGenerated: <basename>.d.ts - With
--emit-cases: a trailing emission summary —Emitted cases → <file> (N directive(s) across M function(s))after writing to disk,Would emit cases → <file> (dry run)followed by a unified diff with--dry-run, orNo changes.when the source is already in sync — each followed by per-function lines:fn: case namesfor written functions,fn: reasonfor skipped ones (e.g.already-generated)
Example:
nudo infer math.js
=== subtract ===
call@L6: (5, 3) => 2
call@L7: (1, 10) => -9
Observed: 2 | -9
nudo infer math.js --dts --loc
=== subtract (math.js:1:0) ===
call@L6: (5, 3) => 2
call@L7: (1, 10) => -9
Observed: 2 | -9
Generated: math.d.ts
JSON output (--json):
nudo infer math.js --json
{
"version": 1,
"file": "math.js",
"summary": {
"functions": 1,
"externalFunctions": 0,
"cases": 3,
"diagnostics": 0
},
"functions": [
{
"name": "subtract",
"loc": {
"start": {
"line": 6,
"column": 0
},
"end": {
"line": 8,
"column": 1
}
},
"entryOnly": false,
"cases": [
{
"name": "positive numbers",
"args": [
"5",
"3"
],
"result": "2",
"throws": null,
"source": "directive",
"intension": {
"display": "subtract: (a: A1, b: A2) => number = (A1 - A2)",
"abs": "2 #exact",
"absMultiline": "subtract\n 2\n conf: exact",
"term": "(A1 - A2)",
"conf": "exact"
}
},
{
"name": "negative result",
"args": [
"1",
"10"
],
"result": "-9",
"throws": null,
"source": "directive",
"intension": {
"display": "subtract: (a: A1, b: A2) => number = (A1 - A2)",
"abs": "-9 #exact",
"absMultiline": "subtract\n -9\n conf: exact",
"term": "(A1 - A2)",
"conf": "exact"
}
},
{
"name": "symbolic",
"args": [
"number",
"number"
],
"result": "number",
"throws": null,
"source": "directive",
"intension": {
"display": "subtract: (a: A1, b: A2) => number = (A1 - A2)",
"abs": "number #partial",
"absMultiline": "subtract\n number\n conf: partial",
"term": "(A1 - A2)",
"conf": "partial"
}
}
],
"combined": "number"
}
],
"diagnostics": []
}
Field notes:
version/file/summary— schema version (1), the analyzed file path, and totals (functions,externalFunctions,cases,diagnostics).cases[].source— where the case came from:"directive"for cases evaluated from@nudo:casedirectives (hand-written or written back bynudo generate, both re-evaluate the directive);"callsite"for cases synthesized from recorded call sites (call@L…);nullforentry@Lfallback cases withunknownparameters.cases[].intension— the lossless Abs signature of the case (re-evaluated withunknownparameters):display,abs,absMultiline,term, andconf; present when the Abs path produced it.combined— the union of all case results, simplified by absorption.entryOnly—truewhen the function received no call-site records, so its signature comes from anentry@Lfallback case withunknownparameters.diagnostics— the same diagnostics shown in the text output'sDiagnostics:section (withrange,severity,message, andcode).
nudo check
Check a file or directory for type errors. Prints one line per diagnostic in the form [severity] path:line:column message (code) and exits with code 1 when any error-level diagnostic is found — warnings alone exit 0.
nudo check <file>
nudo check <directory>
nudo check <file> --callsites <usage-sites...>
Arguments:
| Argument | Description |
|---|---|
<file> | Path to a .js, .mjs, or .ts file (relative or absolute) |
<directory> | Recursively check every inference target under the directory (--json requires a single file) |
Options:
| Option | Description |
|---|---|
--json | Emit stable CheckJson (CI / Agent contract; single file only) |
--verbose | Expand signatures to full Abs (term / pred / conf); default is a one-line human summary |
--callsites <paths...> | Usage-site files (tests/apps): inject their call records so cross-file domain evidence can produce nudo:interface-domain-exceeds |
Example:
nudo check src/broken.js
[warning] src/broken.js:2:9 Cannot resolve 'name' on unknown value (nudo:unknown-recv)
[warning] src/broken.js:2:9 Cannot resolve 'toUpperCase' on unknown value (nudo:unknown-recv)
- A file with no diagnostics prints
No issues found.and exits0. - When the origin of a bad value is known, a hint line follows:
→ value originates at line:column. - A refinement violation is error-level, so
checkexits1:
[error] src/set.js:12:0 setDelay[ms]: 实参 ⊭ 前置 (nudo:constraint-violated)
actual: 0 #exact
expected: ms > 0
nudo interface
Print each function's effective interface with its source layer — [handwritten] (source @nudo:refine/@nudo:interface ∪ sidecar binding), [generated] (persisted @generated segment), or [implicit] (call-site inference). Print only by default; --emit persists inferred domains as sidecar @generated segments; --draft generates a reviewable contract draft from existing code (code-first / migration). Alias: nudo refine.
nudo interface <paths...> [--callsites <paths...>]
nudo interface --emit <paths...> [--fn <name>] [--all] [--dry-run] [--exit-on-diff] [--callsites <paths...>]
nudo interface --draft <paths...> [--write] [--fn <name>] [--dry-run] [--callsites <paths...>]
Arguments:
| Argument | Description |
|---|---|
<paths...> | File(s) or directory(s) — at least one required (sidecar *.nudo.js/*.nudo.ts and *.nudo.draft.js targets are skipped) |
Options:
| Option | Description |
|---|---|
--emit | Write/update @generated segments instead of printing (update mode: strips and rewrites generated segments, idempotent) |
--draft | Generate a reviewable interface draft from existing code (prints a *.nudo.draft.js module — not auto-bound) |
--write | With --draft: write/update <file>.nudo.draft.js (never overwrites handwritten *.nudo.js) |
--fn <name> | With --emit/--draft: only these export names (repeatable) |
--all | With --emit: target every top-level export (explicit opt-in) |
--dry-run | With --emit or --draft --write: print instead of writing to disk |
--exit-on-diff | With --emit: exit 1 when the sidecar would change (CI gate) |
--callsites <paths...> | Usage-site files (tests/apps): their calls to this file's exports feed the domain evidence (domain roots with no in-file call sites) |
Exit codes: 0 — printed/emitted/drafted successfully (including "no interface changes"); 1 — usage error (no paths, --write without --draft, --emit+--draft together), --exit-on-diff with a non-empty diff, or an emit issue such as nudo:interface-name-clash (handwritten binding wins, write skipped).
Example (draft — code-first):
nudo interface --draft double.js --write
double.js
double [draft callsite/callsite] fn({ x: number() }, number())
Draft written → double.nudo.draft.js
review, then copy accepted exports into double.nudo.js
Handwritten bindings are listed as skipped and never overwritten. The draft file is not ambient-loaded until you copy exports into *.nudo.js.
Example (print):
nudo interface calc.js
calc.js
addTax [handwritten] (x: number().gt(1)) → number()
greet [handwritten] (name: union(lit("ada"), lit("bob"))) → string()
Example (emit + drift):
nudo interface --emit double.js --fn double
Updated double.js → double.nudo.js
written: double
re-run `nudo check double.js` to see the persisted interfaces in action
// double.nudo.js
// @generated by nudo — do not edit; regenerate with `nudo interface --emit`
// source: double.js:double
export const double = fn({ x: lit(4) }, lit(8));
After the source changes, --dry-run --exit-on-diff shows the pending update and exits 1:
[dry-run] would update double.js:
--- a/double.nudo.js
+++ b/double.nudo.js
@@ -1,4 +1,4 @@
// @generated by nudo — do not edit; regenerate with `nudo interface --emit`
// source: double.js:double
-export const double = fn({ x: lit(4) }, lit(8));
+export const double = fn({ x: lit(6) }, lit(12));
nudo types
Type-as-computation view: show term + constraints from Abs algebra (not just extensional shape). Accepts a single file or a directory (recursively).
nudo types <file> [--fn <name>] [--assume <pred...>] [--generalize]
nudo types <directory> [--assume <pred...>] [--generalize]
| Option | Description |
|---|---|
--fn <name> | Only analyze this function |
--assume <pred...> | Assume constraints, e.g. x>0 y>=1 |
--generalize | Show polymorphic signatures via symbolic execution |
nudo test
Run @nudo:case directives as assertions. Cases with => expected are checked with subtype semantics; failures exit 1. Cases without an expected type are reported as unchecked. Accepts a file or directory.
nudo test <file>
nudo test <directory>
nudo doctor
Health-check source files: call-site solidification drift (with --callsites), analysis errors, and functions without cases. Exits with code 1 when any file has drift or errors — uncovered functions are informational only and never change the exit code.
nudo doctor [paths...] [options]
Arguments:
| Argument | Description |
|---|---|
[paths...] | File(s) or directory(s) to check. Directories are scanned recursively for inference targets (.js/.mjs/.ts, excluding node_modules); defaults to the current directory |
Options:
| Option | Description |
|---|---|
--callsites <paths...> | Usage-site files or directories (tests/apps). With it, doctor re-runs the same re-solidify chain as infer --emit-cases=update for every file and reports drift when the generated call@ directives would change — see Health Checks and CI Drift Gating |
--json | Output the report as structured JSON |
What is checked:
- Drift — with
--callsites: the generatedcall@directives no longer match what the usage sites would produce today (same chain asinfer --emit-cases=update, judged per file) - Errors — analysis failures, including missing files and syntax errors
- Entry-only count / uncovered functions — informational: how many functions have no call evidence;
uncovered(zero cases) is currently always empty in practice — every non-skipped function gets at least anentry@Lfallback case — and never affects the exit code
Exit codes:
| Code | Meaning |
|---|---|
0 | No drift and no errors — uncovered functions alone still exit 0 |
1 | Any file has drift or an error |
Examples:
Healthy (exit code 0):
nudo doctor lib.js --callsites test.js
lib.js
· 3 function(s), 1 entry-only
Summary: 1 file(s) · 0 drift · 0 error(s) · 0 uncovered function(s)
Result: OK (uncovered function(s) are informational only)
Drift — the frozen call@ directives are stale (exit code 1); the refresh command is printed ready to copy:
nudo doctor lib.js --callsites test.js
lib.js
· 3 function(s), 1 entry-only
✗ drift: 5 directive(s) changed (+3 new, -2 removed) — refresh with: nudo infer lib.js --callsites test.js --emit-cases=update
Summary: 1 file(s) · 1 drift · 0 error(s) · 0 uncovered function(s)
Result: FAIL (drift or errors found)
Analysis errors fail the run the same way (exit code 1):
missing.js
✗ error: File not found: <path>
broken.js
✗ error: Unexpected token (1:18)
In CI, gate a whole source tree on drift in one command:
nudo doctor src/ --callsites tests/
JSON output (--json):
{
"ok": false,
"files": [
{
"file": "lib.js",
"functions": 3,
"entryOnly": 1,
"uncovered": [],
"drift": {
"added": 3,
"removed": 2
}
}
],
"summary": {
"files": 1,
"drift": 1,
"errors": 0,
"uncovered": 0
}
}
Field notes:
ok— matches the exit code:falseexactly when any file drifted or errored.files[]— one entry per file:file,functions,entryOnly,uncovered;drift: { added, removed }appears only on drifting files,erroronly on failed ones.summary— totals:files,drift,errors,uncovered.
nudo generate
Generate runtime validators from inferred types. Output is printed to stdout.
nudo generate <file> [options]
Arguments:
| Argument | Description |
|---|---|
<file> | Path to a .js, .mjs, or .ts file (relative or absolute) |
Options:
| Option | Description |
|---|---|
--format <format> | Output format: zod, guard, dts, all (default: all) |
--output <dir> | Write validator files to this directory (<name>.nudo.zod.ts, <name>.nudo.guard.ts, <name>.d.ts). Omit to print to stdout |
Output formats:
zod— Zod schema strings (as comments) for each function case (input and output); input parameters are namedarg0,arg1, …guard— zero-dependency runtime type guard functions, one per case, namedis<Function><Case>Outputdts— TypeScript declarations; one widened signature per function with real parameter names, with each case's precise result preserved in JSDoc (same output asnudo infer --dts)all— all of the above
Example:
nudo generate src/user.js --format zod
// === createUser Zod Schemas ===
// debug "input":
// Input: { arg0: z.object({ name: z.string(), age: z.number() }) }
// Output: z.object({ id: z.literal(123), name: z.string(), age: z.number() })
nudo emit
Emit TypeScript .d.ts declarations for the npm compatibility exit. Equivalent to nudo generate --format dts.
nudo emit <file> [options]
Arguments:
| Argument | Description |
|---|---|
<file> | Path to a .js, .mjs, or .ts file |
Options:
| Option | Description |
|---|---|
--output <dir> | Write <name>.d.ts to this directory. Omit to print to stdout |
Example:
nudo emit src/user.js --output dist/types
nudo guard
Generate runtime type-guard functions from inferred result types. Prefer the Abs path (denoteGuard: shape + decidable numeric preds) when the case has a lossless Abs result; fall back to the extensional projection otherwise. Equivalent to nudo generate --format guard.
nudo guard <file> [options]
Arguments:
| Argument | Description |
|---|---|
<file> | Path to a .js, .mjs, or .ts file |
Options:
| Option | Description |
|---|---|
--output <dir> | Write <name>.nudo.guard.ts to this directory. Omit to print to stdout |
nudo watch
Watch a file or directory and re-run inference on changes.
nudo watch <path> [options]
Arguments:
| Argument | Description |
|---|---|
<path> | File or directory to watch |
Options:
| Option | Description |
|---|---|
--dts | Generate .d.ts files on each run |
Behavior:
- File: watches the file's directory and re-analyzes tracked files on change
- Directory: recursively watches all inference targets (
.js/.mjs/.ts, excludingnode_modules) — files without Nudo directives are analyzed too (whole-program inference: call sites across watched files synthesizecall@Lcases; uncalled functions getentry@Lcases) - Debouncing: 200ms debounce to batch rapid edits
- Incremental: only changed files and their dependents are re-analyzed; each run prints
Incremental: re-analyzed N, skipped M (…ms) - Output is cleared and reprinted on each run
Example:
nudo watch .
nudo watch src/utils.js --dts
nudo harvest
Convert installed @types/<pkg> .d.ts declarations into a Nudo env file — TypeScript source that rebuilds those types with Nudo env constructors, loaded via the /// @nudo:env directive. The @types package must be installed first.
nudo harvest <pkg> [options]
Arguments:
| Argument | Description |
|---|---|
<pkg> | Package name under @types (e.g. node) |
Options:
| Option | Description |
|---|---|
--out <file> | Output .ts env file (default: ./nudo-harvest-<pkg>.ts) |
Example:
pnpm add -D @types/node
nudo harvest node
Harvested @types/node → nudo-harvest-node.ts
files: 80
symbols: 1671
skipped: 148
Usage — add this directive at the top of your JS file:
/// @nudo:env nudo-harvest-node.ts
File Patterns
- Input:
.js,.mjs, and.tsfiles (parsed via Babel; TypeScript type annotations are stripped at the parser layer and the file is inferred with JS semantics). Directories are accepted wherever a target path is — they are recursively scanned for inference targets (.js/.mjs/.ts, excluding.d.tsand.tsx) - Directives are optional: files without any
@nudo:*directive are analyzed too — their functions get types from observed call sites, withentry@Lfallback cases (unknownparameters) when nothing calls them - Watch mode: directories are scanned recursively for inference targets, excluding
node_modules
Exit Codes
| Code | Meaning |
|---|---|
0 | Success |
1 | Fatal error — missing input or callsite file, parse failure, or --json combined with a directory target |
1 | nudo check found at least one error-level diagnostic (warnings alone exit 0) |
1 | nudo doctor found drift or analysis errors — uncovered functions alone exit 0 |
1 | nudo harvest — @types/<pkg> not installed, or no .d.ts files found in it |
1 | --emit-cases misuse — combined with --json, an invalid mode value, or --exit-on-diff without --dry-run; also --exit-on-diff when the --dry-run diff is non-empty |
Note: diagnostics printed by infer — including [error]-severity ones such as a failed @nudo:refine assertion — do not change infer's exit code; infer still exits 0. Use nudo check to gate CI on diagnostics.