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.
Basic analysis
Section titled “Basic analysis”The only required flag for a JSON analysis is --input (-i), the project root:
cants --input ./my-ts-projectWith no --output, the analysis is printed to stdout as compact JSON. Add --output (-o) to write it to a file instead:
cants --input ./my-ts-project --output ./out# -> ./out/analysis.jsonOutput targets (--emit)
Section titled “Output targets (--emit)”--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:
| Target | Output |
|---|---|
json (default) | The TSApplication artifact: symbol_table, call_graph, and external_symbols. |
neo4j | A labeled property graph — a self-contained graph.cypher snapshot, or a live Bolt push. |
schema | The versioned Neo4j schema contract, schema.json (schema_version 2.0.0). |
# Default: the JSON artifactcants --input ./my-ts-project --emit json --output ./out# -> ./out/analysis.jsonThe schema contract
Section titled “The schema contract”--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:
cants --emit schema --output ./out# -> ./out/schema.json (omit --output to print to stdout)Neo4j output
Section titled “Neo4j output”--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:
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.cypherA 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.
When --neo4j-uri is present, cants pushes incrementally over Bolt. It ensures the constraints and indexes exist, diffs each module’s content_hash against the database, and only re-pushes the modules that changed — re-loads touch changed modules only:
export NEO4J_PASSWORD=secretcants --input ./my-ts-project \ --emit neo4j \ --app-name my-ts-app \ --neo4j-uri bolt://localhost:7687 \ --neo4j-user neo4j \ --neo4j-database neo4jThis is the producer half of a producer/consumer split: run it out-of-band as a CI job or a Kubernetes Job/CronJob that pushes into a managed or clustered Neo4j, and let lightweight read-only consumers (agents, the CLDK Python SDK, dashboards) query the database independently.
--app-name: the scoping anchor
Section titled “--app-name: the scoping anchor”--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).
Incremental graph loads
Section titled “Incremental graph loads”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.
# Push only the modules that changed since the last loadcants --input ./my-ts-project \ --emit neo4j \ --app-name my-ts-app \ --neo4j-uri bolt://localhost:7687 \ --target-files src/billing.ts src/invoice.tsQuerying across applications
Section titled “Querying across applications”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 packagesORDER BY applicationSee the Neo4j graph schema for the full label and relationship topology, and the Neo4j guide for deployment patterns.
Call-graph provider
Section titled “Call-graph provider”--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:
# tsc resolver (default)cants --input ./my-ts-project --call-graph-provider tsc
# jelly flow analysiscants --input ./my-ts-project --call-graph-provider jelly
# run both and comparecants --input ./my-ts-project --call-graph-provider bothAnalysis levels
Section titled “Analysis levels”--analysis-level (-a) selects how deep the call-graph resolution goes:
# Level 1 (default) — tsc resolver call graph + RTA + phantom external nodescants --input ./my-ts-project --analysis-level 1
# Level 2 — additionally enrich the call graphcants --input ./my-ts-project --analysis-level 2Every 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.
Build control: materializing dependencies
Section titled “Build control: materializing dependencies”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:
# Reuse an already-prepared node_modules; skip materializationcants --input ./my-ts-project --no-build
# Force a clean rebuild (reinstall dependencies, rebuild the analysis)cants --input ./my-ts-project --eagerMaterialization 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.
Caching: eager vs lazy
Section titled “Caching: eager vs lazy”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.
# Lazy (default) — reuse unchanged files from cachecants --input ./my-ts-project
# Eager — rebuild the analysis and reinstall dependenciescants --input ./my-ts-project --eagerControl where the cache lives with --cache-dir (-c). If unset, it defaults to .codeanalyzer inside the input project:
cants --input ./my-ts-project --cache-dir /tmp/ca-cacheIncremental mode
Section titled “Incremental mode”To re-analyze only specific files rather than the whole project, pass --target-files (-t) — the rest of the project is served from cache:
cants --input ./my-ts-project --target-files src/a.ts src/b.tsThis 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).
Including test files
Section titled “Including test files”Test trees are skipped by default. Include them with --include-tests (or state the default explicitly with --skip-tests):
cants --input ./my-ts-project --include-testsPhantom nodes
Section titled “Phantom nodes”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:
cants --input ./my-ts-project --no-phantomsSee Call graph & dispatch for what phantom nodes capture.
Verbosity
Section titled “Verbosity”The tool is quiet by default. Stack -v for progressively more logging:
cants --input ./my-ts-project -v # infocants --input ./my-ts-project -vv # debugPutting it together
Section titled “Putting it together”A typical CI invocation — eager rebuild, incremental push to a shared Neo4j, verbose:
export NEO4J_PASSWORD=secretcants \ --input ./my-ts-project \ --emit neo4j \ --app-name my-ts-app \ --neo4j-uri bolt://neo4j.internal:7687 \ --neo4j-database neo4j \ --eager \ -vDownstream, 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 CLDKfrom 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.DiGraphSee the CLDK guide for the full read API.