Skip to content

CLI usage

The cants command runs static analysis on a TypeScript/JavaScript project and either emits a TSApplication artifact (analysis.json) or projects that same analysis into a Neo4j property graph. This guide walks through the common invocations; for the full flag table see the CLI reference.

The only required flag for a JSON analysis is --input (-i), the project root:

Terminal window
cants --input ./my-ts-project

With no --output, the analysis is printed to stdout as compact JSON. Add --output (-o) to write it to a file instead:

Terminal window
cants --input ./my-ts-project --output ./out
# -> ./out/analysis.json

--emit <target> selects what the analyzer produces. One analysis is built in memory and then serialized to the chosen target — the targets are mutually exclusive, not additive:

TargetOutput
json (default)The TSApplication artifact: symbol_table, call_graph, and external_symbols.
neo4jA labeled property graph — a self-contained graph.cypher snapshot, or a live Bolt push.
schemaThe versioned Neo4j schema contract, schema.json (schema_version 2.0.0).
Terminal window
# Default: the JSON artifact
cants --input ./my-ts-project --emit json --output ./out
# -> ./out/analysis.json

--emit schema serializes the in-repo catalog to a machine-readable schema.json — the node labels, relationship types, and properties that the Neo4j projection guarantees. It needs no --input and is stamped with schema_version 2.0.0, so a consumer (a dashboard, the CLDK SDK) can detect a producer/consumer mismatch before it queries:

Terminal window
cants --emit schema --output ./out
# -> ./out/schema.json (omit --output to print to stdout)

--emit neo4j replaces the JSON entirely and instead projects the analysis into a Neo4j property graph. Where a single analysis.json has to be loaded whole into memory and doesn’t compose across a portfolio, the graph is a persistent, queryable system of record: many applications live in one database — each anchored at its own :TSApplication node — and whole-monorepo or cross-service questions become a Cypher traversal instead of a parse of giant JSON blobs.

There are two mutually-exclusive writers, chosen by whether --neo4j-uri is set.

With no --neo4j-uri, the projection is written as a self-contained graph.cypher script: DDL constraints and indexes, a scoped wipe of this app’s prior subgraph, then batched UNWIND … MERGE for nodes and relationships. Load it with cypher-shell:

Terminal window
cants --input ./my-ts-project --emit neo4j --app-name my-ts-app --output ./out
# -> ./out/graph.cypher
cypher-shell -u neo4j -p "$NEO4J_PASSWORD" < ./out/graph.cypher

A snapshot is not incremental by design — a static script has no view of the live database, so it always rewrites this application’s subgraph from scratch.

--app-name sets the name property of the single :TSApplication anchor node — the MERGE key for the whole graph (a uniqueness constraint enforces it). Every module hangs off this node via TS_HAS_MODULE, and the schema_version is stamped onto it. It defaults to the input directory’s basename when omitted.

It is also the multi-tenant boundary. The scoped wipe matches (:TSApplication {name: <app-name>}) and detach-deletes only that application’s modules and declarations, so loading billing-service never clobbers web-frontend in the same database. Shared :TSExternal, :TSPackage, and :TSDecorator nodes are MERGE-only and survive across applications. Keep --app-name stable — the CLDK SDK reads the graph back by exactly this name (application_name must equal the --app-name the graph was loaded with).

On the Bolt path, --target-files (-t) flips the run from full to targeted: per changed module cants deletes that module’s outgoing edges, detach-deletes the declarations it no longer emits, and MERGE-upserts its current nodes and edges. Nodes are never blindly deleted, so a declaration another unchanged module still references survives.

Terminal window
# Push only the modules that changed since the last load
cants --input ./my-ts-project \
--emit neo4j \
--app-name my-ts-app \
--neo4j-uri bolt://localhost:7687 \
--target-files src/billing.ts src/invoice.ts

Because many app-scoped subgraphs share one database, cross-portfolio analysis is a single Cypher query. For example, every external package any application imports:

MATCH (a:TSApplication)-[:TS_HAS_MODULE]->(:TSModule)-[:TS_IMPORTS]->(p:TSPackage)
RETURN a.name AS application, collect(DISTINCT p.name) AS packages
ORDER BY application

See the Neo4j graph schema for the full label and relationship topology, and the Neo4j guide for deployment patterns.

--call-graph-provider selects the call-graph backend. The default tsc uses the TypeScript compiler’s resolver; jelly uses the embedded cs-au-dk/jelly flow analyzer (shipped inside the cants binary — no extra install); both runs each and diffs them:

Terminal window
# tsc resolver (default)
cants --input ./my-ts-project --call-graph-provider tsc
# jelly flow analysis
cants --input ./my-ts-project --call-graph-provider jelly
# run both and compare
cants --input ./my-ts-project --call-graph-provider both

--analysis-level (-a) selects how deep the call-graph resolution goes:

Terminal window
# Level 1 (default) — tsc resolver call graph + RTA + phantom external nodes
cants --input ./my-ts-project --analysis-level 1
# Level 2 — additionally enrich the call graph
cants --input ./my-ts-project --analysis-level 2

Every run produces a symbol table and a call graph. At level 1, edges come from the TypeScript checker plus Rapid Type Analysis, with phantom nodes for calls into imported libraries.

By default the analyzer materializes the project’s node_modules before parsing, so the checker can resolve imported types and library call targets. Two flags control this:

Terminal window
# Reuse an already-prepared node_modules; skip materialization
cants --input ./my-ts-project --no-build
# Force a clean rebuild (reinstall dependencies, rebuild the analysis)
cants --input ./my-ts-project --eager

Materialization runs the project’s package manager with --ignore-scripts and degrades gracefully: if the install fails, the run still completes with partial type information. See Installation.

Analysis is lazy by default: cants caches the symbol table and call graph under .codeanalyzer/ and reuses entries for files that haven’t changed (detected by content hash, mtime, and size). Pass --eager to rebuild everything from scratch; --lazy is the explicit default.

Terminal window
# Lazy (default) — reuse unchanged files from cache
cants --input ./my-ts-project
# Eager — rebuild the analysis and reinstall dependencies
cants --input ./my-ts-project --eager

Control where the cache lives with --cache-dir (-c). If unset, it defaults to .codeanalyzer inside the input project:

Terminal window
cants --input ./my-ts-project --cache-dir /tmp/ca-cache

To re-analyze only specific files rather than the whole project, pass --target-files (-t) — the rest of the project is served from cache:

Terminal window
cants --input ./my-ts-project --target-files src/a.ts src/b.ts

This restricts the work to the named files, reusing the cached analysis for everything else. Combined with --emit neo4j --neo4j-uri, it also makes the graph load targeted (see Incremental graph loads above).

Test trees are skipped by default. Include them with --include-tests (or state the default explicitly with --skip-tests):

Terminal window
cants --input ./my-ts-project --include-tests

By default, calls into imported libraries become phantom external_symbols so the graph keeps cross-boundary edges. Disable that with --no-phantoms if you want a graph restricted to in-project targets only:

Terminal window
cants --input ./my-ts-project --no-phantoms

See Call graph & dispatch for what phantom nodes capture.

The tool is quiet by default. Stack -v for progressively more logging:

Terminal window
cants --input ./my-ts-project -v # info
cants --input ./my-ts-project -vv # debug

A typical CI invocation — eager rebuild, incremental push to a shared Neo4j, verbose:

Terminal window
export NEO4J_PASSWORD=secret
cants \
--input ./my-ts-project \
--emit neo4j \
--app-name my-ts-app \
--neo4j-uri bolt://neo4j.internal:7687 \
--neo4j-database neo4j \
--eager \
-v

Downstream, a read-only consumer reads that same graph back through the CLDK Python SDK — no JDK, no native binary, no project source, just the Bolt URI and read-only credentials:

from cldk import CLDK
from cldk.analysis.commons.backend_config import Neo4jConnectionConfig
analysis = CLDK.typescript(
backend=Neo4jConnectionConfig(
uri="bolt://neo4j.internal:7687",
username="neo4j",
password="", # read-only credentials suffice
application_name="my-ts-app", # == the --app-name above
),
)
classes = analysis.get_classes() # Dict[str, TSClass]
cg = analysis.get_call_graph() # networkx.DiGraph

See the CLDK guide for the full read API.