{"_id":"@acoyfellow/deadlint","name":"@acoyfellow/deadlint","dist-tags":{"latest":"0.0.1"},"versions":{"0.0.1":{"name":"@acoyfellow/deadlint","version":"0.0.1","description":"Find dead cross-boundary code (Workers DurableObject / RpcTarget / Agent methods) and structural clones in a TypeScript codebase.","keywords":["typescript","lint","dead-code","duplicate-code","cloudflare-workers","durable-objects","rpc","static-analysis"],"homepage":"https://github.com/acoyfellow/deadlint#readme","bugs":{"url":"https://github.com/acoyfellow/deadlint/issues"},"repository":{"type":"git","url":"git+https://github.com/acoyfellow/deadlint.git"},"license":"MIT","author":{"name":"acoyfellow","email":"coeyman@gmail.com"},"type":"module","bin":{"deadlint":"bin/deadlint.mjs"},"scripts":{"deadlint":"tsx src/cli.ts","test":"node --import tsx --test test/deadlint.test.ts test/hook.test.ts","typecheck":"tsc --noEmit","ci":"pnpm typecheck && pnpm test"},"dependencies":{"ts-morph":"^24.0.0","tsx":"^4.21.0"},"devDependencies":{"@types/node":"^22.0.0","typescript":"^5.6.0"},"engines":{"node":">=20"},"_id":"@acoyfellow/deadlint@0.0.1","gitHead":"0d653471c82f4332fea702d881d8a9c7fa27b8f6","_nodeVersion":"22.22.2","_npmVersion":"10.9.7","dist":{"integrity":"sha512-SOPNdZ1aH8i2a4aJuZJjZx6S6KDqZBOzBCwopaZN3vG2yiFOE0BIAs8tHyb3cWM/guomVqwhcTehPurT9vpZag==","shasum":"d0ff0f66144a39283a47e9ce1b3807227ba1133a","tarball":"https://registry.npmjs.org/@acoyfellow/deadlint/-/deadlint-0.0.1.tgz","fileCount":14,"unpackedSize":1155009,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCJpbYQ80TSc4UZz6GTIcitMaKrSIZGYsCI2zUIstmJhQIgHDL+idPXm7JAlpn2gQh7Q2vBaL7BKOlubV2vghspgCQ="}]},"_npmUser":{"name":"acoyfellow","email":"Coeyman@gmail.com"},"directories":{},"maintainers":[{"name":"acoyfellow","email":"Coeyman@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/deadlint_0.0.1_1778710026631_0.407037686254232"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-13T22:07:06.500Z","0.0.1":"2026-05-13T22:07:06.889Z","modified":"2026-05-13T22:07:07.073Z"},"maintainers":[{"name":"acoyfellow","email":"Coeyman@gmail.com"}],"description":"Find dead cross-boundary code (Workers DurableObject / RpcTarget / Agent methods) and structural clones in a TypeScript codebase.","homepage":"https://github.com/acoyfellow/deadlint#readme","keywords":["typescript","lint","dead-code","duplicate-code","cloudflare-workers","durable-objects","rpc","static-analysis"],"repository":{"type":"git","url":"git+https://github.com/acoyfellow/deadlint.git"},"author":{"name":"acoyfellow","email":"coeyman@gmail.com"},"bugs":{"url":"https://github.com/acoyfellow/deadlint/issues"},"license":"MIT","readme":"<p align=\"center\">\n  <img src=\"./docs/img/banner.jpg\" alt=\"three skulls and ritual ornaments, block-printed in oxblood ink on cream paper\" width=\"100%\">\n</p>\n\n<h1 align=\"center\">deadlint</h1>\n\n<p align=\"center\"><em>Ruthlessly Eliminate the Dead.</em></p>\n\n---\n\nFind dead public methods on Cloudflare Workers `DurableObject` /\n`WorkerEntrypoint` / `WorkflowEntrypoint` / `RpcTarget` / `Agent` subclasses,\nand structural clones across your TypeScript codebase.\n\n## The gap\n\n| Tool                         | Stops at                | Result                                            |\n| ---------------------------- | ----------------------- | ------------------------------------------------- |\n| `tsc` (`noUnusedLocals`)     | function boundary       | unused vars only                                  |\n| `oxlint` / `biome` / `eslint`| file/class boundary     | every `export` is treated as live                 |\n| `knip` / `ts-prune`          | module-export boundary  | every public class member is treated as the API  |\n| **deadlint**                 | RPC stub / clone        | this is the layer that was missing                |\n\nIn a Workers / Agents codebase, every public method on a DO is — to a static\nanalyzer — an entry point. Anyone with a stub could call it, so nothing dares\nflag it. Real codebases accumulate dead RPC methods for years and no linter\nwill tell you.\n\ndeadlint walks the call graph plus a targeted token scan and tells you which\nones are actually unreachable.\n\n## What it found, on real repos\n\n```text\n$ npx @acoyfellow/deadlint ./artifacts\ndeadlint report — ./artifacts\n\nDead RPC methods (1)\n───────────────────────────\n  GitServer.streamingUploadPack  (extends DurableObject)\n    apps/worker/src/git-server.ts:299\n\nClones — engine: inline (3)\n───────────────────────────\n  1.00  27L  readBlob ≈ readTree\n    src/capabilities/repo.ts:135\n    src/capabilities/repo.ts:167\n  1.00  27L  readBlob ≈ readCommit\n    src/capabilities/repo.ts:135\n    src/capabilities/repo.ts:199\n  1.00  27L  readTree ≈ readCommit\n    src/capabilities/repo.ts:167\n    src/capabilities/repo.ts:199\n\n4 findings\n```\n\n`streamingUploadPack` was a wrapper around `streamingUploadPackWithEventType`\nthat nothing called for months. `readBlob` / `readTree` / `readCommit` were\n27-line copy-pastes of each other. No other linter saw any of it.\n\n## Install and run\n\n```bash\n# one-off\nnpx @acoyfellow/deadlint /path/to/your/repo\n\n# global\nnpm i -g @acoyfellow/deadlint\ndeadlint /path/to/your/repo\n```\n\nThe path needs a `tsconfig.json` somewhere — at the root, in `apps/worker/`,\nor anywhere within the first 3 directory levels. Multi-package monorepos\nwithout a root config (a `packages/api/tsconfig.json` + `packages/web/tsconfig.json`\nlayout) are auto-discovered and scanned per-project.\n\n```bash\ndeadlint ./repo                              # full scan (default)\ndeadlint ./repo --check dead-rpc             # just the dead methods\ndeadlint ./repo --check clones               # just the clones\ndeadlint ./repo --clones-engine both         # similarity-ts + inline engine\ndeadlint ./repo --json > findings.json       # machine-readable\ndeadlint --help                              # all flags\n```\n\nExit `0` = clean, `1` = findings, `2` = misconfig.\n\n## What it actually checks\n\n**Dead RPC methods.** For every public method on a class extending\n`DurableObject`, `WorkerEntrypoint`, `WorkflowEntrypoint`, `RpcTarget`,\nor `Agent`, deadlint looks for callers across three signals:\n\n1. The TypeScript language service (precise — but blind to JSRPC stubs).\n2. A token scan for `.method(` / `[\"method\"](` direct dispatch.\n3. A token scan for `.call(\"method\", …)` string-key dispatch — the\n   Agents SDK pattern frontend code uses to reach DO methods through\n   the WebSocket proxy.\n\nPatterns 2 and 3 are scanned across both TypeScript files and companion\nfiles (`.svelte`, `.vue`, `.astro`, `.tsx`, `.jsx`) so frontend call\nsites that aren't compiled by your tsconfig are still seen. A method is\nflagged dead **only when all three signals turn up zero**. Common\nstdlib names (`map`, `then`, `set`, …) are excluded from the token\nscans to avoid coincidental keep-alives. Workers/Agents runtime hooks\n(`fetch`, `alarm`, `onConnect`, …) are allow-listed.\n\nOverride the boundary class list with `--bases Foo,Bar`.\n\n**Structural clones.** Two engines, run independently, results merged.\n\n`similarity` (default) shells out to\n[`similarity-ts`](https://github.com/mizchi/similarity) — a Rust binary\nusing oxc-parser and TSED. Higher precision, biased toward larger\nfunctions. Install once: `cargo install similarity-ts`.\n\n`inline` is built into deadlint, ~150 lines of `ts-morph`. Each function\nbody is normalized to a `SyntaxKind`-only token sequence (identifiers\nand literals erased, so renamed copies match exactly). Findings are\nemitted on identical shapes (`1.00`) or 5-gram Jaccard similarity above\n`--clone-threshold` (default `0.85`). No external dependencies.\n\nUse `--clones-engine both` to run them side by side. They find largely\nnon-overlapping pairs.\n\n## Always on, every repo\n\n```bash\ndeadlint --install-hook\n```\n\nSets `git config --global core.hooksPath ~/.config/git/hooks` and writes\na `pre-push` script that runs `deadlint . --check dead-rpc` before every\npush. Covers every repo on your machine — public, private, GitHub, GitLab.\nThe hook silently no-ops on non-TypeScript repos.\n\n```bash\ndeadlint --hook-status      # is it installed?\ndeadlint --uninstall-hook   # remove it (only if we wrote it)\ngit push --no-verify        # bypass once\n```\n\nThe installer refuses to clobber a pre-existing `pre-push` hook unless\nyou pass `--force`. The uninstaller refuses to remove anything that\nisn't deadlint-managed. You can't accidentally lose work.\n\n## What it won't catch\n\n- Fully dynamic RPC: `stub[methodFromConfig]()` where the method name is\n  computed at runtime. None of the signals can see it.\n- Cross-repo dead code. If your callers live in a different repository,\n  deadlint sees nothing.\n- HTTP routes dispatched by URL path rather than method name. Add the\n  router class to `--bases` if appropriate.\n- Behavioral clones with different control flow. The inline engine is\n  shape-based; the similarity engine helps but isn't magic.\n\nBuild-output directories (`dist`, `build`, `.svelte-kit`, `.next`, etc.)\nare excluded by default — they generate ~100% clone matches against the\nsource they were built from. Pass `--exclude` to replace the list, or\n`--also-exclude` to extend it. Note: `lib` is **not** in the default\nlist because SvelteKit and many other frameworks use `src/lib/` for\nsource code; pass `--also-exclude lib` if your project emits to it.\n\nFindings are meant for human review. The tool biases toward false\nnegatives — it would rather miss a dead method than wrongly flag a live one.\n\n## License\n\n[MIT](./LICENSE)\n","readmeFilename":"README.md","_rev":"1-d5bc577d2e289107918fd7a901c61cd0"}