diff --git a/.editorconfig b/.editorconfig
index 5fbb1cdc..0020fc03 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,3 +1,5 @@
-[*.ts]
+root = true
+
+[*]
 indent_style = space
 indent_size = 2
diff --git a/.eslintignore b/.eslintignore
deleted file mode 100644
index a413cb13..00000000
--- a/.eslintignore
+++ /dev/null
@@ -1,3 +0,0 @@
-node_modules
-packages/*/node_modules
-**/*.d.ts
diff --git a/.eslintrc b/.eslintrc
deleted file mode 100644
index 9bed3407..00000000
--- a/.eslintrc
+++ /dev/null
@@ -1,49 +0,0 @@
-{
-  "parser": "@typescript-eslint/parser",
-  "extends": [
-    "eslint:recommended",
-    "plugin:@typescript-eslint/eslint-recommended",
-    "plugin:@typescript-eslint/recommended",
-    "plugin:import/recommended",
-    "plugin:import/typescript",
-    "prettier"
-  ],
-  "plugins": ["@typescript-eslint", "import", "prettier"],
-  "rules": {
-    "prettier/prettier": ["error", { "singleQuote": true }],
-    "@typescript-eslint/array-type": ["error", { "default": "generic" }],
-    "@typescript-eslint/explicit-member-accessibility": "off",
-    "@typescript-eslint/explicit-function-return-type": [
-      "error",
-      { "allowExpressions": true }
-    ],
-    "@typescript-eslint/no-use-before-define": [
-      "error",
-      { "functions": false }
-    ],
-    "@typescript-eslint/no-parameter-properties": "off",
-    "@typescript-eslint/no-object-literal-type-assertion": "off",
-    "@typescript-eslint/no-unused-vars": "error"
-  },
-  "settings": {
-    "import/resolver": {
-      "node": {
-        "extensions": [".js", ".jsx", ".ts", ".tsx"]
-      }
-    }
-  },
-  "overrides": [
-    {
-      "files": ["**/fixtures/**/*.ts"],
-      "rules": {
-        "no-console": "off"
-      }
-    },
-    {
-      "files": ["**/bin/*"],
-      "env": {
-        "node": true
-      }
-    }
-  ]
-}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index fa843263..5b521d23 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -29,7 +29,7 @@ jobs:
       - uses: actions/setup-node@v3
         with:
           node-version: ${{ matrix.node-version }}
-          cache: 'pnpm'
+          cache: pnpm
 
       - name: Install Dependencies
         run: pnpm install --frozen-lockfile
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 886020e0..9eb5805f 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -1,12 +1,12 @@
-name: "CodeQL"
+name: CodeQL
 
 on:
   push:
-    branches: [ "main" ]
+    branches: [main]
   pull_request:
-    branches: [ "main" ]
+    branches: [main]
   schedule:
-    - cron: "54 14 * * 3"
+    - cron: '54 14 * * 3'
 
 jobs:
   analyze:
@@ -20,7 +20,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        language: [ javascript ]
+        language: [javascript]
 
     steps:
       - name: Checkout
@@ -38,4 +38,4 @@ jobs:
       - name: Perform CodeQL Analysis
         uses: github/codeql-action/analyze@v2
         with:
-          category: "/language:${{ matrix.language }}"
+          category: '/language:${{ matrix.language }}'
diff --git a/.gitignore b/.gitignore
index d32e154e..3502ef7f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,36 +1,144 @@
-# ignore pack
-*.tgz
+# Created by https://www.toptal.com/developers/gitignore/api/node
+# Edit at https://www.toptal.com/developers/gitignore?templates=node
 
-# ignore development
-node_modules
+### Node ###
+# Logs
+logs
 *.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
 
-# ignore vim dotfiles
-*.swp
-*.swo
+# node-waf configuration
+.lock-wscript
 
-# ignore idea directory
-.idea/
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
 
-# TypeScript build files
-build
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
 *.tsbuildinfo
 
-# allow fixtures
-!test/fixtures/**/*.js
-test/fixtures/**/*-typescript.js
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+out
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+.temp
+
+# Docusaurus cache and generated files
+.docusaurus
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
 
-# allow JS config files
-!*.config.js
+# yarn v2
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
 
-# allow JS script files
-!script/_utils/runCommand.js
+### Node Patch ###
+# Serverless Webpack directories
+.webpack/
 
-# ignore tmp directory
-tmp
+# Optional stylelint cache
 
-# ignore custom build files
-packages/matchers/src/matchers.ts.expected
+# SvelteKit build / generate output
+.svelte-kit
 
-# test coverage
-coverage
\ No newline at end of file
+# End of https://www.toptal.com/developers/gitignore/api/node
diff --git a/.husky/pre-commit b/.husky/pre-commit
deleted file mode 100755
index fbd10372..00000000
--- a/.husky/pre-commit
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/usr/bin/env sh
-. "$(dirname -- "$0")/_/husky.sh"
-
-pnpx commitlint --from="$(git merge-base HEAD main)"
diff --git a/.node-version b/.node-version
deleted file mode 100644
index 58a4133d..00000000
--- a/.node-version
+++ /dev/null
@@ -1 +0,0 @@
-16.13.0
diff --git a/.prettierrc b/.prettierrc
deleted file mode 100644
index fd496a82..00000000
--- a/.prettierrc
+++ /dev/null
@@ -1,4 +0,0 @@
-{
-  "singleQuote": true,
-  "semi": false
-}
diff --git a/commitlint.config.js b/commitlint.config.js
index 98ee7dfc..d179c690 100644
--- a/commitlint.config.js
+++ b/commitlint.config.js
@@ -1,3 +1,3 @@
-module.exports = {
+export default {
   extends: ['@commitlint/config-conventional'],
 }
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 00000000..4be066cb
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,5 @@
+import antfu from '@antfu/eslint-config'
+
+export default antfu(
+  { type: 'lib', ignores: ['**/fixtures', 'packages/matchers/src/matchers/generated.ts'], typescript: { tsconfigPath: 'tsconfig.json' } },
+)
diff --git a/package.json b/package.json
index 7c5db461..978b069f 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,6 @@
 {
-  "name": "codemod",
+  "name": "codemod-esm",
+  "type": "module",
   "private": true,
   "workspaces": {
     "packages": [
@@ -9,47 +10,38 @@
       "**/@types/**"
     ]
   },
+  "packageManager": "pnpm@10.5.2+sha512.da9dc28cd3ff40d0592188235ab25d3202add8a207afbedc682220e4a0029ffbff4562102b9e6e46b4e3f9e8bd53e6d05de48544b0c57d4b0179e22c76d1199b",
+  "scripts": {
+    "prepare": "husky"
+  },
+  "devDependencies": {
+    "@antfu/eslint-config": "^4.4.0",
+    "@types/jest": "^29.5.14",
+    "@types/node": "^22.13.9",
+    "bun": "^1.2.4",
+    "esbuild": "^0.25.0",
+    "eslint": "^9.21.0",
+    "husky": "^9.1.7",
+    "lerna": "^8.2.1",
+    "lint-staged": "^15.4.3",
+    "prettier": "^3.5.3",
+    "rimraf": "^6.0.1",
+    "typescript": "^5.8.2"
+  },
   "husky": {
     "hooks": {
-      "pre-commit": "lint-staged",
-      "commit-msg": "commitlint -e $GIT_PARAMS"
+      "pre-commit": "lint-staged"
     }
   },
   "lint-staged": {
     "*.md": [
-      "prettier --write"
+      "eslint --fix"
     ],
     "*.ts": [
       "eslint --fix"
     ],
-    "package.json": [
-      "sort-package-json"
+    "*.json": [
+      "eslint --fix"
     ]
-  },
-  "resolutions": {
-    "@babel/parser": "7.20.15"
-  },
-  "devDependencies": {
-    "@commitlint/cli": "^14.1.0",
-    "@commitlint/config-conventional": "^14.1.0",
-    "@types/node": "^18.14.0",
-    "@typescript-eslint/eslint-plugin": "^5.3.1",
-    "@typescript-eslint/parser": "^5.3.1",
-    "esbuild": "^0.13.13",
-    "esbuild-runner": "^2.2.1",
-    "eslint": "^8.2.0",
-    "eslint-config-prettier": "^8.3.0",
-    "eslint-plugin-import": "^2.27.5",
-    "eslint-plugin-prettier": "^4.0.0",
-    "husky": "^8.0.0",
-    "jest": "^27.3.1",
-    "lerna": "^4.0.0",
-    "lint-staged": "^13.1.2",
-    "prettier": "^2.4.1",
-    "sort-package-json": "^1.53.1",
-    "typescript": "^4.4.4"
-  },
-  "scripts": {
-    "prepare": "husky install"
   }
 }
diff --git a/packages/cli/.eslintignore b/packages/cli/.eslintignore
deleted file mode 100644
index f6ec2f86..00000000
--- a/packages/cli/.eslintignore
+++ /dev/null
@@ -1,3 +0,0 @@
-build
-coverage
-__tests__/fixtures
diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md
deleted file mode 100644
index 1190661e..00000000
--- a/packages/cli/CHANGELOG.md
+++ /dev/null
@@ -1,95 +0,0 @@
-# Change Log
-
-All notable changes to this project will be documented in this file.
-See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
-
-# [3.3.0](https://github.com/codemod-js/codemod/compare/@codemod/cli@3.2.0...@codemod/cli@3.3.0) (2023-02-18)
-
-
-### Features
-
-* **cli:** add `defineCodemod` to ease creation ([86a62a1](https://github.com/codemod-js/codemod/commit/86a62a11d9f25f2e2e581ff6287ce885ce18f93a))
-
-
-
-
-
-# [3.2.0](https://github.com/codemod-js/codemod/compare/@codemod/cli@3.1.1...@codemod/cli@3.2.0) (2023-02-17)
-
-### Features
-
-- **cli:** add `--parser-plugins` option ([3593893](https://github.com/codemod-js/codemod/commit/3593893791c7e4e0e0c8cea31ea642b229c0bb8a))
-
-## [3.1.0](https://github.com/codemod-js/codemod/compare/@codemod/cli@3.0.1...@codemod/cli@3.1.0) (2021-11-14)
-
-- **feat:** improve gitignore handling ([e04c0c4](https://github.com/codemod-js/codemod/commit/e04c0c41d5cb3d86c6cdbbac932efcad968c73c9))
-
-## [3.0.1](https://github.com/codemod-js/codemod/compare/@codemod/cli@3.0.0...@codemod/cli@3.0.1) (2021-11-12)
-
-- **refactor:** use globby instead of a custom directory traversal (author: [NickHeiner](https://github.com/NickHeiner)) ([468092a](https://github.com/codemod-js/codemod/commit/468092afa532112ba2b126d949dcf0e38f0c2acd))
-
-## [3.0.0](https://github.com/codemod-js/codemod/compare/@codemod/cli@2.3.2...@codemod/cli@3.0.0) (2021-11-12)
-
-- remove underused options: `--printer`, `--babelrc`, `--find-babel-config` ([50a864d](https://github.com/codemod-js/codemod/commit/50a864df7344767a5c0e9e3ab990a0f4d05d634d))
-- update babel to v7.16.x ([1218bf9](https://github.com/codemod-js/codemod/commit/1218bf98145feaa8a692611152559aa6b46b9ba0))
-
-## [2.1.16](https://github.com/codemod-js/codemod/compare/@codemod/cli@2.1.13...@codemod/cli@2.1.16) (2020-04-05)
-
-### Bug Fixes
-
-- **deps:** upgrade recast ([b6220f3](https://github.com/codemod-js/codemod/commit/b6220f3f26a41f4e58bdca7815bc8f6e9a820866))
-- update babel dependencies ([0d9d569](https://github.com/codemod-js/codemod/commit/0d9d56985dbc5d47621073561cd1617116685e5d))
-- upgrade babel to 7.9.0 ([bfdc402](https://github.com/codemod-js/codemod/commit/bfdc402a6ec0d5a1068c02c07107e8f7148e8a1a))
-- upgrade prettier ([4a22030](https://github.com/codemod-js/codemod/commit/4a22030af417911cad1efe44111f9da38c1cc102))
-
-## [2.1.15](https://github.com/codemod-js/codemod/compare/@codemod/cli@2.1.13...@codemod/cli@2.1.15) (2020-03-25)
-
-### Bug Fixes
-
-- update babel dependencies ([0d9d569](https://github.com/codemod-js/codemod/commit/0d9d56985dbc5d47621073561cd1617116685e5d))
-- upgrade babel to 7.9.0 ([bfdc402](https://github.com/codemod-js/codemod/commit/bfdc402a6ec0d5a1068c02c07107e8f7148e8a1a))
-- upgrade prettier ([4a22030](https://github.com/codemod-js/codemod/commit/4a22030af417911cad1efe44111f9da38c1cc102))
-
-## [2.1.11](https://github.com/codemod-js/codemod/compare/@codemod/cli@2.1.10...@codemod/cli@2.1.11) (2019-10-16)
-
-### Bug Fixes
-
-- **package:** remove unnecessary non-development dependency `jest` ([cf322a1](https://github.com/codemod-js/codemod/commit/cf322a1))
-
-## [2.1.10](https://github.com/codemod-js/codemod/compare/@codemod/cli@2.1.9...@codemod/cli@2.1.10) (2019-08-09)
-
-### Bug Fixes
-
-- **license:** update outdated license files ([58e4b11](https://github.com/codemod-js/codemod/commit/58e4b11))
-
-## [2.1.9](https://github.com/codemod-js/codemod/compare/@codemod/cli@2.1.8...@codemod/cli@2.1.9) (2019-08-09)
-
-### Bug Fixes
-
-- **package:** add "types" to package.json ([094d504](https://github.com/codemod-js/codemod/commit/094d504))
-
-## [2.1.8](https://github.com/codemod-js/codemod/compare/@codemod/cli@2.1.7...@codemod/cli@2.1.8) (2019-08-07)
-
-### Bug Fixes
-
-- use `ParserOptions` from `@codemod/parser` ([09bc2b5](https://github.com/codemod-js/codemod/commit/09bc2b5))
-
-## [2.1.7](https://github.com/codemod-js/codemod/compare/@codemod/cli@2.1.6...@codemod/cli@2.1.7) (2019-08-02)
-
-### Refactors
-
-- extract `@codemod/parser` and `@codemod/core` ([c43ecd52](https://github.com/codemod-js/codemod/commit/c43ecd52))
-
-### Features
-
-- initial commit of [@codemod](https://github.com/codemod)/matchers ([84de839](https://github.com/codemod-js/codemod/commit/84de839))
-
-## [2.1.6](https://github.com/codemod-js/codemod/compare/@codemod/cli@2.1.5...@codemod/cli@2.1.6) (2019-07-31)
-
-### Bug Fixes
-
-- update babel dependencies ([6984c7c](https://github.com/codemod-js/codemod/commit/6984c7c))
-
-### Features
-
-- initial commit of `@codemod/matchers` ([84de839](https://github.com/codemod-js/codemod/commit/84de839))
diff --git a/packages/cli/LICENSE.txt b/packages/cli/LICENSE.txt
deleted file mode 100644
index db69b6e1..00000000
--- a/packages/cli/LICENSE.txt
+++ /dev/null
@@ -1,202 +0,0 @@
-
-                                 Apache License
-                           Version 2.0, March 2017
-                        http://www.apache.org/licenses/
-
-   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-   1. Definitions.
-
-      "License" shall mean the terms and conditions for use, reproduction,
-      and distribution as defined by Sections 1 through 9 of this document.
-
-      "Licensor" shall mean the copyright owner or entity authorized by
-      the copyright owner that is granting the License.
-
-      "Legal Entity" shall mean the union of the acting entity and all
-      other entities that control, are controlled by, or are under common
-      control with that entity. For the purposes of this definition,
-      "control" means (i) the power, direct or indirect, to cause the
-      direction or management of such entity, whether by contract or
-      otherwise, or (ii) ownership of fifty percent (50%) or more of the
-      outstanding shares, or (iii) beneficial ownership of such entity.
-
-      "You" (or "Your") shall mean an individual or Legal Entity
-      exercising permissions granted by this License.
-
-      "Source" form shall mean the preferred form for making modifications,
-      including but not limited to software source code, documentation
-      source, and configuration files.
-
-      "Object" form shall mean any form resulting from mechanical
-      transformation or translation of a Source form, including but
-      not limited to compiled object code, generated documentation,
-      and conversions to other media types.
-
-      "Work" shall mean the work of authorship, whether in Source or
-      Object form, made available under the License, as indicated by a
-      copyright notice that is included in or attached to the work
-      (an example is provided in the Appendix below).
-
-      "Derivative Works" shall mean any work, whether in Source or Object
-      form, that is based on (or derived from) the Work and for which the
-      editorial revisions, annotations, elaborations, or other modifications
-      represent, as a whole, an original work of authorship. For the purposes
-      of this License, Derivative Works shall not include works that remain
-      separable from, or merely link (or bind by name) to the interfaces of,
-      the Work and Derivative Works thereof.
-
-      "Contribution" shall mean any work of authorship, including
-      the original version of the Work and any modifications or additions
-      to that Work or Derivative Works thereof, that is intentionally
-      submitted to Licensor for inclusion in the Work by the copyright owner
-      or by an individual or Legal Entity authorized to submit on behalf of
-      the copyright owner. For the purposes of this definition, "submitted"
-      means any form of electronic, verbal, or written communication sent
-      to the Licensor or its representatives, including but not limited to
-      communication on electronic mailing lists, source code control systems,
-      and issue tracking systems that are managed by, or on behalf of, the
-      Licensor for the purpose of discussing and improving the Work, but
-      excluding communication that is conspicuously marked or otherwise
-      designated in writing by the copyright owner as "Not a Contribution."
-
-      "Contributor" shall mean Licensor and any individual or Legal Entity
-      on behalf of whom a Contribution has been received by Licensor and
-      subsequently incorporated within the Work.
-
-   2. Grant of Copyright License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      copyright license to reproduce, prepare Derivative Works of,
-      publicly display, publicly perform, sublicense, and distribute the
-      Work and such Derivative Works in Source or Object form.
-
-   3. Grant of Patent License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      (except as stated in this section) patent license to make, have made,
-      use, offer to sell, sell, import, and otherwise transfer the Work,
-      where such license applies only to those patent claims licensable
-      by such Contributor that are necessarily infringed by their
-      Contribution(s) alone or by combination of their Contribution(s)
-      with the Work to which such Contribution(s) was submitted. If You
-      institute patent litigation against any entity (including a
-      cross-claim or counterclaim in a lawsuit) alleging that the Work
-      or a Contribution incorporated within the Work constitutes direct
-      or contributory patent infringement, then any patent licenses
-      granted to You under this License for that Work shall terminate
-      as of the date such litigation is filed.
-
-   4. Redistribution. You may reproduce and distribute copies of the
-      Work or Derivative Works thereof in any medium, with or without
-      modifications, and in Source or Object form, provided that You
-      meet the following conditions:
-
-      (a) You must give any other recipients of the Work or
-          Derivative Works a copy of this License; and
-
-      (b) You must cause any modified files to carry prominent notices
-          stating that You changed the files; and
-
-      (c) You must retain, in the Source form of any Derivative Works
-          that You distribute, all copyright, patent, trademark, and
-          attribution notices from the Source form of the Work,
-          excluding those notices that do not pertain to any part of
-          the Derivative Works; and
-
-      (d) If the Work includes a "NOTICE" text file as part of its
-          distribution, then any Derivative Works that You distribute must
-          include a readable copy of the attribution notices contained
-          within such NOTICE file, excluding those notices that do not
-          pertain to any part of the Derivative Works, in at least one
-          of the following places: within a NOTICE text file distributed
-          as part of the Derivative Works; within the Source form or
-          documentation, if provided along with the Derivative Works; or,
-          within a display generated by the Derivative Works, if and
-          wherever such third-party notices normally appear. The contents
-          of the NOTICE file are for informational purposes only and
-          do not modify the License. You may add Your own attribution
-          notices within Derivative Works that You distribute, alongside
-          or as an addendum to the NOTICE text from the Work, provided
-          that such additional attribution notices cannot be construed
-          as modifying the License.
-
-      You may add Your own copyright statement to Your modifications and
-      may provide additional or different license terms and conditions
-      for use, reproduction, or distribution of Your modifications, or
-      for any such Derivative Works as a whole, provided Your use,
-      reproduction, and distribution of the Work otherwise complies with
-      the conditions stated in this License.
-
-   5. Submission of Contributions. Unless You explicitly state otherwise,
-      any Contribution intentionally submitted for inclusion in the Work
-      by You to the Licensor shall be under the terms and conditions of
-      this License, without any additional terms or conditions.
-      Notwithstanding the above, nothing herein shall supersede or modify
-      the terms of any separate license agreement you may have executed
-      with Licensor regarding such Contributions.
-
-   6. Trademarks. This License does not grant permission to use the trade
-      names, trademarks, service marks, or product names of the Licensor,
-      except as required for reasonable and customary use in describing the
-      origin of the Work and reproducing the content of the NOTICE file.
-
-   7. Disclaimer of Warranty. Unless required by applicable law or
-      agreed to in writing, Licensor provides the Work (and each
-      Contributor provides its Contributions) on an "AS IS" BASIS,
-      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-      implied, including, without limitation, any warranties or conditions
-      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
-      PARTICULAR PURPOSE. You are solely responsible for determining the
-      appropriateness of using or redistributing the Work and assume any
-      risks associated with Your exercise of permissions under this License.
-
-   8. Limitation of Liability. In no event and under no legal theory,
-      whether in tort (including negligence), contract, or otherwise,
-      unless required by applicable law (such as deliberate and grossly
-      negligent acts) or agreed to in writing, shall any Contributor be
-      liable to You for damages, including any direct, indirect, special,
-      incidental, or consequential damages of any character arising as a
-      result of this License or out of the use or inability to use the
-      Work (including but not limited to damages for loss of goodwill,
-      work stoppage, computer failure or malfunction, or any and all
-      other commercial damages or losses), even if such Contributor
-      has been advised of the possibility of such damages.
-
-   9. Accepting Warranty or Additional Liability. While redistributing
-      the Work or Derivative Works thereof, You may choose to offer,
-      and charge a fee for, acceptance of support, warranty, indemnity,
-      or other liability obligations and/or rights consistent with this
-      License. However, in accepting such obligations, You may act only
-      on Your own behalf and on Your sole responsibility, not on behalf
-      of any other Contributor, and only if You agree to indemnify,
-      defend, and hold each Contributor harmless for any liability
-      incurred by, or claims asserted against, such Contributor by reason
-      of your accepting any such warranty or additional liability.
-
-   END OF TERMS AND CONDITIONS
-
-   APPENDIX: How to apply the Apache License to your work.
-
-      To apply the Apache License to your work, attach the following
-      boilerplate notice, with the fields enclosed by brackets "[]"
-      replaced with your own identifying information. (Don't include
-      the brackets!)  The text should be enclosed in the appropriate
-      comment syntax for the file format. We also recommend that a
-      file or class name and description of purpose be included on the
-      same "printed page" as the copyright notice for easier
-      identification within third-party archives.
-
-   Copyright 2017 Brian Donovan
-
-   Licensed under the Apache License, Version 2.0 (the "License");
-   you may not use this file except in compliance with the License.
-   You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-   Unless required by applicable law or agreed to in writing, software
-   distributed under the License is distributed on an "AS IS" BASIS,
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   See the License for the specific language governing permissions and
-   limitations under the License.
diff --git a/packages/cli/README.md b/packages/cli/README.md
deleted file mode 100644
index 5d2e1e25..00000000
--- a/packages/cli/README.md
+++ /dev/null
@@ -1,108 +0,0 @@
-# codemod
-
-codemod rewrites JavaScript and TypeScript using babel plugins.
-
-## Install
-
-Install from [npm](https://npmjs.com/):
-
-```sh
-$ npm install @codemod/cli
-```
-
-This will install the runner locally as `codemod`. This package requires node
-v16 or higher. This README assumes you've installed locally, but you can also
-install globally with `npm install -g @codemod/cli`.
-
-## Usage
-
-The primary interface is as a command line tool, usually run like so:
-
-```sh
-$ npx codemod --plugin transform-module-name \
-  path/to/file.js \
-  another/file.js \
-  a/directory
-```
-
-This will re-write the files `path/to/file.js`, `another/file.js`, and any supported files found in `a/directory` by transforming them with the babel plugin `transform-module-name`. Multiple plugins may be specified, and multiple files or directories may be re-written at once.
-
-Note that TypeScript support is provided by babel and therefore may not completely support all valid TypeScript code. If you encounter an issue, consider looking for it in the [babel issues labeled `area: typescript`](https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+label%3A%22area%3A+typescript%22+) before filing an issue.
-
-Plugins may also be loaded from remote URLs, including saved [AST Explorer](https://astexplorer.net/) URLs, using `--remote-plugin`. This feature should only be used as a convenience to load code that you or someone you trust wrote. It will run with your full user privileges, so please exercise caution!
-
-```sh
-$ npx codemod --remote-plugin URL …
-```
-
-By default, `codemod` makes minimal changes to your source files by using [recast](https://github.com/benjamn/recast) to parse and print your code, retaining the original comments and formatting. If desired, you can reformat files using [Prettier](https://prettier.io/) or ESLint or whatever other tools you prefer after the fact.
-
-For more detailed options, run `npx codemod --help`.
-
-## Writing a Plugin
-
-There are [many, many existing plugins](https://www.npmjs.com/search?q=babel-plugin) that you can use. However, if you need to write your own you should consult the [babel handbook](https://github.com/thejameskyle/babel-handbook). If you publish a plugin intended specifically as a codemod, consider using both the [`babel-plugin`](https://www.npmjs.com/search?q=babel-plugin) and [`babel-codemod`](https://www.npmjs.com/search?q=babel-codemod) keywords.
-
-`@codemod/cli` provides a few helpers to make writing codemod plugins easier. For example:
-
-```ts
-/**
- * This codemod rewrites `A * A` to `A ** 2` for any expression `A`.
- */
-import { defineCodemod } from '@codemod/cli'
-
-// `m` is `@codemod/matchers`, a library of useful matchers
-// `t` is `@babel/types`, babel AST type predicates and builders
-export default defineCodemod(({ t, m }) => {
-  // `operand` is a capture matcher that will be filled in by `multiplyBySelf`,
-  // which is a binary expression matcher that matches any expression multiplied
-  // by itself.
-  const operand = m.capture(m.anyExpression())
-  const multiplyBySelf = m.binaryExpression(
-    '*',
-    operand,
-    m.fromCapture(operand)
-  )
-
-  return {
-    visitor: {
-      BinaryExpression(path) {
-        m.match(multiplyBySelf, { operand }, path.node, ({ operand }) => {
-          path.replaceWith(
-            t.binaryExpression('**', operand, t.numericLiteral(2))
-          )
-        })
-      },
-    },
-  }
-})
-```
-
-### Transpiling using Babel Plugins
-
-`codemod` supports parsing language features supported by a standard Babel or TypeScript build toolchain, similar to what a Create React App build pipeline can handle. Feel free to write your plugins using these language features–they'll be transpiled on the fly.
-
-### Passing Options to Plugins
-
-You can pass a JSON object as options to a plugin:
-
-```sh
-# Pass a JSON object literal
-$ npx codemod --plugin ./my-plugin.ts --plugin-options '{"opt": true}'
-# Pass a JSON object from a file
-$ npx codemod --plugin ./my-plugin.ts --plugin-options @opts.json
-```
-
-## Contributing
-
-See [CONTRIBUTING.md](../../CONTRIBUTING.md) for information on setting up the project for development and on contributing to the project.
-
-## License
-
-Copyright 2017 Brian Donovan
-
-Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
diff --git a/packages/cli/__tests__/cli/cli.test.ts b/packages/cli/__tests__/cli/cli.test.ts
deleted file mode 100644
index eb4ac69c..00000000
--- a/packages/cli/__tests__/cli/cli.test.ts
+++ /dev/null
@@ -1,133 +0,0 @@
-import { promises as fs } from 'fs'
-import { dirname, resolve as pathResolve } from 'path'
-import createTemporaryFile, {
-  createTemporaryFiles,
-} from '../helpers/createTemporaryFile'
-import plugin from '../helpers/plugin'
-import { runCodemodCLI } from '../helpers/runCodemodCLI'
-
-it('respects globs', async function () {
-  const pathInFixtures = (dirPath: string): string =>
-    pathResolve(__dirname, '..', 'fixtures', 'glob-test', dirPath)
-  const { status, stdout, stderr } = await runCodemodCLI([
-    '--dry',
-    pathInFixtures('**/*.js'),
-    `!${pathInFixtures('omit.js')}`,
-  ])
-
-  expect(stderr).toEqual('')
-  expect(stdout).toEqual(expect.stringContaining('abc.js'))
-  expect(stdout).toEqual(expect.stringContaining('subdir/def.js'))
-  expect(stdout).not.toEqual(expect.stringContaining('omit.js'))
-  expect(status).toEqual(0)
-})
-
-it('can read from stdin and write to stdout given the --stdio flag', async function () {
-  expect(await runCodemodCLI(['--stdio'], { stdin: '3+4' })).toEqual({
-    status: 0,
-    stdout: '3+4',
-    stderr: '',
-  })
-})
-
-it('reads from a file, processes with plugins, then writes to that file', async function () {
-  const afile = await createTemporaryFile('a-file.js', '3 + 4;')
-  expect(
-    await runCodemodCLI([afile, '-p', plugin('increment')], {
-      cwd: dirname(afile),
-    })
-  ).toEqual({
-    status: 0,
-    stdout: `a-file.js\n1 file(s), 1 modified, 0 errors\n`,
-    stderr: '',
-  })
-  expect(await fs.readFile(afile, 'utf8')).toEqual('4 + 5;')
-})
-
-it('processes all matching files in a directory', async function () {
-  const [file1, file2, file3, ignored] = await createTemporaryFiles(
-    ['file1.js', '3 + 4;'],
-    ['file2.ts', '0;'],
-    ['sub-dir/file3.jsx', '99;'],
-    ['ignored.css', '* {}']
-  )
-  expect(
-    await runCodemodCLI([dirname(file1), '-p', plugin('increment')], {
-      cwd: dirname(file1),
-    })
-  ).toEqual({
-    status: 0,
-    stdout: `file1.js\nfile2.ts\nsub-dir/file3.jsx\n3 file(s), 3 modified, 0 errors\n`,
-    stderr: '',
-  })
-  expect(await fs.readFile(file1, 'utf8')).toEqual('4 + 5;')
-  expect(await fs.readFile(file2, 'utf8')).toEqual('1;')
-  expect(await fs.readFile(file3, 'utf8')).toEqual('100;')
-  expect(await fs.readFile(ignored, 'utf8')).toEqual('* {}')
-})
-
-it('prints files not processed in dim colors', async function () {
-  const afile = await createTemporaryFile('a-file.js', '3 + 4;')
-  expect(await runCodemodCLI([afile], { cwd: dirname(afile) })).toEqual({
-    status: 0,
-    stdout: `a-file.js\n1 file(s), 0 modified, 0 errors\n`,
-    stderr: '',
-  })
-  expect(await fs.readFile(afile, 'utf8')).toEqual('3 + 4;')
-})
-
-it('can rewrite TypeScript files ending in `.ts`', async function () {
-  const afile = await createTemporaryFile(
-    'a-file.ts',
-    'type A = any;\nlet a = {} as any;'
-  )
-  expect(
-    await runCodemodCLI(
-      [afile, '-p', plugin('replace-any-with-object', '.ts')],
-      { cwd: dirname(afile) }
-    )
-  ).toEqual({
-    status: 0,
-    stdout: `a-file.ts\n1 file(s), 1 modified, 0 errors\n`,
-    stderr: '',
-  })
-
-  expect(await fs.readFile(afile, 'utf8')).toEqual(
-    'type A = object;\nlet a = {} as object;'
-  )
-})
-
-it('can rewrite TypeScript files ending in `.tsx`', async function () {
-  const afile = await createTemporaryFile(
-    'a-file.tsx',
-    'export default () => (
);'
-  )
-  expect(await runCodemodCLI([afile], { cwd: dirname(afile) })).toEqual({
-    status: 0,
-    stdout: `a-file.tsx\n1 file(s), 0 modified, 0 errors\n`,
-    stderr: '',
-  })
-
-  expect(await fs.readFile(afile, 'utf8')).toEqual(
-    'export default () => (
);'
-  )
-})
-
-it('can pass options to a plugin without naming it', async function () {
-  expect(
-    await runCodemodCLI(
-      [
-        '--plugin',
-        plugin('append-options-string', '.ts'),
-        '--plugin-options',
-        `${JSON.stringify({ a: 4 })}`,
-        '--stdio',
-      ],
-      { stdin: '' }
-    )
-  ).toEqual({
-    status: 0,
-    stdout: `${JSON.stringify(JSON.stringify({ a: 4 }))};`,
-    stderr: '',
-  })
-})
diff --git a/packages/cli/__tests__/cli/dry-run.test.ts b/packages/cli/__tests__/cli/dry-run.test.ts
deleted file mode 100644
index 7f847227..00000000
--- a/packages/cli/__tests__/cli/dry-run.test.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { promises as fs } from 'fs'
-import { dirname } from 'path'
-import createTemporaryFile from '../helpers/createTemporaryFile'
-import plugin from '../helpers/plugin'
-import { runCodemodCLI } from '../helpers/runCodemodCLI'
-
-it('processes files but does not replace their contents when using --dry', async function () {
-  const afile = await createTemporaryFile('a-file.js', '3 + 4;')
-  expect(
-    await runCodemodCLI([afile, '-p', plugin('increment'), '--dry'], {
-      cwd: dirname(afile),
-    })
-  ).toEqual({
-    status: 0,
-    stdout: `a-file.js\nDRY RUN: no files affected\n1 file(s), 1 modified, 0 errors\n`,
-    stderr: '',
-  })
-  expect(await fs.readFile(afile, 'utf8')).toEqual('3 + 4;')
-})
diff --git a/packages/cli/__tests__/cli/errors.test.ts b/packages/cli/__tests__/cli/errors.test.ts
deleted file mode 100644
index f7718d53..00000000
--- a/packages/cli/__tests__/cli/errors.test.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import createTemporaryFile from '../helpers/createTemporaryFile'
-import plugin from '../helpers/plugin'
-import { runCodemodCLI } from '../helpers/runCodemodCLI'
-
-test('fails with an error when passing an invalid option', async function () {
-  expect(await runCodemodCLI(['--not-a-real-option'])).toEqual({
-    status: 1,
-    stdout: '',
-    stderr: expect.stringContaining(
-      'ERROR: unexpected option: --not-a-real-option'
-    ),
-  })
-})
-
-test('fails with an error when a plugin throws an exception', async function () {
-  expect(
-    await runCodemodCLI(['--plugin', plugin('bad-plugin'), '--stdio'], {
-      stdin: '3+4',
-    })
-  ).toEqual({
-    status: 1,
-    stdout: '',
-    stderr: expect.stringContaining('I am a bad plugin'),
-  })
-})
-
-it.skip('does not try to load TypeScript files when --no-transpile-plugins is set', async function () {
-  const afile = await createTemporaryFile('a-file.js', '3 + 4;')
-  expect(
-    await runCodemodCLI([
-      afile,
-      '--no-transpile-plugins',
-      '-p',
-      plugin('increment-typescript', ''),
-    ])
-  ).toEqual({
-    status: 255,
-    stdout: '',
-    stderr: expect.stringContaining('SyntaxError'),
-  })
-})
diff --git a/packages/cli/__tests__/cli/extensions.test.ts b/packages/cli/__tests__/cli/extensions.test.ts
deleted file mode 100644
index e0d7bca5..00000000
--- a/packages/cli/__tests__/cli/extensions.test.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import { promises as fs } from 'fs'
-import { dirname } from 'path'
-import createTemporaryFile, {
-  createTemporaryFiles,
-} from '../helpers/createTemporaryFile'
-import plugin from '../helpers/plugin'
-import { runCodemodCLI } from '../helpers/runCodemodCLI'
-
-test('can load plugins written with ES modules by default', async function () {
-  const afile = await createTemporaryFile('a-file.js', '3 + 4;')
-  expect(
-    await runCodemodCLI([afile, '-p', plugin('increment-export-default')], {
-      cwd: dirname(afile),
-    })
-  ).toEqual({
-    status: 0,
-    stdout: `a-file.js\n1 file(s), 1 modified, 0 errors\n`,
-    stderr: '',
-  })
-  expect(await fs.readFile(afile, 'utf8')).toEqual('4 + 5;')
-})
-
-test('can load plugins written in TypeScript by default', async function () {
-  const afile = await createTemporaryFile('a-file.js', '3 + 4;')
-  expect(
-    await runCodemodCLI([afile, '-p', plugin('increment-typescript', '.ts')], {
-      cwd: dirname(afile),
-    })
-  ).toEqual({
-    status: 0,
-    stdout: `a-file.js\n1 file(s), 1 modified, 0 errors\n`,
-    stderr: '',
-  })
-  expect(await fs.readFile(afile, 'utf8')).toEqual('4 + 5;')
-})
-
-test('can implicitly find plugins with .ts extensions', async function () {
-  const afile = await createTemporaryFile('a-file.js', '3 + 4;')
-  expect(
-    await runCodemodCLI([afile, '-p', plugin('increment-typescript', '')], {
-      cwd: dirname(afile),
-    })
-  ).toEqual({
-    status: 0,
-    stdout: `a-file.js\n1 file(s), 1 modified, 0 errors\n`,
-    stderr: '',
-  })
-  expect(await fs.readFile(afile, 'utf8')).toEqual('4 + 5;')
-})
-
-test('can load plugins with multiple files with ES modules by default`', async function () {
-  const afile = await createTemporaryFile('a-file.js', '3 + 4;')
-  expect(
-    await runCodemodCLI(
-      [
-        afile,
-        '-p',
-        plugin('increment-export-default-multiple/increment-export-default'),
-      ],
-      { cwd: dirname(afile) }
-    )
-  ).toEqual({
-    status: 0,
-    stdout: `a-file.js\n1 file(s), 1 modified, 0 errors\n`,
-    stderr: '',
-  })
-  expect(await fs.readFile(afile, 'utf8')).toEqual('4 + 5;')
-})
-
-test('processes all matching files in a directory with custom extensions', async function () {
-  const [ignored, processed] = await createTemporaryFiles(
-    ['ignored.js', '3 + 4;'],
-    ['processed.myjs', '0;']
-  )
-  expect(
-    await runCodemodCLI(
-      [dirname(ignored), '-p', plugin('increment'), '--extensions', '.myjs'],
-      { cwd: dirname(ignored) }
-    )
-  ).toEqual({
-    status: 0,
-    stdout: `processed.myjs\n1 file(s), 1 modified, 0 errors\n`,
-    stderr: '',
-  })
-  expect(await fs.readFile(ignored, 'utf8')).toEqual('3 + 4;')
-  expect(await fs.readFile(processed, 'utf8')).toEqual('1;')
-})
diff --git a/packages/cli/__tests__/cli/info.test.ts b/packages/cli/__tests__/cli/info.test.ts
deleted file mode 100644
index b711ee48..00000000
--- a/packages/cli/__tests__/cli/info.test.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { runCodemodCLI } from '../helpers/runCodemodCLI'
-
-test('prints help', async function () {
-  expect(await runCodemodCLI(['--help'])).toEqual({
-    status: 0,
-    stdout: expect.stringContaining('codemod [OPTIONS]'),
-    stderr: '',
-  })
-})
-
-test('prints the version', async function () {
-  expect(await runCodemodCLI(['--version'])).toEqual({
-    status: 0,
-    stdout: expect.stringMatching(/\d+\.\d+\.\d+\s*$/),
-    stderr: '',
-  })
-})
diff --git a/packages/cli/__tests__/cli/plugin-syntax.ts b/packages/cli/__tests__/cli/plugin-syntax.ts
deleted file mode 100644
index 715a47d7..00000000
--- a/packages/cli/__tests__/cli/plugin-syntax.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import plugin from '../helpers/plugin'
-import { runCodemodCLI } from '../helpers/runCodemodCLI'
-
-it('can load a plugin that uses class properties', async function () {
-  expect(
-    await runCodemodCLI(
-      ['--plugin', plugin('class-properties', '.ts'), '--stdio'],
-      { stdin: '' }
-    )
-  ).toEqual({
-    status: 0,
-    stdout: '',
-    stderr: '',
-  })
-})
-
-it('can load a plugin that uses generators', async function () {
-  expect(
-    await runCodemodCLI(['--plugin', plugin('generators', '.ts'), '--stdio'], {
-      stdin: '',
-    })
-  ).toEqual({
-    status: 0,
-    stdout: '',
-    stderr: '',
-  })
-})
diff --git a/packages/cli/__tests__/cli/remote-plugin.test.ts b/packages/cli/__tests__/cli/remote-plugin.test.ts
deleted file mode 100644
index 34a9a9d3..00000000
--- a/packages/cli/__tests__/cli/remote-plugin.test.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { promises as fs } from 'fs'
-import { dirname } from 'path'
-import createTemporaryFile from '../helpers/createTemporaryFile'
-import plugin from '../helpers/plugin'
-import { runCodemodCLI } from '../helpers/runCodemodCLI'
-import { startServer } from '../helpers/TestServer'
-
-test('can load and run with a remote plugin', async () => {
-  const afile = await createTemporaryFile('a-file.js', '3 + 4;')
-  const server = await startServer(async (req, res) => {
-    expect(req.url).toEqual('/plugin.js')
-
-    res.end(
-      await fs.readFile(plugin('increment-export-default'), {
-        encoding: 'utf8',
-      })
-    )
-  })
-
-  try {
-    const { status, stdout, stderr } = await runCodemodCLI(
-      [afile, '--remote-plugin', server.requestURL('/plugin.js').toString()],
-      { cwd: dirname(afile) }
-    )
-
-    expect({ status, stdout, stderr }).toEqual({
-      status: 0,
-      stdout: `a-file.js\n1 file(s), 1 modified, 0 errors\n`,
-      stderr: '',
-    })
-
-    expect(await fs.readFile(afile, 'utf8')).toEqual('4 + 5;')
-  } finally {
-    await server.stop()
-  }
-})
diff --git a/packages/cli/__tests__/cli/source-type.test.ts b/packages/cli/__tests__/cli/source-type.test.ts
deleted file mode 100644
index 1e64faa2..00000000
--- a/packages/cli/__tests__/cli/source-type.test.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-import { dirname } from 'path'
-import createTemporaryFile, {
-  createTemporaryFiles,
-} from '../helpers/createTemporaryFile'
-import { runCodemodCLI } from '../helpers/runCodemodCLI'
-
-it('can specify the source type as "script"', async function () {
-  const afile = await createTemporaryFile(
-    'a-file.js',
-    'with (a) { b; }' // `with` statements aren't allowed in modules
-  )
-  expect(
-    await runCodemodCLI([afile, '--source-type', 'script'], {
-      cwd: dirname(afile),
-    })
-  ).toEqual({
-    status: 0,
-    stdout: `a-file.js\n1 file(s), 0 modified, 0 errors\n`,
-    stderr: '',
-  })
-})
-
-it('can specify the source type as "module"', async function () {
-  const afile = await createTemporaryFile(
-    'a-file.js',
-    'import "./b-file"' // `import` statements aren't allowed in scripts
-  )
-  expect(
-    await runCodemodCLI([afile, '--source-type', 'module'], {
-      cwd: dirname(afile),
-    })
-  ).toEqual({
-    status: 0,
-    stdout: `a-file.js\n1 file(s), 0 modified, 0 errors\n`,
-    stderr: '',
-  })
-})
-
-it('can specify the source type as "unambiguous"', async function () {
-  const [afile, bfile] = await createTemporaryFiles(
-    [
-      'a-file.js',
-      'with (a) { b; }', // `with` statements aren't allowed in modules
-    ],
-    [
-      'b-file.js',
-      'import "./a-file"', // `import` statements aren't allowed in scripts
-    ]
-  )
-  expect(
-    await runCodemodCLI([afile, bfile, '--source-type', 'unambiguous'], {
-      cwd: dirname(afile),
-    })
-  ).toEqual({
-    status: 0,
-    stdout: `a-file.js\nb-file.js\n2 file(s), 0 modified, 0 errors\n`,
-    stderr: '',
-  })
-})
-
-it('fails when given an invalid source type', async function () {
-  expect(await runCodemodCLI(['--source-type', 'hypercard'])).toEqual({
-    status: 1,
-    stdout: '',
-    stderr: expect.stringContaining(
-      `ERROR: expected '--source-type' to be one of "module", "script", or "unambiguous" but got: "hypercard"`
-    ),
-  })
-})
diff --git a/packages/cli/__tests__/fixtures/astexplorer/default.json b/packages/cli/__tests__/fixtures/astexplorer/default.json
deleted file mode 100644
index a91dca02..00000000
--- a/packages/cli/__tests__/fixtures/astexplorer/default.json
+++ /dev/null
@@ -1,93 +0,0 @@
-{
-  "url": "https://api.github.com/gists/68827467161b95ee48048ff906fab6d8/5ece951309157948661ae732af89209715bfe532",
-  "forks_url": "https://api.github.com/gists/68827467161b95ee48048ff906fab6d8/forks",
-  "commits_url": "https://api.github.com/gists/68827467161b95ee48048ff906fab6d8/commits",
-  "id": "68827467161b95ee48048ff906fab6d8",
-  "git_pull_url": "https://gist.github.com/68827467161b95ee48048ff906fab6d8.git",
-  "git_push_url": "https://gist.github.com/68827467161b95ee48048ff906fab6d8.git",
-  "html_url": "https://gist.github.com/68827467161b95ee48048ff906fab6d8",
-  "files": {
-    "astexplorer.json": {
-      "filename": "astexplorer.json",
-      "type": "application/json",
-      "language": "JSON",
-      "raw_url": "https://gist.githubusercontent.com/astexplorer/68827467161b95ee48048ff906fab6d8/raw/cb6fddcb3f083959bd7a1eb5ad5162a4aae118a7/astexplorer.json",
-      "size": 172,
-      "truncated": false,
-      "content": "{\n  \"v\": 2,\n  \"parserID\": \"babylon6\",\n  \"toolID\": \"babelv6\",\n  \"settings\": {\n    \"babylon6\": {}\n  },\n  \"versions\": {\n    \"babylon6\": \"6.18.0\",\n    \"babelv6\": \"6.26.0\"\n  }\n}"
-    },
-    "source.js": {
-      "filename": "source.js",
-      "type": "application/javascript",
-      "language": "JavaScript",
-      "raw_url": "https://gist.githubusercontent.com/astexplorer/68827467161b95ee48048ff906fab6d8/raw/6fd1298030902993fc1162089b648dd6cd37206d/source.js",
-      "size": 476,
-      "truncated": false,
-      "content": "/**\n * Paste or drop some JavaScript here and explore\n * the syntax tree created by chosen parser.\n * You can use all the cool new features from ES6\n * and even more. Enjoy!\n */\n\nlet tips = [\n  \"Click on any AST node with a '+' to expand it\",\n\n  \"Hovering over a node highlights the \\\n   corresponding part in the source code\",\n\n  \"Shift click on an AST node expands the whole substree\"\n];\n\nfunction printTips() {\n  tips.forEach((tip, i) => console.log(`Tip ${i}:` + tip));\n}\n"
-    },
-    "transform.js": {
-      "filename": "transform.js",
-      "type": "application/javascript",
-      "language": "JavaScript",
-      "raw_url": "https://gist.githubusercontent.com/astexplorer/68827467161b95ee48048ff906fab6d8/raw/610c084b3ef8b9d5d481f01098fb46ff80f7a570/transform.js",
-      "size": 252,
-      "truncated": false,
-      "content": "export default function (babel) {\n  const { types: t } = babel;\n  \n  return {\n    name: \"ast-transform\", // not required\n    visitor: {\n      Identifier(path) {\n        path.node.name = path.node.name.split('').reverse().join('');\n      }\n    }\n  };\n}\n"
-    }
-  },
-  "public": false,
-  "created_at": "2018-01-03T14:16:56Z",
-  "updated_at": "2018-01-03T14:19:43Z",
-  "description": null,
-  "comments": 0,
-  "user": null,
-  "comments_url": "https://api.github.com/gists/68827467161b95ee48048ff906fab6d8/comments",
-  "owner": {
-    "login": "astexplorer",
-    "id": 24887483,
-    "avatar_url": "https://avatars2.githubusercontent.com/u/24887483?v=4",
-    "gravatar_id": "",
-    "url": "https://api.github.com/users/astexplorer",
-    "html_url": "https://github.com/astexplorer",
-    "followers_url": "https://api.github.com/users/astexplorer/followers",
-    "following_url": "https://api.github.com/users/astexplorer/following{/other_user}",
-    "gists_url": "https://api.github.com/users/astexplorer/gists{/gist_id}",
-    "starred_url": "https://api.github.com/users/astexplorer/starred{/owner}{/repo}",
-    "subscriptions_url": "https://api.github.com/users/astexplorer/subscriptions",
-    "organizations_url": "https://api.github.com/users/astexplorer/orgs",
-    "repos_url": "https://api.github.com/users/astexplorer/repos",
-    "events_url": "https://api.github.com/users/astexplorer/events{/privacy}",
-    "received_events_url": "https://api.github.com/users/astexplorer/received_events",
-    "type": "User",
-    "site_admin": false
-  },
-  "forks": [],
-  "history": [
-    {
-      "user": {
-        "login": "astexplorer",
-        "id": 24887483,
-        "avatar_url": "https://avatars2.githubusercontent.com/u/24887483?v=4",
-        "gravatar_id": "",
-        "url": "https://api.github.com/users/astexplorer",
-        "html_url": "https://github.com/astexplorer",
-        "followers_url": "https://api.github.com/users/astexplorer/followers",
-        "following_url": "https://api.github.com/users/astexplorer/following{/other_user}",
-        "gists_url": "https://api.github.com/users/astexplorer/gists{/gist_id}",
-        "starred_url": "https://api.github.com/users/astexplorer/starred{/owner}{/repo}",
-        "subscriptions_url": "https://api.github.com/users/astexplorer/subscriptions",
-        "organizations_url": "https://api.github.com/users/astexplorer/orgs",
-        "repos_url": "https://api.github.com/users/astexplorer/repos",
-        "events_url": "https://api.github.com/users/astexplorer/events{/privacy}",
-        "received_events_url": "https://api.github.com/users/astexplorer/received_events",
-        "type": "User",
-        "site_admin": false
-      },
-      "version": "5ece951309157948661ae732af89209715bfe532",
-      "committed_at": "2018-01-03T14:16:55Z",
-      "change_status": { "total": 43, "additions": 43, "deletions": 0 },
-      "url": "https://api.github.com/gists/68827467161b95ee48048ff906fab6d8/5ece951309157948661ae732af89209715bfe532"
-    }
-  ],
-  "truncated": false
-}
diff --git a/packages/cli/__tests__/fixtures/babel-config/babel.config.js b/packages/cli/__tests__/fixtures/babel-config/babel.config.js
deleted file mode 100644
index 028da5a9..00000000
--- a/packages/cli/__tests__/fixtures/babel-config/babel.config.js
+++ /dev/null
@@ -1,16 +0,0 @@
-/* eslint-env node */
-
-module.exports = function (api) {
-  api.cache(true)
-  return {
-    plugins: [
-      {
-        visitor: {
-          NumericLiteral(path) {
-            path.node.value = 42
-          },
-        },
-      },
-    ],
-  }
-}
diff --git a/packages/cli/__tests__/fixtures/babel-config/index.js b/packages/cli/__tests__/fixtures/babel-config/index.js
deleted file mode 100644
index f36673f6..00000000
--- a/packages/cli/__tests__/fixtures/babel-config/index.js
+++ /dev/null
@@ -1 +0,0 @@
-const a = 1
diff --git a/packages/cli/__tests__/fixtures/glob-test/abc.js b/packages/cli/__tests__/fixtures/glob-test/abc.js
deleted file mode 100644
index e69de29b..00000000
diff --git a/packages/cli/__tests__/fixtures/glob-test/subdir/def.js b/packages/cli/__tests__/fixtures/glob-test/subdir/def.js
deleted file mode 100644
index e69de29b..00000000
diff --git a/packages/cli/__tests__/fixtures/plugin/append-options-string.ts b/packages/cli/__tests__/fixtures/plugin/append-options-string.ts
deleted file mode 100644
index 9f2a9750..00000000
--- a/packages/cli/__tests__/fixtures/plugin/append-options-string.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { defineCodemod } from '../../../src'
-
-export default defineCodemod(({ t }, options) => ({
-  visitor: {
-    Program(path) {
-      path.node.body.push(
-        t.expressionStatement(t.stringLiteral(JSON.stringify(options)))
-      )
-    },
-  },
-}))
diff --git a/packages/cli/__tests__/fixtures/plugin/bad-plugin.js b/packages/cli/__tests__/fixtures/plugin/bad-plugin.js
deleted file mode 100644
index ea0f5147..00000000
--- a/packages/cli/__tests__/fixtures/plugin/bad-plugin.js
+++ /dev/null
@@ -1,9 +0,0 @@
-module.exports = function () {
-  return {
-    visitor: {
-      NumericLiteral(path) {
-        throw new Error('I am a bad plugin')
-      },
-    },
-  }
-}
diff --git a/packages/cli/__tests__/fixtures/plugin/class-properties.ts b/packages/cli/__tests__/fixtures/plugin/class-properties.ts
deleted file mode 100644
index 104da8ac..00000000
--- a/packages/cli/__tests__/fixtures/plugin/class-properties.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { PluginObj } from '@babel/core'
-
-class Count {
-  // This file exists to verify that class properties like ↓ can be loaded.
-  count = 0
-
-  incr(): void {
-    this.count++
-  }
-}
-
-export default function (): PluginObj {
-  const debuggerCount = new Count()
-
-  return {
-    visitor: {
-      DebuggerStatement(): void {
-        debuggerCount.incr()
-      },
-    },
-  }
-}
diff --git a/packages/cli/__tests__/fixtures/plugin/generators.ts b/packages/cli/__tests__/fixtures/plugin/generators.ts
deleted file mode 100644
index 83d480db..00000000
--- a/packages/cli/__tests__/fixtures/plugin/generators.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { PluginObj } from '@babel/core'
-
-// This file exists to verify that generator functions like ↓ can be loaded.
-function* counter(): IterableIterator {
-  let i = 0
-
-  while (true) {
-    yield i++
-  }
-}
-
-export default function (): PluginObj {
-  const identifiers = counter()
-
-  return {
-    visitor: {
-      Identifier(): void {
-        console.log(identifiers.next())
-      },
-    },
-  }
-}
diff --git a/packages/cli/__tests__/fixtures/plugin/increment-export-default-multiple/increment-export-default.js b/packages/cli/__tests__/fixtures/plugin/increment-export-default-multiple/increment-export-default.js
deleted file mode 100644
index 400b8e6e..00000000
--- a/packages/cli/__tests__/fixtures/plugin/increment-export-default-multiple/increment-export-default.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import incrementValue from './increment-value'
-
-export default function () {
-  return {
-    visitor: {
-      NumericLiteral(path) {
-        path.node.value = incrementValue(path.node.value)
-      },
-    },
-  }
-}
diff --git a/packages/cli/__tests__/fixtures/plugin/increment-export-default-multiple/increment-value.js b/packages/cli/__tests__/fixtures/plugin/increment-export-default-multiple/increment-value.js
deleted file mode 100644
index a7c405df..00000000
--- a/packages/cli/__tests__/fixtures/plugin/increment-export-default-multiple/increment-value.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import path from 'path'
-
-export default function incrementValue(value) {
-  return value + 1
-}
diff --git a/packages/cli/__tests__/fixtures/plugin/increment-export-default.js b/packages/cli/__tests__/fixtures/plugin/increment-export-default.js
deleted file mode 100644
index 4bf4878f..00000000
--- a/packages/cli/__tests__/fixtures/plugin/increment-export-default.js
+++ /dev/null
@@ -1,9 +0,0 @@
-export default function () {
-  return {
-    visitor: {
-      NumericLiteral(path) {
-        path.node.value += 1
-      },
-    },
-  }
-}
diff --git a/packages/cli/__tests__/fixtures/plugin/increment-typescript.ts b/packages/cli/__tests__/fixtures/plugin/increment-typescript.ts
deleted file mode 100644
index ebc606bc..00000000
--- a/packages/cli/__tests__/fixtures/plugin/increment-typescript.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { defineCodemod } from '../../../src'
-
-enum IncrementValues {
-  One = 1,
-  Two = 2,
-}
-
-export default defineCodemod(() => ({
-  visitor: {
-    NumericLiteral(path) {
-      path.node.value += IncrementValues.One
-    },
-  },
-}))
diff --git a/packages/cli/__tests__/fixtures/plugin/increment.js b/packages/cli/__tests__/fixtures/plugin/increment.js
deleted file mode 100644
index 5bff9ff3..00000000
--- a/packages/cli/__tests__/fixtures/plugin/increment.js
+++ /dev/null
@@ -1,9 +0,0 @@
-module.exports = function () {
-  return {
-    visitor: {
-      NumericLiteral(path) {
-        path.node.value += 1
-      },
-    },
-  }
-}
diff --git a/packages/cli/__tests__/fixtures/plugin/index.js b/packages/cli/__tests__/fixtures/plugin/index.js
deleted file mode 100644
index b93418a4..00000000
--- a/packages/cli/__tests__/fixtures/plugin/index.js
+++ /dev/null
@@ -1,6 +0,0 @@
-module.exports = function () {
-  return {
-    name: 'basic-plugin',
-    visitor: {},
-  }
-}
diff --git a/packages/cli/__tests__/fixtures/plugin/replace-any-with-object.ts b/packages/cli/__tests__/fixtures/plugin/replace-any-with-object.ts
deleted file mode 100644
index 6368167a..00000000
--- a/packages/cli/__tests__/fixtures/plugin/replace-any-with-object.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import * as Babel from '@babel/core'
-import { NodePath } from '@babel/traverse'
-import { TSAnyKeyword } from '@babel/types'
-
-export default function (babel: typeof Babel): Babel.PluginObj {
-  const { types: t } = babel
-
-  return {
-    visitor: {
-      TSAnyKeyword(path: NodePath) {
-        path.replaceWith(t.tsObjectKeyword())
-      },
-    },
-  }
-}
diff --git a/packages/cli/__tests__/fixtures/prettier/defaults/.prettierrc b/packages/cli/__tests__/fixtures/prettier/defaults/.prettierrc
deleted file mode 100644
index 0967ef42..00000000
--- a/packages/cli/__tests__/fixtures/prettier/defaults/.prettierrc
+++ /dev/null
@@ -1 +0,0 @@
-{}
diff --git a/packages/cli/__tests__/fixtures/prettier/defaults/index.jsx b/packages/cli/__tests__/fixtures/prettier/defaults/index.jsx
deleted file mode 100644
index 96a04534..00000000
--- a/packages/cli/__tests__/fixtures/prettier/defaults/index.jsx
+++ /dev/null
@@ -1,34 +0,0 @@
-function HelloWorld({
-  greeting = "hello",
-  greeted = '"World"',
-  silent = false,
-  onMouseOver,
-}) {
-  if (!greeting) {
-    return null;
-  }
-
-  // TODO: Don't use random in render
-  let num = Math.floor(Math.random() * 1e7)
-    .toString()
-    .replace(/\.\d+/gi, "");
-
-  return (
-    
-      
-        {greeting.slice(0, 1).toUpperCase() + greeting.slice(1).toLowerCase()}
-       
-      {greeting.endsWith(",") ? (
-        " "
-      ) : (
-        ", " 
-      )}
-      {greeted} 
-      {silent ? "." : "!"}
-    
-  );
-}
diff --git a/packages/cli/__tests__/fixtures/prettier/with-config/.prettierrc b/packages/cli/__tests__/fixtures/prettier/with-config/.prettierrc
deleted file mode 100644
index 650cb880..00000000
--- a/packages/cli/__tests__/fixtures/prettier/with-config/.prettierrc
+++ /dev/null
@@ -1,4 +0,0 @@
-{
-  "singleQuote": true,
-  "semi": true
-}
diff --git a/packages/cli/__tests__/fixtures/prettier/with-config/index.js b/packages/cli/__tests__/fixtures/prettier/with-config/index.js
deleted file mode 100644
index 64cd2c01..00000000
--- a/packages/cli/__tests__/fixtures/prettier/with-config/index.js
+++ /dev/null
@@ -1 +0,0 @@
-var a = '';
diff --git a/packages/cli/__tests__/helpers/TestServer.ts b/packages/cli/__tests__/helpers/TestServer.ts
deleted file mode 100644
index f016a573..00000000
--- a/packages/cli/__tests__/helpers/TestServer.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import getPort = require('get-port')
-import { createServer, IncomingMessage, Server, ServerResponse } from 'http'
-import { URL } from 'url'
-
-export type RequestHandler = (req: IncomingMessage, res: ServerResponse) => void
-
-export class RealTestServer {
-  private server: Server
-
-  readonly protocol: string = 'http:'
-  readonly hostname: string = '127.0.0.1'
-
-  constructor(readonly port: number, readonly handler: RequestHandler) {
-    this.server = createServer(handler)
-  }
-
-  async start(): Promise {
-    return new Promise((resolve) => {
-      this.server.once('listening', () => resolve())
-      this.server.listen(this.port)
-    })
-  }
-
-  async stop(): Promise {
-    return new Promise((resolve) => {
-      this.server.close(() => resolve())
-    })
-  }
-
-  requestURL(path: string): URL {
-    return new URL(`${this.protocol}//${this.hostname}:${this.port}${path}`)
-  }
-}
-
-export async function startServer(
-  handler: RequestHandler
-): Promise {
-  const port = await getPort()
-  const server = new RealTestServer(port, handler)
-  await server.start()
-  return server
-}
diff --git a/packages/cli/__tests__/helpers/copyFixturesInto.ts b/packages/cli/__tests__/helpers/copyFixturesInto.ts
deleted file mode 100644
index 116eaee3..00000000
--- a/packages/cli/__tests__/helpers/copyFixturesInto.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { promises as fs } from 'fs'
-import { dirname, join, relative } from 'path'
-import { iterateSources } from '../../src/iterateSources'
-import mkdirp = require('make-dir')
-
-export default async function copyFixturesInto(
-  fixture: string,
-  destination: string
-): Promise {
-  const fixturePath = join('test/fixtures', fixture)
-
-  for await (const file of iterateSources([fixturePath])) {
-    const relativePath = relative(fixturePath, file.path)
-    const destinationPath = join(destination, relativePath)
-    await mkdirp(dirname(destinationPath))
-    await fs.writeFile(destinationPath, file.content)
-  }
-
-  return destination
-}
diff --git a/packages/cli/__tests__/helpers/createTemporaryDirectory.ts b/packages/cli/__tests__/helpers/createTemporaryDirectory.ts
deleted file mode 100644
index de3afd0a..00000000
--- a/packages/cli/__tests__/helpers/createTemporaryDirectory.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import tempy = require('tempy')
-
-export default async function createTemporaryDirectory(): Promise {
-  return tempy.directory()
-}
diff --git a/packages/cli/__tests__/helpers/createTemporaryFile.ts b/packages/cli/__tests__/helpers/createTemporaryFile.ts
deleted file mode 100644
index 7ecf34bb..00000000
--- a/packages/cli/__tests__/helpers/createTemporaryFile.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { promises as fs } from 'fs'
-import { dirname, join } from 'path'
-import tempy = require('tempy')
-import createTemporaryDirectory from './createTemporaryDirectory'
-
-export default async function createTemporaryFile(
-  name: string,
-  content: string
-): Promise {
-  const fullPath = tempy.file({ name })
-  await fs.writeFile(fullPath, content, 'utf8')
-  return fullPath
-}
-
-export async function createTemporaryFiles(
-  ...files: Array<[name: string, content: string]>
-): Promise> {
-  const root = await createTemporaryDirectory()
-
-  return Promise.all(
-    files.map(async ([name, content]) => {
-      const fullPath = join(root, name)
-      await fs.mkdir(dirname(fullPath), { recursive: true })
-      await fs.writeFile(fullPath, content)
-      return fullPath
-    })
-  )
-}
diff --git a/packages/cli/__tests__/helpers/plugin.ts b/packages/cli/__tests__/helpers/plugin.ts
deleted file mode 100644
index 634b145c..00000000
--- a/packages/cli/__tests__/helpers/plugin.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { join } from 'path'
-
-export default function plugin(name: string, ext = '.js'): string {
-  return join(__dirname, `../fixtures/plugin/${name}${ext}`)
-}
diff --git a/packages/cli/__tests__/helpers/runCodemodCLI.ts b/packages/cli/__tests__/helpers/runCodemodCLI.ts
deleted file mode 100644
index 26d64be0..00000000
--- a/packages/cli/__tests__/helpers/runCodemodCLI.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { spawn } from 'child_process'
-import { join } from 'path'
-
-export interface CLIResult {
-  status: number
-  stdout: string
-  stderr: string
-}
-
-export async function runCodemodCLI(
-  args: Array,
-  { stdin = '', cwd }: { stdin?: string; cwd?: string } = {}
-): Promise {
-  const child = spawn(
-    process.env.NODE ?? process.argv0,
-    [join(__dirname, '../../bin/codemod'), ...args],
-    {
-      stdio: 'pipe',
-      cwd,
-      env: { CODEMOD_RUN_WITH_ESBUILD: '1' },
-    }
-  )
-
-  child.stdin.end(stdin)
-
-  let stdout = ''
-  child.stdout.setEncoding('utf-8').on('readable', () => {
-    stdout += child.stdout.read() ?? ''
-  })
-
-  let stderr = ''
-  child.stderr.setEncoding('utf-8').on('readable', () => {
-    stderr += child.stderr.read() ?? ''
-  })
-
-  return new Promise((resolve, reject) => {
-    child
-      .on('exit', (status) => {
-        resolve({ status: status ?? 1, stdout, stderr })
-      })
-      .on('error', reject)
-  })
-}
diff --git a/packages/cli/__tests__/integration.test.ts b/packages/cli/__tests__/integration.test.ts
deleted file mode 100644
index 8290a03c..00000000
--- a/packages/cli/__tests__/integration.test.ts
+++ /dev/null
@@ -1,361 +0,0 @@
-import generate from '@babel/generator'
-import traverse, { NodePath } from '@babel/traverse'
-import { transform } from '@codemod/core'
-import * as m from '@codemod/matchers'
-import { expression, js, t } from '@codemod/utils'
-import convertQUnitAssertExpectToAssertAsync from '../examples/convert-qunit-assert-expect-to-assert-async'
-import convertStaticClassToNamedExports from '../examples/convert-static-class-to-named-exports'
-import dedent = require('dedent')
-
-/**
- * This test demonstrates using captures to extract parts of an AST for use in
- * a codemod that unwraps unnecessary IIFEs. For example:
- *
- * ```js
- * function iifeWithExpression() {
- *   return (() => EXPR);
- * }
- *
- * function iifeWithStatements() {
- *   return (() => {
- *     STMT1;
- *     STMT2;
- *   });
- * }
- *
- * // becomes
- *
- * function iifeWithExpression() {
- *   return EXPR;
- * }
- *
- * function iifeWithStatements() {
- *   STMT1;
- *   STMT2;
- * }
- * ```
- */
-test('codemod: unwrap unneeded IIFE', () => {
-  const ast = js(dedent`
-    function foo() {
-      return (() => 1)();
-    }
-  `)
-
-  let body: m.CapturedMatcher
-
-  const returnedIIFEMatcher = m.returnStatement(
-    m.callExpression(
-      m.function(
-        [],
-        (body = m.capture(m.or(m.anyExpression(), m.blockStatement())))
-      ) as m.Matcher
-    )
-  )
-
-  traverse(ast, {
-    ReturnStatement(path: NodePath): void {
-      m.match(returnedIIFEMatcher, { body }, path.node, ({ body }) => {
-        if (t.isExpression(body)) {
-          path.replaceWith(t.returnStatement(body))
-        } else {
-          path.replaceWithMultiple(body.body)
-        }
-      })
-    },
-  })
-
-  expect(generate(ast).code).toEqual(dedent`
-    function foo() {
-      return 1;
-    }
-  `)
-})
-
-test('codemod: remove return labels', () => {
-  const ast = js(dedent`
-    function foo() {
-      console.log("calling foo");
-      let classlist;
-      console.log("about to return");
-      return (classlist = classes.join(" "));
-    }
-  `)
-  let labelDeclaration: m.CapturedMatcher
-  let label: m.CapturedMatcher
-  let returnStatement: m.CapturedMatcher
-  let value: m.CapturedMatcher
-
-  const returnLabelFunctionMatcher = m.function(
-    undefined,
-    m.blockStatement(
-      m.anyList(
-        m.zeroOrMore(),
-        (labelDeclaration = m.capture(
-          m.variableDeclaration(
-            'let',
-            m.oneOf(
-              m.variableDeclarator(
-                m.identifier((label = m.capture(m.anyString()))),
-                null
-              )
-            )
-          )
-        )),
-        m.zeroOrMore(),
-        (returnStatement = m.capture(
-          m.returnStatement(
-            m.assignmentExpression(
-              '=',
-              m.identifier(m.fromCapture(label)),
-              (value = m.capture())
-            )
-          )
-        )),
-        m.zeroOrMore()
-      )
-    )
-  )
-
-  function processFunction(path: NodePath): void {
-    m.match(
-      returnLabelFunctionMatcher,
-      { returnStatement, value, labelDeclaration },
-      path.node,
-      ({ returnStatement, value, labelDeclaration }) => {
-        const bodyPath = path.get('body')
-        if (bodyPath.isBlockStatement()) {
-          const labelDeclarationPath = bodyPath
-            .get('body')
-            .find((statementPath) => statementPath.node === labelDeclaration)
-
-          if (labelDeclarationPath) {
-            labelDeclarationPath.remove()
-          }
-        }
-        returnStatement.argument = value
-      }
-    )
-  }
-
-  traverse(ast, {
-    FunctionDeclaration: processFunction,
-    FunctionExpression: processFunction,
-    ArrowFunctionExpression: processFunction,
-  })
-
-  expect(generate(ast).code).toEqual(dedent`
-    function foo() {
-      console.log("calling foo");
-      console.log("about to return");
-      return classes.join(" ");
-    }
-  `)
-})
-
-test('codemod: assert to jest expect', () => {
-  const ast = js(dedent`
-    assert.strictEqual(a, 7);
-    assert.deepEqual(b, {});
-    assert.ok(c);
-    assert.ok(!d);
-  `)
-
-  let actual: m.CapturedMatcher
-  let expected: m.CapturedMatcher
-  const assertEqualMatcher = m.callExpression(
-    m.memberExpression(
-      m.identifier('assert'),
-      m.identifier(m.or('strictEqual', 'deepEqual'))
-    ),
-    [
-      (actual = m.capture(m.anyExpression())),
-      (expected = m.capture(m.anyExpression())),
-    ]
-  )
-
-  let falsyValue: m.CapturedMatcher
-  const assertFalsyMatcher = m.callExpression(
-    m.memberExpression(m.identifier('assert'), m.identifier('ok')),
-    [m.unaryExpression('!', (falsyValue = m.capture(m.anyExpression())))]
-  )
-
-  let truthValue: m.CapturedMatcher
-  const assertTruthyMatcher = m.callExpression(
-    m.memberExpression(m.identifier('assert'), m.identifier('ok')),
-    [(truthValue = m.capture())]
-  )
-
-  traverse(ast, {
-    CallExpression(path: NodePath): void {
-      // replace equality assertions
-      m.match(
-        assertEqualMatcher,
-        { actual, expected },
-        path.node,
-        ({ actual, expected }) => {
-          path.replaceWith(
-            t.callExpression(
-              t.memberExpression(
-                t.callExpression(t.identifier('expect'), [actual]),
-                t.identifier('toEqual')
-              ),
-              [expected]
-            )
-          )
-        }
-      )
-
-      // replace e.g. `assert.ok(!a)` with `expect(a).toBeFalsy()`
-      m.match(
-        assertFalsyMatcher,
-        { falsyValue },
-        path.node,
-        ({ falsyValue }) => {
-          path.replaceWith(
-            t.callExpression(
-              t.memberExpression(
-                t.callExpression(t.identifier('expect'), [falsyValue]),
-                t.identifier('toBeFalsy')
-              ),
-              []
-            )
-          )
-        }
-      )
-
-      // replace e.g. `assert.ok(a)` with `expect(a).toBeTruthy()`
-      m.match(
-        assertTruthyMatcher,
-        { truthValue },
-        path.node,
-        ({ truthValue }) => {
-          path.replaceWith(
-            t.callExpression(
-              t.memberExpression(
-                t.callExpression(t.identifier('expect'), [truthValue]),
-                t.identifier('toBeTruthy')
-              ),
-              []
-            )
-          )
-        }
-      )
-    },
-  })
-
-  expect(generate(ast).code).toEqual(dedent`
-    expect(a).toEqual(7);
-    expect(b).toEqual({});
-    expect(c).toBeTruthy();
-    expect(d).toBeFalsy();
-  `)
-})
-
-test('codemod: double-equal null to triple-equal', () => {
-  const ast = js('a == null;')
-  const left = m.capture(m.identifier())
-  const eqeqNullMatcher = m.binaryExpression('==', left, m.nullLiteral())
-  const eqNullOrUndefined = expression<{
-    left: t.Expression
-  }>('%%left%% === null || %%left%% === undefined')
-
-  traverse(ast, {
-    BinaryExpression(path: NodePath): void {
-      m.match(eqeqNullMatcher, { left }, path.node, ({ left }) => {
-        path.replaceWith(eqNullOrUndefined({ left }))
-      })
-    },
-  })
-
-  expect(generate(ast).code).toEqual('a === null || a === undefined;')
-})
-
-test('codemod: assert.expect to assert.async', () => {
-  const input = dedent`
-    test("my test", function (assert) {
-      assert.expect(1);
-      window.server.get("/some/api", () => {
-        asyncThing().then(() => {
-          assert.ok(true, "API called!");
-        });
-      });
-      doStuff();
-    });
-  `
-
-  const output = transform(input, {
-    plugins: [convertQUnitAssertExpectToAssertAsync()],
-  })
-
-  expect(output && output.code).toEqual(dedent`
-    test("my test", function (assert) {
-      const done = assert.async();
-      window.server.get("/some/api", () => {
-        asyncThing().then(() => {
-          assert.ok(true, "API called!");
-          done();
-        });
-      });
-      doStuff();
-    });
-  `)
-})
-
-test('codemod: convert static exported class to named exports', () => {
-  const code = dedent`
-    class MobileAppUpsellHelper {
-      static getIosAppLink(specialTrackingLink) {
-        const trackingLink = specialTrackingLink || "IOS_BRANCH_LINK";
-        return this.getBranchLink(trackingLink);
-      }
-
-      static getAndroidAppLink(specialTrackingLink) {
-        const trackingLink = specialTrackingLink || "ANDROID_BRANCH_LINK";
-        return this.getBranchLink(trackingLink);
-      }
-
-      static getBranchLink(specialTrackingLink) {
-        if (specialTrackingLink && APP_DOWNLOAD_ASSETS[specialTrackingLink]) {
-          return APP_DOWNLOAD_ASSETS[specialTrackingLink];
-        }
-
-        return APP_DOWNLOAD_ASSETS.DEFAULT_BRANCH_LINK;
-      }
-
-      static getHideAppBanner() {
-        return CookieHelper.get("hide_app_banner");
-      }
-    }
-
-    export default MobileAppUpsellHelper;
-  `
-
-  const output = transform(code, {
-    plugins: [convertStaticClassToNamedExports()],
-  })
-
-  expect(output && output.code).toEqual(dedent`
-    export function getIosAppLink(specialTrackingLink) {
-      const trackingLink = specialTrackingLink || "IOS_BRANCH_LINK";
-      return getBranchLink(trackingLink);
-    }
-
-    export function getAndroidAppLink(specialTrackingLink) {
-      const trackingLink = specialTrackingLink || "ANDROID_BRANCH_LINK";
-      return getBranchLink(trackingLink);
-    }
-
-    export function getBranchLink(specialTrackingLink) {
-      if (specialTrackingLink && APP_DOWNLOAD_ASSETS[specialTrackingLink]) {
-        return APP_DOWNLOAD_ASSETS[specialTrackingLink];
-      }
-
-      return APP_DOWNLOAD_ASSETS.DEFAULT_BRANCH_LINK;
-    }
-
-    export function getHideAppBanner() {
-      return CookieHelper.get("hide_app_banner");
-    }
-  `)
-})
diff --git a/packages/cli/__tests__/unit/Config.test.ts b/packages/cli/__tests__/unit/Config.test.ts
deleted file mode 100644
index ed281900..00000000
--- a/packages/cli/__tests__/unit/Config.test.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { join } from 'path'
-import { inspect } from 'util'
-import { Config, ConfigBuilder } from '../../src/Config'
-
-// TODO: move some of the babel plugin loading tests in here
-
-test('has sensible defaults', function () {
-  const config = new Config()
-  expect(config.extensions).toContain('.js')
-  expect(config.extensions).toContain('.ts')
-  expect(config.extensions).toContain('.jsx')
-  expect(config.extensions).toContain('.tsx')
-  expect(config.localPlugins).toEqual([])
-  expect(config.sourcePaths).toEqual([])
-  expect(config.requires).toEqual([])
-  expect(config.pluginOptions.size).toEqual(0)
-  expect(config.stdio).toEqual(false)
-})
-
-test('associates plugin options based on declared name', async function () {
-  const config = new ConfigBuilder()
-    .addLocalPlugin(join(__dirname, '../fixtures/plugin/index.js'))
-    .setOptionsForPlugin({ a: true }, 'basic-plugin')
-    .build()
-
-  // "basic-plugin" is declared in the plugin file
-  const babelPlugin = await config.getBabelPlugin('basic-plugin')
-
-  if (!Array.isArray(babelPlugin)) {
-    throw new Error(
-      `expected plugin to be [plugin, options] tuple: ${inspect(babelPlugin)}`
-    )
-  }
-
-  expect(babelPlugin[1]).toEqual({ a: true })
-})
diff --git a/packages/cli/__tests__/unit/InlineTransformer.test.ts b/packages/cli/__tests__/unit/InlineTransformer.test.ts
deleted file mode 100644
index 825d386b..00000000
--- a/packages/cli/__tests__/unit/InlineTransformer.test.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-import { PluginItem } from '@babel/core'
-import { NodePath } from '@babel/traverse'
-import { NumericLiteral, Program } from '@babel/types'
-import { join } from 'path'
-import { InlineTransformer } from '../../src/InlineTransformer'
-
-test('passes source through as-is when there are no plugins', async function () {
-  const filepath = 'a.js'
-  const content = 'a + b;'
-  const transformer = new InlineTransformer([])
-  const output = await transformer.transform(filepath, content)
-
-  expect(output).toEqual(content)
-})
-
-test('transforms source using plugins', async function () {
-  const filepath = 'a.js'
-  const content = '3 + 4;'
-  const transformer = new InlineTransformer([
-    (): PluginItem => ({
-      visitor: {
-        NumericLiteral(path: NodePath): void {
-          path.node.value++
-        },
-      },
-    }),
-  ])
-  const output = await transformer.transform(filepath, content)
-
-  expect(output).toEqual('4 + 5;')
-})
-
-test('does not include any plugins not specified explicitly', async function () {
-  const filepath = 'a.js'
-  const content = 'export default 0;'
-  const transformer = new InlineTransformer([])
-  const output = await transformer.transform(filepath, content)
-
-  expect(output).toEqual('export default 0;')
-})
-
-test('allows running plugins with options', async function () {
-  const filepath = 'a.js'
-  const content = '3 + 4;'
-  const transformer = new InlineTransformer([
-    [
-      (): PluginItem => ({
-        visitor: {
-          NumericLiteral(
-            path: NodePath,
-            state: { opts: { value?: number } }
-          ) {
-            if (state.opts.value === path.node.value) {
-              path.node.value++
-            }
-          },
-        },
-      }),
-      { value: 3 },
-    ],
-  ])
-  const output = await transformer.transform(filepath, content)
-
-  expect(output).toEqual('4 + 4;')
-})
-
-test('passes the filename', async function () {
-  const filepath = 'a.js'
-  const content = ''
-  let filename: string | undefined
-
-  const transformer = new InlineTransformer([
-    (): PluginItem => ({
-      visitor: {
-        Program(
-          path: NodePath,
-          state: {
-            file: { opts: { filename: string } }
-          }
-        ) {
-          filename = state.file.opts.filename
-        },
-      },
-    }),
-  ])
-
-  // Ignore the result since we only care about arguments to the visitor.
-  await transformer.transform(filepath, content)
-
-  expect(filename).toEqual(join(process.cwd(), 'a.js'))
-})
diff --git a/packages/cli/__tests__/unit/Options.test.ts b/packages/cli/__tests__/unit/Options.test.ts
deleted file mode 100644
index 341e2412..00000000
--- a/packages/cli/__tests__/unit/Options.test.ts
+++ /dev/null
@@ -1,155 +0,0 @@
-import { createHash } from 'crypto'
-import { readFile } from 'fs/promises'
-import { join } from 'path'
-import { inspect } from 'util'
-import { Config } from '../../src/Config'
-import { Options, Command } from '../../src/Options'
-
-test('has sensible defaults', function () {
-  const config = getRunConfig(new Options([]).parse())
-  expect(config.extensions).toContain('.js')
-  expect(config.extensions).toContain('.ts')
-  expect(config.extensions).toContain('.jsx')
-  expect(config.extensions).toContain('.tsx')
-  expect(config.localPlugins).toEqual([])
-  expect(config.sourcePaths).toEqual([])
-  expect(config.requires).toEqual([])
-  expect(config.pluginOptions.size).toEqual(0)
-  expect(config.stdio).toEqual(false)
-})
-
-test('interprets `--help` as asking for help', function () {
-  expect(new Options(['--help']).parse().kind).toEqual('help')
-})
-
-test('interprets `--version` as asking to print the version', function () {
-  expect(new Options(['--version']).parse().kind).toEqual('version')
-})
-
-test('interprets `--extensions` as expected', function () {
-  const config = getRunConfig(new Options(['--extensions', '.js,.ts']).parse())
-  expect(config.extensions).toEqual(new Set(['.js', '.ts']))
-})
-
-test('--add-extension adds to the default extensions', function () {
-  const config = getRunConfig(new Options(['--add-extension', '.myjs']).parse())
-  expect(config.extensions.size > 1).toBeTruthy()
-  expect(config.extensions).toContain('.myjs')
-})
-
-test('fails to parse unknown options', function () {
-  expect(() => new Options(['--wtf']).parse()).toThrow(
-    new Error('unexpected option: --wtf')
-  )
-})
-
-test('interprets non-option arguments as paths', function () {
-  const config = getRunConfig(new Options(['src/', 'a.js']).parse())
-  expect(config.sourcePaths).toEqual(['src/', 'a.js'])
-})
-
-test('interprets `--stdio` as reading/writing stdin/stdout', function () {
-  const config = getRunConfig(new Options(['--stdio']).parse())
-  expect(config.stdio).toEqual(true)
-})
-
-test('can parse inline plugin options as JSON', function () {
-  const config = getRunConfig(
-    new Options(['-o', 'my-plugin={"foo": true}']).parse()
-  )
-  expect(config.pluginOptions.get('my-plugin')).toEqual({ foo: true })
-})
-
-test('associates plugin options based on declared name', async function () {
-  const config = getRunConfig(
-    new Options([
-      '--plugin',
-      join(__dirname, '../fixtures/plugin/index.js'),
-      '--plugin-options',
-      'basic-plugin={"a": true}',
-    ]).parse()
-  )
-
-  expect(config.pluginOptions.get('basic-plugin')).toEqual({ a: true })
-})
-
-test('assigns anonymous options to the most recent plugin', async function () {
-  const config = getRunConfig(
-    new Options([
-      '--plugin',
-      join(__dirname, '../fixtures/plugin/index.js'),
-      '--plugin-options',
-      '{"a": true}',
-    ]).parse()
-  )
-
-  expect(
-    config.pluginOptions.get(join(__dirname, '../fixtures/plugin/index.js'))
-  ).toEqual({
-    a: true,
-  })
-})
-
-test('interprets `--require` as expected', async function () {
-  const config = getRunConfig(new Options(['--require', 'tmp']).parse())
-  const expectedTmpRequirePath = require.resolve('tmp')
-
-  expect(config.requires).toHaveLength(1)
-  const [actualTmpRequirePath] = config.requires
-
-  // `require.resolve` and the `resolve` package are not guaranteed to return
-  // the same path, so we compare the hashes of the files instead.
-  const expectedTmpHash = createHash('md5')
-    .update(await readFile(expectedTmpRequirePath))
-    .digest('hex')
-  const actualTmpHash = createHash('md5')
-    .update(await readFile(actualTmpRequirePath))
-    .digest('hex')
-  expect(actualTmpHash).toEqual(expectedTmpHash)
-})
-
-test('associates plugin options based on inferred name', async function () {
-  const config = getRunConfig(
-    new Options([
-      '--plugin',
-      join(__dirname, '../fixtures/plugin/index.js'),
-      '--plugin-options',
-      'index={"a": true}',
-    ]).parse()
-  )
-
-  // "index" is the name of the file
-  expect(config.pluginOptions.get('index')).toEqual({ a: true })
-
-  const babelPlugin = await config.getBabelPlugin('index')
-
-  if (!Array.isArray(babelPlugin)) {
-    throw new Error(
-      `expected plugin to be [plugin, options] tuple: ${inspect(babelPlugin)}`
-    )
-  }
-
-  expect(babelPlugin[1]).toEqual({ a: true })
-})
-
-test('can parse a JSON file for plugin options', function () {
-  // You wouldn't actually use package.json, but it's a convenient JSON file.
-  const config = getRunConfig(
-    new Options(['-o', 'my-plugin=@package.json']).parse()
-  )
-  const pluginOpts = config.pluginOptions.get('my-plugin')
-  expect(pluginOpts && pluginOpts['name']).toEqual('@codemod/cli')
-})
-
-test('should set dry option', function () {
-  const config = getRunConfig(new Options(['--dry']).parse())
-  expect(config.dry).toEqual(true)
-})
-
-function getRunConfig(command: Command): Config {
-  if (command.kind === 'run') {
-    return command.config
-  } else {
-    throw new Error(`expected a run command but got: ${inspect(command)}`)
-  }
-}
diff --git a/packages/cli/__tests__/unit/TransformRunner.test.ts b/packages/cli/__tests__/unit/TransformRunner.test.ts
deleted file mode 100644
index e31ee5f6..00000000
--- a/packages/cli/__tests__/unit/TransformRunner.test.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import { deepEqual } from 'assert'
-import {
-  TransformRunner,
-  Source,
-  SourceTransformResult,
-  SourceTransformResultKind,
-} from '../../src/TransformRunner'
-
-async function run(
-  runner: TransformRunner
-): Promise> {
-  const result: Array = []
-
-  for await (const transformResult of runner.run()) {
-    result.push(transformResult)
-  }
-
-  return result
-}
-
-async function* asyncIterable(elements: Iterable): AsyncGenerator {
-  for (const element of elements) {
-    yield element
-  }
-}
-
-test('generates a result for each source by calling the transformer', async function () {
-  const aSource = new Source('a.js', 'a;')
-  const bSource = new Source('b.js', 'b;')
-  const sources = asyncIterable([aSource, bSource])
-  const runner = new TransformRunner(sources, {
-    async transform(filepath: string, content: string): Promise {
-      return content.toUpperCase()
-    },
-  })
-
-  deepEqual(await run(runner), [
-    {
-      kind: SourceTransformResultKind.Transformed,
-      source: aSource,
-      output: 'A;',
-    },
-    {
-      kind: SourceTransformResultKind.Transformed,
-      source: bSource,
-      output: 'B;',
-    },
-  ])
-})
-
-test('collects errors for each failed source transform', async function () {
-  const source = new Source('fails.js', 'invalid syntax')
-  const sources = asyncIterable([source])
-  const runner = new TransformRunner(sources, {
-    async transform(filepath: string, content: string): Promise {
-      throw new Error(`unable to process ${filepath}: ${content}`)
-    },
-  })
-
-  deepEqual(await run(runner), [
-    {
-      kind: SourceTransformResultKind.Error,
-      source,
-      error: new Error('unable to process fails.js: invalid syntax'),
-    },
-  ])
-})
diff --git a/packages/cli/__tests__/unit/iterateSources.test.ts b/packages/cli/__tests__/unit/iterateSources.test.ts
deleted file mode 100644
index ce93e0d7..00000000
--- a/packages/cli/__tests__/unit/iterateSources.test.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import { dirname, join } from 'path'
-import { iterateSources } from '../../src/iterateSources'
-import createTemporaryDirectory from '../helpers/createTemporaryDirectory'
-import createTemporaryFile, {
-  createTemporaryFiles,
-} from '../helpers/createTemporaryFile'
-
-async function asyncCollect(iter: AsyncIterable): Promise> {
-  const result: Array = []
-  for await (const element of iter) {
-    result.push(element)
-  }
-  return result
-}
-
-test('empty directory', async () => {
-  const dir = await createTemporaryDirectory()
-  expect(await asyncCollect(iterateSources([dir]))).toEqual([])
-})
-
-test('single file', async () => {
-  const file = await createTemporaryFile('a-file', 'with contents')
-  expect(await asyncCollect(iterateSources([dirname(file)]))).toEqual([
-    {
-      path: file,
-      content: 'with contents',
-    },
-  ])
-})
-
-test('selecting extensions', async () => {
-  const [js] = await createTemporaryFiles(
-    ['js-files/file.js', 'JS file'],
-    ['ts-files/file.ts', 'TS file']
-  )
-  const root = dirname(dirname(js))
-  expect(
-    await asyncCollect(iterateSources([root], { extensions: new Set(['.js']) }))
-  ).toEqual([
-    {
-      path: js,
-      content: 'JS file',
-    },
-  ])
-})
-
-test('globbing', async () => {
-  const paths = await createTemporaryFiles(
-    ['main.ts', 'export default 0'],
-    ['main.js', 'module.exports.default = 0'],
-    ['subdir/index.test.ts', ''],
-    ['subdir/utils.ts', ''],
-    ['subdir/utils.test.ts', ''],
-    ['subdir/.gitignore', 'ignored.test.ts'],
-    ['subdir/ignored.test.ts', ''],
-    ['.git/config', '']
-  )
-
-  expect(
-    await asyncCollect(
-      iterateSources(['**/*.test.ts'], { cwd: dirname(paths[0]) })
-    )
-  ).toEqual(
-    paths
-      .filter((path) => path.endsWith('.test.ts') && !path.includes('ignored'))
-      .map((path) =>
-        expect.objectContaining({
-          path,
-        })
-      )
-  )
-})
-
-test('gitignore', async () => {
-  const [main] = await createTemporaryFiles(
-    ['main.ts', 'export default 0'],
-    ['.git/config', ''],
-    ['.gitignore', '*.d.ts'],
-    ['ignored.d.ts', ''],
-    ['subdir/.gitignore', '*.js'],
-    ['subdir/ignored-by-root.d.ts', ''],
-    ['subdir/ignored-by-subdir.js', ''],
-    ['subdir/subdir2/.gitignore', ''],
-    ['subdir/subdir2/ignored-by-root.d.ts', ''],
-    ['subdir/subdir2/ignored-by-subdir.js', '']
-  )
-
-  const root = dirname(main)
-  expect(await asyncCollect(iterateSources([root]))).toEqual([
-    { path: main, content: 'export default 0' },
-  ])
-
-  const subdir = join(root, 'subdir')
-  expect(await asyncCollect(iterateSources([subdir]))).toEqual([])
-})
diff --git a/packages/cli/__tests__/unit/resolvers/AstExplorerResolver.test.ts b/packages/cli/__tests__/unit/resolvers/AstExplorerResolver.test.ts
deleted file mode 100644
index ebfcbb0c..00000000
--- a/packages/cli/__tests__/unit/resolvers/AstExplorerResolver.test.ts
+++ /dev/null
@@ -1,121 +0,0 @@
-import { promises as fs } from 'fs'
-import { join } from 'path'
-import { AstExplorerResolver } from '../../../src/resolvers/AstExplorerResolver'
-import { startServer } from '../../helpers/TestServer'
-
-test('normalizes a gist+commit editor URL into an API URL', async function () {
-  const resolver = new AstExplorerResolver()
-  const normalized = await resolver.normalize(
-    'https://astexplorer.net/#/gist/688274/5ece95'
-  )
-
-  expect(normalized).toEqual(
-    'https://astexplorer.net/api/v1/gist/688274/5ece95'
-  )
-})
-
-test('normalizes http gist+commit editor URL to an https API URL', async function () {
-  const resolver = new AstExplorerResolver()
-  const normalized = await resolver.normalize(
-    'http://astexplorer.net/#/gist/b5b33c/f9ae8a'
-  )
-
-  expect(normalized).toEqual(
-    'https://astexplorer.net/api/v1/gist/b5b33c/f9ae8a'
-  )
-})
-
-test('normalizes a gist-only editor URL into an API URL', async function () {
-  const resolver = new AstExplorerResolver()
-  const normalized = await resolver.normalize(
-    'https://astexplorer.net/#/gist/688274'
-  )
-
-  expect(normalized).toEqual('https://astexplorer.net/api/v1/gist/688274')
-})
-
-test('normalizes a gist+latest editor URL into an API URL', async function () {
-  const resolver = new AstExplorerResolver()
-  const normalized = await resolver.normalize(
-    'https://astexplorer.net/#/gist/688274/latest'
-  )
-
-  expect(normalized).toEqual(
-    'https://astexplorer.net/api/v1/gist/688274/latest'
-  )
-})
-
-test('extracts the transform from the editor view', async function () {
-  const result = await fs.readFile(
-    join(__dirname, '../../fixtures/astexplorer/default.json'),
-    { encoding: 'utf8' }
-  )
-  const server = await startServer((req, res) => {
-    res.end(result)
-  })
-
-  try {
-    const resolver = new AstExplorerResolver(server.requestURL('/'))
-    expect(
-      await fs.readFile(
-        await resolver.resolve(server.requestURL('/#/gist/abc/def').toString()),
-        { encoding: 'utf8' }
-      )
-    ).toEqual(JSON.parse(result).files['transform.js'].content)
-  } finally {
-    await server.stop()
-  }
-})
-
-test('fails when returned data is not JSON', async function () {
-  const server = await startServer((req, res) => {
-    res.end('this is not JSON')
-  })
-  const url = server.requestURL('/')
-
-  try {
-    const resolver = new AstExplorerResolver(server.requestURL('/'))
-
-    await expect(resolver.resolve(url.toString())).rejects.toThrowError(
-      `data loaded from ${url} is not JSON: this is not JSON`
-    )
-  } finally {
-    await server.stop()
-  }
-})
-
-test('fails when files data is not present', async function () {
-  const server = await startServer((req, res) => {
-    res.end(JSON.stringify({}))
-  })
-
-  try {
-    const resolver = new AstExplorerResolver(server.requestURL('/'))
-
-    await expect(
-      resolver.resolve(server.requestURL('/').toString())
-    ).rejects.toThrowError(
-      "'transform.js' could not be found, perhaps transform is disabled"
-    )
-  } finally {
-    await server.stop()
-  }
-})
-
-test('fails when transform.js is not present', async function () {
-  const server = await startServer((req, res) => {
-    res.end(JSON.stringify({ files: {} }))
-  })
-
-  try {
-    const resolver = new AstExplorerResolver(server.requestURL('/'))
-
-    await expect(
-      resolver.resolve(server.requestURL('/').toString())
-    ).rejects.toThrowError(
-      "'transform.js' could not be found, perhaps transform is disabled"
-    )
-  } finally {
-    await server.stop()
-  }
-})
diff --git a/packages/cli/__tests__/unit/resolvers/FileSystemResolver.test.ts b/packages/cli/__tests__/unit/resolvers/FileSystemResolver.test.ts
deleted file mode 100644
index 940171fd..00000000
--- a/packages/cli/__tests__/unit/resolvers/FileSystemResolver.test.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { join } from 'path'
-import { FileSystemResolver } from '../../../src/resolvers/FileSystemResolver'
-
-test('can resolve any files that exist as-is', async function () {
-  const resolver = new FileSystemResolver()
-  expect(await resolver.canResolve(__filename)).toBeTruthy()
-  expect(await resolver.resolve(__filename)).toEqual(__filename)
-})
-
-test('can resolve files by inferring an extension from a configurable set of extensions', async function () {
-  const resolver = new FileSystemResolver(new Set(['.json']))
-  const packageJsonWithoutExtension = join(__dirname, '../../../package')
-  expect(await resolver.canResolve(packageJsonWithoutExtension)).toBeTruthy()
-  expect(await resolver.resolve(packageJsonWithoutExtension)).toEqual(
-    `${packageJsonWithoutExtension}.json`
-  )
-})
-
-test('can resolve files by inferring an dot-less extension from a configurable set of extensions', async function () {
-  const resolver = new FileSystemResolver(new Set(['json']))
-  const packageJsonWithoutExtension = join(__dirname, '../../../package')
-  expect(await resolver.canResolve(packageJsonWithoutExtension)).toBeTruthy()
-  expect(await resolver.resolve(packageJsonWithoutExtension)).toEqual(
-    `${packageJsonWithoutExtension}.json`
-  )
-})
-
-test('fails to resolve a non-existent file', async function () {
-  const resolver = new FileSystemResolver()
-  expect(!(await resolver.canResolve('/this/file/is/not/there'))).toBeTruthy()
-
-  await expect(
-    resolver.resolve('/this/file/is/not/there')
-  ).rejects.toThrowError(
-    'unable to resolve file from source: /this/file/is/not/there'
-  )
-})
diff --git a/packages/cli/__tests__/unit/resolvers/NetworkResolver.test.ts b/packages/cli/__tests__/unit/resolvers/NetworkResolver.test.ts
deleted file mode 100644
index ed30a587..00000000
--- a/packages/cli/__tests__/unit/resolvers/NetworkResolver.test.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-import { promises as fs } from 'fs'
-import { NetworkResolver } from '../../../src/resolvers/NetworkResolver'
-import { startServer } from '../../helpers/TestServer'
-
-test('can load data from a URL', async function () {
-  const server = await startServer((req, res) => {
-    res.end('here you go!')
-  })
-
-  try {
-    const resolver = new NetworkResolver()
-    const url = server.requestURL('/gimme')
-
-    expect(await resolver.canResolve(url.toString())).toBeTruthy()
-
-    const filename = await resolver.resolve(url.toString())
-
-    expect(await fs.readFile(filename, { encoding: 'utf8' })).toEqual(
-      'here you go!'
-    )
-  } finally {
-    await server.stop()
-  }
-})
-
-test('only resolves absolute HTTP URLs', async function () {
-  const resolver = new NetworkResolver()
-
-  expect(await resolver.canResolve('http://example.com/')).toBeTruthy()
-  expect(await resolver.canResolve('https://example.com/')).toBeTruthy()
-  expect(await resolver.canResolve('/')).toBeFalsy()
-  expect(
-    await resolver.canResolve('afp://192.168.0.1/volume/folder/file.js')
-  ).toBeFalsy()
-  expect(await resolver.canResolve('data:,Hello%2C%20World!')).toBeFalsy()
-})
-
-test('follows redirects', async function () {
-  const server = await startServer((req, res) => {
-    if (req.url === '/') {
-      res.writeHead(302, { Location: '/plugin' })
-      res.end()
-    } else if (req.url === '/plugin') {
-      res.end('redirected successfully!')
-    } else {
-      res.writeHead(404)
-      res.end()
-    }
-  })
-
-  try {
-    const resolver = new NetworkResolver()
-    const filename = await resolver.resolve(server.requestURL('/').toString())
-
-    expect(await fs.readFile(filename, { encoding: 'utf8' })).toEqual(
-      'redirected successfully!'
-    )
-  } finally {
-    await server.stop()
-  }
-})
-
-test('throws if it gets a non-200 response', async function () {
-  const server = await startServer((req, res) => {
-    res.statusCode = 400
-    res.end()
-  })
-
-  try {
-    const resolver = new NetworkResolver()
-    const url = server.requestURL('/')
-
-    await expect(resolver.resolve(url.toString())).rejects.toThrowError(
-      'failed to load plugin'
-    )
-  } finally {
-    await server.stop()
-  }
-})
diff --git a/packages/cli/bin/codemod b/packages/cli/bin/codemod
deleted file mode 100755
index b79bf35e..00000000
--- a/packages/cli/bin/codemod
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/usr/bin/env node
-
-if (process.env.CODEMOD_RUN_WITH_ESBUILD) {
-  require('esbuild-runner/register')
-}
-
-const run = (() => {
-  try {
-    return require('../src').default
-  } catch {
-    // ignore
-  }
-
-  try {
-    return require('../').default
-  } catch {
-    process.stderr.write(
-      'codemod does not seem to be built and the development files could not be loaded'
-    )
-    process.exit(1)
-  }
-})()
-
-run(process.argv)
-  .then((status) => {
-    process.exit(status)
-  })
-  .catch((err) => {
-    console.error(err.stack)
-    process.exit(-1)
-  })
diff --git a/packages/cli/bin/codemod-dev b/packages/cli/bin/codemod-dev
deleted file mode 100755
index 464e03ae..00000000
--- a/packages/cli/bin/codemod-dev
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/usr/bin/env bash
-
-SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
-
-export CODEMOD_RUN_WITH_ESBUILD=1
-exec "${SCRIPT_DIR}/codemod" $@
diff --git a/packages/cli/examples/convert-qunit-assert-expect-to-assert-async.ts b/packages/cli/examples/convert-qunit-assert-expect-to-assert-async.ts
deleted file mode 100644
index c26420e1..00000000
--- a/packages/cli/examples/convert-qunit-assert-expect-to-assert-async.ts
+++ /dev/null
@@ -1,117 +0,0 @@
-/**
- * Converts deprecated `assert.expect(N)`-style tests to use `assert.async()`.
- *
- * @example
- *
- * test('my test', function (assert) {
- *   assert.expect(1);
- *   window.server.get('/some/api', () => {
- *     asyncThing().then(() => {
- *       assert.ok(true, 'API called!');
- *     });
- *   });
- *   doStuff();
- * });
- *
- * // becomes
- *
- * test('my test', function (assert) {
- *   const done = assert.async();
- *   window.server.get('/some/api', () => {
- *     asyncThing().then(() => {
- *       assert.ok(true, 'API called!');
- *       done();
- *     });
- *   });
- *   doStuff();
- * });
- */
-
-import { defineCodemod, t } from '../src'
-
-export default defineCodemod(({ m, utils }) => {
-  // capture `assert` parameter
-  const assertBinding = m.capture(m.identifier())
-
-  // capture `assert.expect();` inside the async test
-  const assertExpect = m.capture(
-    m.expressionStatement(
-      m.callExpression(
-        m.memberExpression(
-          m.fromCapture(assertBinding),
-          m.identifier('expect')
-        ),
-        [m.numericLiteral()]
-      )
-    )
-  )
-
-  // capture `assert.(…);` inside the callback
-  const callbackAssertion = m.capture(
-    m.expressionStatement(
-      m.callExpression(
-        m.memberExpression(m.fromCapture(assertBinding), m.identifier())
-      )
-    )
-  )
-
-  // callback function body
-  const callbackFunctionBody = m.containerOf(
-    m.blockStatement(
-      m.anyList(m.zeroOrMore(), callbackAssertion, m.zeroOrMore())
-    )
-  )
-
-  // async test function body
-  const asyncTestFunctionBody = m.blockStatement(
-    m.anyList(
-      m.zeroOrMore(),
-      assertExpect,
-      m.zeroOrMore(),
-      m.expressionStatement(
-        m.callExpression(
-          undefined,
-          m.anyList(
-            m.zeroOrMore(),
-            m.or(
-              m.functionExpression(undefined, undefined, callbackFunctionBody),
-              m.arrowFunctionExpression(undefined, callbackFunctionBody)
-            )
-          )
-        )
-      ),
-      m.zeroOrMore()
-    )
-  )
-
-  // match the whole `test('description', function(assert) { … })`
-  const asyncTestMatcher = m.callExpression(m.identifier('test'), [
-    m.stringLiteral(),
-    m.functionExpression(undefined, [assertBinding], asyncTestFunctionBody),
-  ])
-
-  const makeDone = utils.statement<{ assert: t.Identifier }>(
-    'const done = %%assert%%.async();'
-  )
-  const callDone = utils.statement('done();')
-
-  return {
-    visitor: {
-      CallExpression(path) {
-        m.matchPath(
-          asyncTestMatcher,
-          {
-            assertExpect,
-            callbackAssertion,
-            assertBinding,
-          },
-          path,
-          ({ assertExpect, callbackAssertion, assertBinding }) => {
-            assertExpect.replaceWith(makeDone({ assert: assertBinding.node }))
-            callbackAssertion.insertAfter(callDone())
-          }
-        )
-      },
-    },
-  }
-})
diff --git a/packages/cli/examples/convert-static-class-to-named-exports.ts b/packages/cli/examples/convert-static-class-to-named-exports.ts
deleted file mode 100644
index 5b837762..00000000
--- a/packages/cli/examples/convert-static-class-to-named-exports.ts
+++ /dev/null
@@ -1,184 +0,0 @@
-/**
- * Converts default-exported class with all static methods to named exports.
- *
- * @example
- *
- * class MobileAppUpsellHelper {
- *   static getIosAppLink(specialTrackingLink) {
- *     const trackingLink = specialTrackingLink || 'IOS_BRANCH_LINK';
- *     return this.getBranchLink(trackingLink);
- *   }
- *
- *   static getAndroidAppLink(specialTrackingLink) {
- *     const trackingLink = specialTrackingLink || 'ANDROID_BRANCH_LINK';
- *     return this.getBranchLink(trackingLink);
- *   }
- *
- *   static getBranchLink(specialTrackingLink) {
- *     if (specialTrackingLink && APP_DOWNLOAD_ASSETS[specialTrackingLink]) {
- *       return APP_DOWNLOAD_ASSETS[specialTrackingLink];
- *     }
- *
- *     return APP_DOWNLOAD_ASSETS.DEFAULT_BRANCH_LINK;
- *   }
- *
- *   static getHideAppBanner() {
- *     return CookieHelper.get('hide_app_banner');
- *   }
- * }
- *
- * export default MobileAppUpsellHelper;
- *
- * // becomes
- *
- * export function getIosAppLink(specialTrackingLink) {
- *   const trackingLink = specialTrackingLink || 'IOS_BRANCH_LINK';
- *   return getBranchLink(trackingLink);
- * }
- *
- * export function getAndroidAppLink(specialTrackingLink) {
- *   const trackingLink = specialTrackingLink || 'ANDROID_BRANCH_LINK';
- *   return getBranchLink(trackingLink);
- * }
- *
- * export function getBranchLink(specialTrackingLink) {
- *   if (specialTrackingLink && APP_DOWNLOAD_ASSETS[specialTrackingLink]) {
- *     return APP_DOWNLOAD_ASSETS[specialTrackingLink];
- *   }
- *
- *   return APP_DOWNLOAD_ASSETS.DEFAULT_BRANCH_LINK;
- * }
- *
- * export function getHideAppBanner() {
- *   return CookieHelper.get('hide_app_banner');
- * }
- */
-
-import { defineCodemod, t } from '../src'
-
-export default defineCodemod(({ t, m }) => {
-  // capture the name of the exported class
-  const classId = m.capture(m.identifier())
-
-  // capture the class declaration
-  const classDeclaration = m.capture(
-    m.classDeclaration(
-      classId,
-      undefined,
-      m.classBody(
-        m.arrayOf(
-          m.classMethod(
-            'method',
-            m.identifier(),
-            m.arrayOf(
-              m.or(
-                m.identifier(),
-                m.assignmentPattern(),
-                m.objectPattern(),
-                m.arrayPattern(),
-                m.restElement()
-              )
-            ),
-            m.anything(),
-            false,
-            true
-          )
-        )
-      )
-    )
-  )
-
-  // capture the export, making sure to match the class name
-  const exportDeclaration = m.capture(
-    m.exportDefaultDeclaration(m.fromCapture(classId))
-  )
-
-  // match a program that contains a matching class and export declaration
-  const matcher = m.program(
-    m.anyList(
-      m.zeroOrMore(),
-      classDeclaration,
-      m.zeroOrMore(),
-      exportDeclaration,
-      m.zeroOrMore()
-    )
-  )
-
-  // match `this.*`, used internally
-  const thisPropertyAccessMatcher = m.memberExpression(
-    m.thisExpression(),
-    m.identifier(),
-    false
-  )
-
-  return {
-    visitor: {
-      Program(path) {
-        m.matchPath(
-          matcher,
-          { exportDeclaration, classDeclaration },
-          path,
-          ({ exportDeclaration, classDeclaration }) => {
-            const replacements: Array = []
-            const classBody = classDeclaration.get('body')
-
-            for (const property of classBody.get('body')) {
-              if (!property.isClassMethod()) {
-                throw new Error(
-                  `unexpected ${property.type} while looking for ClassMethod`
-                )
-              }
-
-              if (!t.isIdentifier(property.node.key)) {
-                throw new Error(
-                  `unexpected ${
-                    property.get('key').type
-                  } while looking for Identifier`
-                )
-              }
-
-              if (
-                property.node.params.some((p) => t.isTSParameterProperty(p))
-              ) {
-                continue
-              }
-
-              replacements.push(
-                t.exportNamedDeclaration(
-                  t.functionDeclaration(
-                    property.node.key,
-                    property.node.params as Array<
-                      t.Identifier | t.Pattern | t.RestElement
-                    >,
-                    property.node.body,
-                    property.node.generator,
-                    property.node.async
-                  ),
-                  []
-                )
-              )
-
-              property.get('body').traverse({
-                enter(path) {
-                  if (path.isFunction()) {
-                    if (!path.isArrowFunctionExpression()) {
-                      path.skip()
-                    }
-                  } else if (
-                    path.isMemberExpression() &&
-                    thisPropertyAccessMatcher.match(path.node)
-                  ) {
-                    path.replaceWith(path.node.property)
-                  }
-                },
-              })
-            }
-
-            exportDeclaration.remove()
-            classDeclaration.replaceWithMultiple(replacements)
-          }
-        )
-      },
-    },
-  }
-})
diff --git a/packages/cli/jest.config.js b/packages/cli/jest.config.js
deleted file mode 100644
index 8ba8a00a..00000000
--- a/packages/cli/jest.config.js
+++ /dev/null
@@ -1,10 +0,0 @@
-/* eslint-env node */
-
-/** @type {import('@jest/types').Config.InitialOptions} */
-module.exports = {
-  testEnvironment: 'node',
-  testRegex: '/__tests__/(test|.*\\.test)\\.ts$',
-  transform: {
-    '\\.ts$': 'esbuild-runner/jest',
-  },
-}
diff --git a/packages/cli/package.json b/packages/cli/package.json
deleted file mode 100644
index c3ba010f..00000000
--- a/packages/cli/package.json
+++ /dev/null
@@ -1,85 +0,0 @@
-{
-  "name": "@codemod/cli",
-  "version": "3.3.0",
-  "description": "codemod rewrites JavaScript and TypeScript",
-  "repository": "https://github.com/codemod-js/codemod.git",
-  "license": "Apache-2.0",
-  "author": "Brian Donovan",
-  "main": "build/index.js",
-  "types": "build/index.d.ts",
-  "bin": {
-    "codemod": "./bin/codemod"
-  },
-  "files": [
-    "bin",
-    "build"
-  ],
-  "scripts": {
-    "build": "tsc --build tsconfig.build.json",
-    "clean": "rm -rf build tsconfig.build.tsbuildinfo",
-    "lint": "eslint .",
-    "lint:fix": "eslint . --fix",
-    "prepublishOnly": "pnpm clean && pnpm build",
-    "test": "is-ci test:coverage test:watch",
-    "test:coverage": "jest --coverage",
-    "test:watch": "jest --watch"
-  },
-  "dependencies": {
-    "@babel/core": "^7.20.12",
-    "@babel/generator": "^7.20.14",
-    "@babel/parser": "^7.20.15",
-    "@babel/plugin-proposal-class-properties": "^7.18.6",
-    "@babel/preset-env": "^7.20.2",
-    "@babel/preset-typescript": "^7.18.6",
-    "@babel/traverse": "^7.20.13",
-    "@babel/types": "^7.20.7",
-    "@codemod/core": "^2.2.0",
-    "@codemod/matchers": "^1.6.0",
-    "@codemod/parser": "^1.4.0",
-    "@codemod/utils": "^1.1.0",
-    "core-js": "^3.1.4",
-    "cross-fetch": "^3.1.5",
-    "esbuild": "^0.13.13",
-    "esbuild-runner": "^2.2.1",
-    "find-up": "^5.0.0",
-    "get-stream": "^5.1.0",
-    "globby": "^11.0.0",
-    "ignore": "^5.1.9",
-    "is-ci-cli": "^2.2.0",
-    "pirates": "^4.0.0",
-    "recast": "^0.19.0",
-    "regenerator-runtime": "^0.13.3",
-    "resolve": "^1.22.1",
-    "source-map-support": "^0.5.6",
-    "tmp": "^0.2.1"
-  },
-  "devDependencies": {
-    "@types/babel__core": "^7.20.0",
-    "@types/babel__generator": "^7.6.4",
-    "@types/babel__traverse": "^7.18.3",
-    "@types/glob": "^7.1.0",
-    "@types/got": "^9.6.7",
-    "@types/jest": "^27.0.2",
-    "@types/node": "^18.14.0",
-    "@types/resolve": "^1.17.1",
-    "@types/rimraf": "3.0.0",
-    "@types/semver": "^7.1.0",
-    "@types/source-map-support": "^0.5.0",
-    "@types/tmp": "^0.2.0",
-    "commitlint": "^14.1.0",
-    "get-port": "^5.0.0",
-    "jest": "^27.3.1",
-    "make-dir": "^3.1.0",
-    "prettier": "^2.4.1",
-    "rimraf": "3.0.2",
-    "semver": "^7.3.5",
-    "tempy": "^1",
-    "typescript": "^4.9.5"
-  },
-  "engines": {
-    "node": ">=12.0.0"
-  },
-  "publishConfig": {
-    "access": "public"
-  }
-}
diff --git a/packages/cli/src/CLIEngine.ts b/packages/cli/src/CLIEngine.ts
deleted file mode 100644
index 15eb481c..00000000
--- a/packages/cli/src/CLIEngine.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-import { PluginItem } from '@babel/core'
-import { promises as fs } from 'fs'
-import { Config } from './Config'
-import { InlineTransformer } from './InlineTransformer'
-import { iterateSources } from './iterateSources'
-import {
-  TransformRunner,
-  Source,
-  SourceTransformResult,
-  SourceTransformResultKind,
-} from './TransformRunner'
-import getStream = require('get-stream')
-
-export class RunResult {
-  constructor(readonly stats: RunStats) {}
-}
-
-export class RunStats {
-  constructor(
-    readonly modified: number = 0,
-    readonly errors: number = 0,
-    readonly total: number = 0
-  ) {}
-}
-
-export class CLIEngine {
-  constructor(
-    readonly config: Config,
-    readonly onTransform: (result: SourceTransformResult) => void = () => {
-      // do nothing by default
-    }
-  ) {}
-
-  private async loadPlugins(): Promise> {
-    await this.config.loadBabelTranspile()
-    this.config.loadRequires()
-    return await this.config.getBabelPlugins()
-  }
-
-  async run(): Promise {
-    const plugins = await this.loadPlugins()
-    let modified = 0
-    let errors = 0
-    let total = 0
-    const dryRun = this.config.dry
-    let sources: AsyncGenerator
-
-    if (this.config.stdio) {
-      sources = (async function* getStdinSources(): AsyncGenerator {
-        yield new Source('', await getStream(process.stdin))
-      })()
-    } else {
-      sources = iterateSources(this.config.sourcePaths, {
-        extensions: this.config.extensions,
-      })
-    }
-
-    const runner = new TransformRunner(
-      sources,
-      new InlineTransformer(plugins, this.config.parserPlugins)
-    )
-
-    for await (const result of runner.run()) {
-      this.onTransform(result)
-
-      if (result.kind === SourceTransformResultKind.Transformed) {
-        if (this.config.stdio) {
-          process.stdout.write(result.output)
-        } else {
-          if (result.output !== result.source.content) {
-            modified++
-            if (!dryRun) {
-              await fs.writeFile(result.source.path, result.output, 'utf8')
-            }
-          }
-        }
-      } else if (result.error) {
-        errors++
-      }
-
-      total++
-    }
-
-    return new RunResult(new RunStats(modified, errors, total))
-  }
-}
diff --git a/packages/cli/src/Config.ts b/packages/cli/src/Config.ts
deleted file mode 100644
index 857c5376..00000000
--- a/packages/cli/src/Config.ts
+++ /dev/null
@@ -1,293 +0,0 @@
-import * as Babel from '@babel/core'
-import { ParserOptions, ParserPluginName } from '@codemod/parser'
-import { basename, extname } from 'path'
-import { TransformableExtensions } from './extensions'
-import { PluginLoader } from './PluginLoader'
-import { AstExplorerResolver } from './resolvers/AstExplorerResolver'
-import { FileSystemResolver } from './resolvers/FileSystemResolver'
-import { NetworkResolver } from './resolvers/NetworkResolver'
-import { PackageResolver } from './resolvers/PackageResolver'
-
-export class Plugin {
-  readonly declaredName?: string
-
-  constructor(
-    readonly rawPlugin: (babel: typeof Babel) => Babel.PluginObj,
-    readonly inferredName: string,
-    readonly source?: string,
-    readonly resolvedPath?: string
-  ) {
-    try {
-      const instance = rawPlugin(Babel)
-
-      if (instance.name) {
-        this.declaredName = instance.name
-      }
-    } catch {
-      // We won't be able to determine what the plugin names itself ¯\_(ツ)_/¯
-    }
-  }
-}
-
-export class Config {
-  constructor(
-    readonly sourcePaths: Array = [],
-    readonly localPlugins: Array = [],
-    readonly remotePlugins: Array = [],
-    readonly pluginOptions: Map = new Map(),
-    readonly parserPlugins = new Set(),
-    readonly extensions: Set = TransformableExtensions,
-    readonly sourceType: ParserOptions['sourceType'] = 'unambiguous',
-    readonly requires: Array = [],
-    readonly transpilePlugins: boolean = true,
-    readonly stdio: boolean = false,
-    readonly dry: boolean = false
-  ) {}
-
-  private pluginLoader = new PluginLoader([
-    new FileSystemResolver(),
-    new PackageResolver(),
-  ])
-
-  private remotePluginLoader = new PluginLoader([
-    new AstExplorerResolver(),
-    new NetworkResolver(),
-  ])
-
-  private _pluginCache?: Array
-
-  async getPlugins(): Promise> {
-    if (!this._pluginCache) {
-      const localPlugins = Promise.all(
-        this.localPlugins.map(async (localPlugin) => {
-          const pluginExports = await this.pluginLoader.load(localPlugin)
-          const defaultExport = pluginExports['default'] || pluginExports
-
-          return new Plugin(
-            defaultExport,
-            basename(localPlugin, extname(localPlugin)),
-            localPlugin
-          )
-        })
-      )
-
-      const remotePlugins = Promise.all(
-        this.remotePlugins.map(async (remotePlugin) => {
-          const pluginExports = await this.remotePluginLoader.load(remotePlugin)
-          const defaultExport = pluginExports['default'] || pluginExports
-
-          return new Plugin(
-            defaultExport,
-            basename(remotePlugin, extname(remotePlugin)),
-            remotePlugin
-          )
-        })
-      )
-
-      this._pluginCache = [...(await localPlugins), ...(await remotePlugins)]
-    }
-
-    return this._pluginCache
-  }
-
-  loadRequires(): void {
-    for (const modulePath of this.requires) {
-      require(modulePath)
-    }
-  }
-
-  async loadBabelTranspile(): Promise {
-    if (this.transpilePlugins && !require.extensions['.ts']) {
-      await import('esbuild-runner/register.js')
-    }
-  }
-
-  async getPlugin(name: string): Promise {
-    for (const plugin of await this.getPlugins()) {
-      if (
-        plugin.declaredName === name ||
-        plugin.inferredName === name ||
-        plugin.source === name
-      ) {
-        return plugin
-      }
-    }
-
-    return null
-  }
-
-  async getBabelPlugins(): Promise> {
-    const result: Array = []
-
-    for (const plugin of await this.getPlugins()) {
-      const options =
-        (plugin.declaredName && this.pluginOptions.get(plugin.declaredName)) ||
-        this.pluginOptions.get(plugin.inferredName) ||
-        (plugin.source && this.pluginOptions.get(plugin.source))
-
-      if (options) {
-        result.push([plugin.rawPlugin, options])
-      } else {
-        result.push(plugin.rawPlugin)
-      }
-    }
-
-    return result
-  }
-
-  async getBabelPlugin(name: string): Promise {
-    const plugin = await this.getPlugin(name)
-
-    if (!plugin) {
-      return null
-    }
-
-    const options = this.pluginOptions.get(name)
-
-    if (options) {
-      return [plugin.rawPlugin, options]
-    } else {
-      return plugin.rawPlugin
-    }
-  }
-}
-
-export class ConfigBuilder {
-  private _sourcePaths?: Array
-  private _localPlugins?: Array
-  private _remotePlugins?: Array
-  private _pluginOptions?: Map
-  private _parserPluginNames = new Set()
-  private _extensions: Set = new Set(TransformableExtensions)
-  private _sourceType: ParserOptions['sourceType'] = 'module'
-  private _requires?: Array
-  private _transpilePlugins?: boolean
-  private _stdio?: boolean
-  private _dry?: boolean
-
-  sourcePaths(value: Array): this {
-    this._sourcePaths = value
-    return this
-  }
-
-  addSourcePath(value: string): this {
-    if (!this._sourcePaths) {
-      this._sourcePaths = []
-    }
-    this._sourcePaths.push(value)
-    return this
-  }
-
-  addSourcePaths(...values: Array): this {
-    for (const value of values) {
-      this.addSourcePath(value)
-    }
-    return this
-  }
-
-  localPlugins(value: Array): this {
-    this._localPlugins = value
-    return this
-  }
-
-  addLocalPlugin(value: string): this {
-    if (!this._localPlugins) {
-      this._localPlugins = []
-    }
-    this._localPlugins.push(value)
-    return this
-  }
-
-  remotePlugins(value: Array): this {
-    this._remotePlugins = value
-    return this
-  }
-
-  addRemotePlugin(value: string): this {
-    if (!this._remotePlugins) {
-      this._remotePlugins = []
-    }
-    this._remotePlugins.push(value)
-    return this
-  }
-
-  pluginOptions(value: Map): this {
-    this._pluginOptions = value
-    return this
-  }
-
-  setOptionsForPlugin(options: object, plugin: string): this {
-    if (!this._pluginOptions) {
-      this._pluginOptions = new Map()
-    }
-    this._pluginOptions.set(plugin, options)
-    return this
-  }
-
-  addParserPlugin(name: ParserPluginName): this {
-    this._parserPluginNames.add(name)
-    return this
-  }
-
-  extensions(value: Set): this {
-    this._extensions = value
-    return this
-  }
-
-  addExtension(value: string): this {
-    if (!this._extensions) {
-      this._extensions = new Set()
-    }
-    this._extensions.add(value)
-    return this
-  }
-
-  sourceType(value: ParserOptions['sourceType']): this {
-    this._sourceType = value
-    return this
-  }
-
-  requires(value: Array): this {
-    this._requires = value
-    return this
-  }
-
-  addRequire(value: string): this {
-    if (!this._requires) {
-      this._requires = []
-    }
-    this._requires.push(value)
-    return this
-  }
-
-  transpilePlugins(value: boolean): this {
-    this._transpilePlugins = value
-    return this
-  }
-
-  stdio(value: boolean): this {
-    this._stdio = value
-    return this
-  }
-
-  dry(value: boolean): this {
-    this._dry = value
-    return this
-  }
-
-  build(): Config {
-    return new Config(
-      this._sourcePaths,
-      this._localPlugins,
-      this._remotePlugins,
-      this._pluginOptions,
-      this._parserPluginNames,
-      this._extensions,
-      this._sourceType,
-      this._requires,
-      this._transpilePlugins,
-      this._stdio,
-      this._dry
-    )
-  }
-}
diff --git a/packages/cli/src/InlineTransformer.ts b/packages/cli/src/InlineTransformer.ts
deleted file mode 100644
index 2bf100ed..00000000
--- a/packages/cli/src/InlineTransformer.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { PluginItem } from '@babel/core'
-import { ParserPlugin } from '@babel/parser'
-import { transform, TransformOptions } from '@codemod/core'
-import Transformer from './Transformer'
-
-export class InlineTransformer implements Transformer {
-  constructor(
-    private readonly plugins: Iterable,
-    private readonly parserPlugins: Iterable = []
-  ) {}
-
-  async transform(filepath: string, content: string): Promise {
-    const options: TransformOptions = {
-      filename: filepath,
-      babelrc: false,
-      configFile: false,
-      plugins: [...this.plugins],
-      parserOpts: {
-        plugins: [...this.parserPlugins],
-      },
-    }
-
-    const result = transform(content, options)
-
-    if (!result) {
-      throw new Error(`[${filepath}] babel transform returned null`)
-    }
-
-    return result.code as string
-  }
-}
diff --git a/packages/cli/src/Options.ts b/packages/cli/src/Options.ts
deleted file mode 100644
index b525a267..00000000
--- a/packages/cli/src/Options.ts
+++ /dev/null
@@ -1,203 +0,0 @@
-import { isParserPluginName } from '@codemod/parser'
-import { existsSync, readFileSync } from 'fs'
-import { resolve } from 'path'
-import { sync as resolveSync } from 'resolve'
-import { Config, ConfigBuilder } from './Config'
-import { RequireableExtensions } from './extensions'
-
-export interface RunCommand {
-  kind: 'run'
-  config: Config
-}
-
-export interface HelpCommand {
-  kind: 'help'
-}
-
-export interface VersionCommand {
-  kind: 'version'
-}
-
-export type Command = RunCommand | HelpCommand | VersionCommand
-
-export class Options {
-  constructor(readonly args: Array) {}
-
-  parse(): RunCommand | HelpCommand | VersionCommand {
-    const config = new ConfigBuilder()
-    let lastPlugin: string | undefined
-
-    for (let i = 0; i < this.args.length; i++) {
-      const arg = this.args[i]
-
-      switch (arg) {
-        case '-p':
-        case '--plugin':
-          i++
-          lastPlugin = this.args[i]
-          config.addLocalPlugin(lastPlugin)
-          break
-
-        case '--remote-plugin':
-          i++
-          lastPlugin = this.args[i]
-          config.addRemotePlugin(lastPlugin)
-          break
-
-        case '-o':
-        case '--plugin-options': {
-          i++
-
-          const value = this.args[i]
-          let name: string
-          let optionsRaw: string
-
-          if (value.startsWith('@')) {
-            if (!lastPlugin) {
-              throw new Error(
-                `${arg} must follow --plugin or --remote-plugin if no name is given`
-              )
-            }
-
-            optionsRaw = readFileSync(value.slice(1), 'utf8')
-            name = lastPlugin
-          } else if (/^\s*{/.test(value)) {
-            if (!lastPlugin) {
-              throw new Error(
-                `${arg} must follow --plugin or --remote-plugin if no name is given`
-              )
-            }
-
-            optionsRaw = value
-            name = lastPlugin
-          } else {
-            const nameAndOptions = value.split('=')
-            name = nameAndOptions[0]
-            optionsRaw = nameAndOptions[1]
-
-            if (optionsRaw.startsWith('@')) {
-              optionsRaw = readFileSync(optionsRaw.slice(1), 'utf8')
-            }
-          }
-
-          try {
-            config.setOptionsForPlugin(JSON.parse(optionsRaw), name)
-          } catch (err) {
-            throw new Error(
-              `unable to parse JSON config for ${name}: ${optionsRaw}`
-            )
-          }
-          break
-        }
-
-        case '--parser-plugins': {
-          i++
-          const value = this.args[i]
-          if (!value) {
-            throw new Error(`${arg} must be followed by a comma-separated list`)
-          }
-          for (const plugin of value.split(',')) {
-            if (isParserPluginName(plugin)) {
-              config.addParserPlugin(plugin)
-            } else {
-              throw new Error(`unknown parser plugin: ${plugin}`)
-            }
-          }
-          break
-        }
-
-        case '-r':
-        case '--require':
-          i++
-          config.addRequire(getRequirableModulePath(this.args[i]))
-          break
-
-        case '--transpile-plugins':
-        case '--no-transpile-plugins':
-          config.transpilePlugins(arg === '--transpile-plugins')
-          break
-
-        case '--extensions':
-          i++
-          config.extensions(
-            new Set(
-              this.args[i]
-                .split(',')
-                .map((ext) => (ext[0] === '.' ? ext : `.${ext}`))
-            )
-          )
-          break
-
-        case '--add-extension':
-          i++
-          config.addExtension(this.args[i])
-          break
-
-        case '--source-type': {
-          i++
-          const sourceType = this.args[i]
-          if (
-            sourceType === 'module' ||
-            sourceType === 'script' ||
-            sourceType === 'unambiguous'
-          ) {
-            config.sourceType(sourceType)
-          } else {
-            throw new Error(
-              `expected '--source-type' to be one of "module", "script", ` +
-                `or "unambiguous" but got: "${sourceType}"`
-            )
-          }
-          break
-        }
-
-        case '-s':
-        case '--stdio':
-          config.stdio(true)
-          break
-
-        case '-h':
-        case '--help':
-          return { kind: 'help' }
-
-        case '--version':
-          return { kind: 'version' }
-
-        case '-d':
-        case '--dry':
-          config.dry(true)
-          break
-
-        default:
-          if (arg[0] === '-') {
-            throw new Error(`unexpected option: ${arg}`)
-          } else {
-            config.addSourcePath(arg)
-          }
-          break
-      }
-    }
-
-    return {
-      kind: 'run',
-      config: config.build(),
-    }
-  }
-}
-
-/**
- * Gets a path that can be passed to `require` for a given module path.
- */
-function getRequirableModulePath(modulePath: string): string {
-  if (existsSync(modulePath)) {
-    return resolve(modulePath)
-  }
-
-  for (const ext of RequireableExtensions) {
-    if (existsSync(modulePath + ext)) {
-      return resolve(modulePath + ext)
-    }
-  }
-
-  return resolveSync(modulePath, { basedir: process.cwd() })
-}
diff --git a/packages/cli/src/PluginLoader.ts b/packages/cli/src/PluginLoader.ts
deleted file mode 100644
index bd8b3c4c..00000000
--- a/packages/cli/src/PluginLoader.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import Resolver from './resolvers/Resolver'
-
-export class PluginLoader {
-  constructor(private readonly resolvers: Array) {}
-
-  async load(source: string): Promise {
-    for (const resolver of this.resolvers) {
-      if (await resolver.canResolve(source)) {
-        const resolvedPath = await resolver.resolve(source)
-        return require(resolvedPath)
-      }
-    }
-
-    throw new Error(`unable to resolve a plugin from source: ${source}`)
-  }
-}
diff --git a/packages/cli/src/TransformRunner.ts b/packages/cli/src/TransformRunner.ts
deleted file mode 100644
index d738aaf8..00000000
--- a/packages/cli/src/TransformRunner.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import Transformer from './Transformer'
-
-export class Source {
-  constructor(readonly path: string, readonly content: string) {}
-}
-
-export enum SourceTransformResultKind {
-  Transformed = 'Transformed',
-  Error = 'Error',
-}
-
-export type SourceTransformResult =
-  | {
-      kind: SourceTransformResultKind.Transformed
-      source: Source
-      output: string
-    }
-  | { kind: SourceTransformResultKind.Error; source: Source; error: Error }
-
-export class TransformRunner {
-  constructor(
-    readonly sources: AsyncGenerator,
-    readonly transformer: Transformer
-  ) {}
-
-  async *run(): AsyncIterableIterator {
-    for await (const source of this.sources) {
-      let result: SourceTransformResult
-
-      try {
-        const output = await this.transformer.transform(
-          source.path,
-          source.content
-        )
-        result = {
-          kind: SourceTransformResultKind.Transformed,
-          source,
-          output,
-        }
-      } catch (error) {
-        result = { kind: SourceTransformResultKind.Error, source, error }
-      }
-
-      yield result
-    }
-  }
-}
diff --git a/packages/cli/src/Transformer.ts b/packages/cli/src/Transformer.ts
deleted file mode 100644
index 95744ba1..00000000
--- a/packages/cli/src/Transformer.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export default interface Transformer {
-  transform(filepath: string, content: string): Promise
-}
diff --git a/packages/cli/src/defineCodemod.ts b/packages/cli/src/defineCodemod.ts
deleted file mode 100644
index e6d1810f..00000000
--- a/packages/cli/src/defineCodemod.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import * as utils from '@codemod/utils'
-import * as matchers from '@codemod/matchers'
-
-/**
- * Defines a codemod function that can be used with `@codemod/cli`,
- * `@codemod/core`, or `@babel/core`. Provides easy access to the
- * `@codemod/matchers` and `@codemod/utils` APIs.
- */
-export function defineCodemod(
-  fn: (
-    helpers: {
-      utils: typeof utils
-      matchers: typeof matchers
-      types: typeof utils.types
-      m: typeof matchers
-      t: typeof utils.types
-    },
-    options?: T
-  ) => utils.Babel.PluginItem
-) {
-  return function (_?: unknown, options?: T) {
-    return fn(
-      { utils, matchers, types: utils.types, m: matchers, t: utils.types },
-      options
-    )
-  }
-}
diff --git a/packages/cli/src/extensions.ts b/packages/cli/src/extensions.ts
deleted file mode 100644
index c3927aba..00000000
--- a/packages/cli/src/extensions.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-function union(...sets: Array>): Set {
-  return new Set(sets.reduce((result, set) => [...result, ...set], []))
-}
-
-export const TypeScriptExtensions = new Set([
-  '.ts',
-  '.tsx',
-  '.mts',
-  '.mtsx',
-  '.cts',
-  '.ctsx',
-])
-export const JavaScriptExtensions = new Set([
-  '.js',
-  '.jsx',
-  '.mjs',
-  '.mjsx',
-  '.cjs',
-  '.cjsx',
-  '.es',
-  '.es6',
-])
-export const PluginExtensions = union(
-  TypeScriptExtensions,
-  JavaScriptExtensions
-)
-export const RequireableExtensions = union(
-  TypeScriptExtensions,
-  JavaScriptExtensions
-)
-export const TransformableExtensions = union(
-  TypeScriptExtensions,
-  JavaScriptExtensions
-)
diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts
deleted file mode 100644
index 8c781976..00000000
--- a/packages/cli/src/index.ts
+++ /dev/null
@@ -1,175 +0,0 @@
-import { basename, relative } from 'path'
-import { CLIEngine } from './CLIEngine'
-import { Config } from './Config'
-import { Options, Command } from './Options'
-import {
-  SourceTransformResult,
-  SourceTransformResultKind,
-} from './TransformRunner'
-import * as matchers from '@codemod/matchers'
-
-export * from './defineCodemod'
-export { t, types } from '@codemod/utils'
-export { matchers, matchers as m }
-
-function optionAnnotation(
-  value: boolean | Array | Map | string
-): string {
-  if (Array.isArray(value) || value instanceof Map) {
-    return ' (allows multiple)'
-  } else if (typeof value === 'boolean') {
-    return ` (default: ${value ? 'on' : 'off'})`
-  } else if (typeof value === 'string') {
-    return ` (default: ${value})`
-  } else {
-    return ''
-  }
-}
-
-function printHelp(argv: Array, out: NodeJS.WritableStream): void {
-  const $0 = basename(argv[1])
-  const defaults = new Config()
-
-  out.write(
-    `
-${$0} [OPTIONS] [PATH … | --stdio]
-
-OPTIONS
-  -p, --plugin PLUGIN               Transform sources with PLUGIN${optionAnnotation(
-    defaults.localPlugins
-  )}.
-      --remote-plugin URL           Fetch a plugin from URL${optionAnnotation(
-        defaults.remotePlugins
-      )}.
-  -o, --plugin-options OPTS         JSON-encoded OPTS for the last plugin provided${optionAnnotation(
-    defaults.pluginOptions
-  )}.
-      --parser-plugins PLUGINS      Comma-separated PLUGINS to use with @babel/parser.
-  -r, --require PATH                Require PATH before transform${optionAnnotation(
-    defaults.requires
-  )}.
-      --add-extension EXT           Add an extension to the list of supported extensions.
-      --extensions EXTS             Comma-separated extensions to process (default: "${Array.from(
-        defaults.extensions
-      ).join(',')}").
-      --source-type                 Parse as "module", "script", or "unambiguous" (meaning babel
-                                    will try to guess, default: "${
-                                      defaults.sourceType
-                                    }").
-      --[no-]transpile-plugins      Transpile plugins to enable future syntax${optionAnnotation(
-        defaults.transpilePlugins
-      )}.
-  -s, --stdio                       Read source from stdin and print to stdout${optionAnnotation(
-    defaults.stdio
-  )}.
-  -d, --dry                         Run plugins without modifying files on disk${optionAnnotation(
-    defaults.dry
-  )}.
-      --version                     Print the version of ${$0}.
-  -h, --help                        Show this help message.
-
-  NOTE: \`--remote-plugin\` should only be used as a convenience to load code that you or someone
-        you trust wrote. It will run with your full user privileges, so please exercise caution!
-
-EXAMPLES
-  # Run with a relative plugin on all files in \`src/\`.
-  $ ${$0} -p ./typecheck.js src/
-
-  # Run with a remote plugin from astexplorer.net on all files in \`src/\`.
-  $ ${$0} --remote-plugin 'https://astexplorer.net/#/gist/688274…' src/
-
-  # Run with multiple plugins.
-  $ ${$0} -p ./a.js -p ./b.js some-file.js
-
-  # Transform TypeScript sources.
-  # ${$0} -p ./a.js my-typescript-file.ts a-component.tsx
-
-  # Run with a plugin in \`node_modules\` on stdin.
-  $ ${$0} -s -p babel-plugin-typecheck <, out: NodeJS.WritableStream): void {
-  // eslint-disable-next-line @typescript-eslint/no-var-requires
-  out.write(require('../package.json').version)
-  out.write('\n')
-}
-
-export async function run(argv: Array): Promise {
-  let command: Command
-
-  try {
-    command = new Options(argv.slice(2)).parse()
-  } catch (error) {
-    process.stderr.write(`ERROR: ${error.message}\n`)
-    printHelp(argv, process.stderr)
-    return 1
-  }
-
-  if (command.kind === 'help') {
-    printHelp(argv, process.stdout)
-    return 0
-  }
-
-  if (command.kind === 'version') {
-    printVersion(argv, process.stdout)
-    return 0
-  }
-
-  const config = command.config
-  const dim = process.stdout.isTTY ? '\x1b[2m' : ''
-  const reset = process.stdout.isTTY ? '\x1b[0m' : ''
-
-  function onTransform(result: SourceTransformResult): void {
-    const relativePath = relative(process.cwd(), result.source.path)
-    if (result.kind === SourceTransformResultKind.Transformed) {
-      if (!config.stdio) {
-        if (result.output === result.source.content) {
-          process.stdout.write(`${dim}${relativePath}${reset}\n`)
-        } else {
-          process.stdout.write(`${relativePath}\n`)
-        }
-      }
-    } else if (result.error) {
-      if (!config.stdio) {
-        process.stderr.write(
-          `Encountered an error while processing ${relativePath}:\n`
-        )
-      }
-
-      process.stderr.write(`${result.error.stack}\n`)
-    }
-  }
-
-  const { stats } = await new CLIEngine(config, onTransform).run()
-
-  if (!config.stdio) {
-    if (config.dry) {
-      process.stdout.write('DRY RUN: no files affected\n')
-    }
-
-    process.stdout.write(
-      `${stats.total} file(s), ${stats.modified} modified, ${stats.errors} errors\n`
-    )
-  }
-
-  // exit status is number of errors up to byte max value
-  return Math.min(stats.errors, 255)
-}
-
-export default run
diff --git a/packages/cli/src/iterateSources.ts b/packages/cli/src/iterateSources.ts
deleted file mode 100644
index c955231a..00000000
--- a/packages/cli/src/iterateSources.ts
+++ /dev/null
@@ -1,201 +0,0 @@
-import { strict as assert } from 'assert'
-import { promises as fs } from 'fs'
-import ignore, { Ignore } from 'ignore'
-import { basename, dirname, extname, isAbsolute, join, relative } from 'path'
-import { Source } from './TransformRunner'
-import globby = require('globby')
-import findUp = require('find-up')
-
-export interface Options {
-  extensions?: Set
-  cwd?: string
-}
-
-const DOTGIT = '.git'
-const GITIGNORE = '.gitignore'
-
-function pathWithin(container: string, contained: string): string | undefined {
-  const pathInContainer = relative(container, contained)
-  return pathInContainer.startsWith('../') ? undefined : pathInContainer
-}
-
-function pathContains(container: string, contained: string): boolean {
-  return typeof pathWithin(container, contained) === 'string'
-}
-
-async function readIgnoreFile(path: string): Promise {
-  const ig = ignore()
-  ig.add(await fs.readFile(path, { encoding: 'utf-8' }))
-  return ig
-}
-
-async function findGitroot(from: string): Promise {
-  const dotgit = await findUp(DOTGIT, { cwd: from, type: 'directory' })
-  return dotgit && dirname(dotgit)
-}
-
-class FileFilter {
-  private readonly cwd: string
-  private readonly extensions?: Set
-  private readonly ignoresByGitignoreDirectory = new Map()
-
-  constructor({ cwd, extensions }: { cwd: string; extensions?: Set }) {
-    this.cwd = cwd
-    this.extensions = extensions
-  }
-
-  static async build({
-    cwd,
-    extensions,
-  }: {
-    cwd: string
-    extensions?: Set
-  }): Promise {
-    return new FileFilter({
-      cwd,
-      extensions,
-    }).addGitignoreFilesTraversingUpToGitRoot()
-  }
-
-  async addGitignoreFilesTraversingUpToGitRoot(
-    start = this.cwd
-  ): Promise {
-    const gitroot = await findGitroot(start)
-
-    if (gitroot) {
-      let search = start
-
-      while (pathContains(gitroot, search)) {
-        const gitignorePath = await findUp(GITIGNORE, {
-          cwd: search,
-          type: 'file',
-        })
-
-        if (!gitignorePath) {
-          break
-        }
-
-        await this.addGitignoreFile(gitignorePath)
-        search = dirname(dirname(gitignorePath))
-      }
-    }
-
-    return this
-  }
-
-  async addGitignoreFile(gitignorePath: string): Promise {
-    if (!this.ignoresByGitignoreDirectory.has(dirname(gitignorePath))) {
-      this.ignoresByGitignoreDirectory.set(
-        dirname(gitignorePath),
-        await readIgnoreFile(gitignorePath)
-      )
-    }
-  }
-
-  test(path: string, { isFile }: { isFile: boolean }): boolean {
-    const base = basename(path)
-
-    if (base === DOTGIT || base === GITIGNORE) {
-      return true
-    }
-
-    if (isFile && this.extensions && !this.extensions.has(extname(path))) {
-      return true
-    }
-
-    for (const [directory, ig] of this.ignoresByGitignoreDirectory) {
-      const pathInDirectory = pathWithin(directory, path)
-      if (!pathInDirectory) {
-        continue
-      }
-
-      if (ig.ignores(pathInDirectory)) {
-        return true
-      }
-    }
-
-    return false
-  }
-}
-
-async function* iterateDirectory(
-  root: string,
-  { extensions }: Options = {}
-): AsyncGenerator {
-  assert(isAbsolute(root), `expected absolute path: ${root}`)
-
-  const filter = await FileFilter.build({ cwd: root, extensions })
-  const queue: Array = [root]
-
-  while (queue.length > 0) {
-    const directory = queue.shift() as string
-    const entries = await fs.readdir(directory, { withFileTypes: true })
-
-    for (const entry of entries) {
-      if (entry.isFile() && entry.name === GITIGNORE) {
-        const path = join(directory, entry.name)
-        await filter.addGitignoreFile(path)
-      }
-    }
-
-    for (const entry of entries) {
-      const path = join(directory, entry.name)
-
-      if (filter.test(path, { isFile: entry.isFile() })) {
-        continue
-      }
-
-      if (entry.isFile()) {
-        const content = await fs.readFile(path, { encoding: 'utf-8' })
-        yield { path, content }
-      } else if (entry.isDirectory()) {
-        queue.push(path)
-      }
-    }
-  }
-}
-
-async function* iterateFiles(
-  paths: Array,
-  { extensions, cwd }: { extensions?: Set; cwd: string }
-): AsyncGenerator {
-  const filter = await FileFilter.build({ cwd, extensions })
-
-  for (const path of paths) {
-    await filter.addGitignoreFilesTraversingUpToGitRoot(dirname(path))
-    if (!filter.test(path, { isFile: true })) {
-      const content = await fs.readFile(path, { encoding: 'utf-8' })
-      yield { path, content }
-    }
-  }
-}
-
-/**
- * Builds an iterator that loops through all the files in the given paths,
- * matching an allowlist of extensions. Ignores files excluded by git.
- */
-export async function* iterateSources(
-  roots: Array,
-  {
-    extensions,
-    cwd = process.cwd(),
-  }: { extensions?: Set; cwd?: string } = {}
-): AsyncGenerator {
-  assert(isAbsolute(cwd), `expected absolute path: ${cwd}`)
-
-  for (const root of roots) {
-    if (globby.hasMagic(root)) {
-      const matches = await globby(isAbsolute(root) ? root : join(cwd, root), {
-        cwd,
-      })
-      yield* iterateFiles(matches, { cwd, extensions })
-    } else if ((await fs.lstat(root)).isDirectory()) {
-      yield* iterateDirectory(isAbsolute(root) ? root : join(cwd, root), {
-        cwd,
-        extensions,
-      })
-    } else {
-      yield* iterateFiles([root], { cwd })
-    }
-  }
-}
diff --git a/packages/cli/src/resolvers/AstExplorerResolver.ts b/packages/cli/src/resolvers/AstExplorerResolver.ts
deleted file mode 100644
index 7c1e65dc..00000000
--- a/packages/cli/src/resolvers/AstExplorerResolver.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import { promises as fs } from 'fs'
-import { URL } from 'url'
-import { NetworkResolver } from './NetworkResolver'
-
-const EDITOR_HASH_PATTERN = /^#\/gist\/(\w+)(?:\/(\w+))?$/
-
-/**
- * Resolves plugins from AST Explorer transforms.
- *
- * astexplorer.net uses GitHub gists to save and facilitate sharing. This
- * resolver accepts either the editor URL or the gist API URL.
- */
-export class AstExplorerResolver extends NetworkResolver {
-  constructor(
-    private readonly baseURL: URL = new URL('https://astexplorer.net/')
-  ) {
-    super()
-  }
-
-  async canResolve(source: string): Promise {
-    if (await super.canResolve(source)) {
-      const url = new URL(await this.normalize(source))
-      const canResolve =
-        this.matchesHost(url) &&
-        /^\/api\/v1\/gist\/[a-f0-9]+(\/(?:[a-f0-9]+|latest))?$/.test(
-          url.pathname
-        )
-      return canResolve
-    }
-
-    return false
-  }
-
-  async resolve(source: string): Promise {
-    const filename = await super.resolve(await this.normalize(source))
-    const text = await fs.readFile(filename, { encoding: 'utf8' })
-    let data
-
-    try {
-      data = JSON.parse(text)
-    } catch {
-      throw new Error(
-        `data loaded from ${source} is not JSON: ${text.slice(0, 100)}`
-      )
-    }
-
-    if (
-      !data ||
-      !data.files ||
-      !data.files['transform.js'] ||
-      !data.files['transform.js'].content
-    ) {
-      throw new Error(
-        "'transform.js' could not be found, perhaps transform is disabled"
-      )
-    }
-
-    await fs.writeFile(filename, data.files['transform.js'].content, {
-      encoding: 'utf8',
-    })
-
-    return filename
-  }
-
-  async normalize(source: string): Promise {
-    const url = new URL(source)
-
-    if (!this.matchesHost(url)) {
-      return source
-    }
-
-    const match = url.hash.match(EDITOR_HASH_PATTERN)
-
-    if (!match) {
-      return source
-    }
-
-    let path = `/api/v1/gist/${match[1]}`
-
-    if (match[2]) {
-      path += `/${match[2]}`
-    }
-
-    return new URL(path, this.baseURL).toString()
-  }
-
-  private matchesHost(url: URL): boolean {
-    if (url.host !== this.baseURL.host) {
-      return false
-    }
-
-    // use SSL even if the URL doesn't use it
-    return url.protocol === this.baseURL.protocol || url.protocol === 'http:'
-  }
-}
diff --git a/packages/cli/src/resolvers/FileSystemResolver.ts b/packages/cli/src/resolvers/FileSystemResolver.ts
deleted file mode 100644
index 59f0c211..00000000
--- a/packages/cli/src/resolvers/FileSystemResolver.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import { promises as fs } from 'fs'
-import { resolve } from 'path'
-import { PluginExtensions } from '../extensions'
-import Resolver from './Resolver'
-
-async function isFile(path: string): Promise {
-  try {
-    return (await fs.stat(path)).isFile()
-  } catch {
-    return false
-  }
-}
-
-/**
- * Resolves file system paths to plugins.
- */
-export class FileSystemResolver implements Resolver {
-  constructor(
-    private readonly optionalExtensions: Set = PluginExtensions
-  ) {}
-
-  private *enumerateCandidateSources(source: string): IterableIterator {
-    yield resolve(source)
-
-    for (const ext of this.optionalExtensions) {
-      if (ext[0] !== '.') {
-        yield resolve(`${source}.${ext}`)
-      } else {
-        yield resolve(source + ext)
-      }
-    }
-  }
-
-  async canResolve(source: string): Promise {
-    for (const candidate of this.enumerateCandidateSources(source)) {
-      if (await isFile(candidate)) {
-        return true
-      }
-    }
-
-    return false
-  }
-
-  async resolve(source: string): Promise {
-    for (const candidate of this.enumerateCandidateSources(source)) {
-      if (await isFile(candidate)) {
-        return candidate
-      }
-    }
-
-    throw new Error(`unable to resolve file from source: ${source}`)
-  }
-}
diff --git a/packages/cli/src/resolvers/NetworkResolver.ts b/packages/cli/src/resolvers/NetworkResolver.ts
deleted file mode 100644
index 9894daa4..00000000
--- a/packages/cli/src/resolvers/NetworkResolver.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import { promises as fs } from 'fs'
-import { fetch } from 'cross-fetch'
-import { tmpNameSync as tmp } from 'tmp'
-import { URL } from 'url'
-import Resolver from './Resolver'
-
-export class NetworkLoadError extends Error {
-  constructor(readonly response: Response) {
-    super(`failed to load plugin from '${response.url}'`)
-  }
-}
-
-/**
- * Resolves plugins over the network to a local file.
- *
- * This plugin accepts only absolute HTTP URLs.
- */
-export class NetworkResolver implements Resolver {
-  async canResolve(source: string): Promise {
-    try {
-      const url = new URL(source)
-      return url.protocol === 'http:' || url.protocol === 'https:'
-    } catch {
-      return false
-    }
-  }
-
-  async resolve(source: string): Promise {
-    const response = await fetch(source, { redirect: 'follow' })
-
-    if (response.status !== 200) {
-      throw new NetworkLoadError(response)
-    }
-
-    const filename = tmp({ postfix: '.js' })
-    await fs.writeFile(filename, await response.text())
-    return filename
-  }
-}
diff --git a/packages/cli/src/resolvers/PackageResolver.ts b/packages/cli/src/resolvers/PackageResolver.ts
deleted file mode 100644
index 434d1040..00000000
--- a/packages/cli/src/resolvers/PackageResolver.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { sync as resolveSync } from 'resolve'
-import Resolver from './Resolver'
-
-/**
- * Resolves node modules by name relative to the working directory.
- *
- * For example, if used in a project that includes `myplugin` in its
- * `node_modules`, this class will resolve to the main file of `myplugin`.
- */
-export class PackageResolver implements Resolver {
-  async canResolve(source: string): Promise {
-    try {
-      await this.resolve(source)
-      return true
-    } catch {
-      return false
-    }
-  }
-
-  async resolve(source: string): Promise {
-    return resolveSync(source, { basedir: process.cwd() })
-  }
-}
diff --git a/packages/cli/src/resolvers/Resolver.ts b/packages/cli/src/resolvers/Resolver.ts
deleted file mode 100644
index 0fa4f49a..00000000
--- a/packages/cli/src/resolvers/Resolver.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * Resolvers locate plugin paths given a source string.
- */
-export default interface Resolver {
-  /**
-   * Determines whether the source can be resolved by this resolver. This should
-   * not necessarily determine whether the source actually does resolve to
-   * anything, but rather whether it is of the right form to be resolved.
-   *
-   * For example, if a resolver were written to load a plugin from a `data:` URI
-   * then this method might simply check that the URI is a valid `data:` URI
-   * rather than actually decoding and handling the contents of said URI.
-   */
-  canResolve(source: string): Promise
-
-  /**
-   * Determines a file path that, when loaded as JavaScript, exports a babel
-   * plugin suitable for use in the transform pipeline. If `source` does not
-   * actually resolve to a file already on disk, consider writing the contents
-   * to disk in a temporary location.
-   */
-  resolve(source: string): Promise
-}
diff --git a/packages/cli/tsconfig.build.json b/packages/cli/tsconfig.build.json
deleted file mode 100644
index e4ab1471..00000000
--- a/packages/cli/tsconfig.build.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
-  "extends": "./tsconfig.json",
-  "include": ["src"],
-  "exclude": ["src/__tests__"],
-  "compilerOptions": {
-    "noEmit": false,
-    "sourceMap": true,
-    "declaration": true,
-    "rootDir": "src",
-    "outDir": "build"
-  },
-  "references": [
-    { "path": "../core/tsconfig.build.json" },
-    { "path": "../matchers/tsconfig.build.json" },
-    { "path": "../parser/tsconfig.build.json" },
-    { "path": "../utils/tsconfig.build.json" }
-  ]
-}
diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json
deleted file mode 100644
index abc844f7..00000000
--- a/packages/cli/tsconfig.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
-  "compilerOptions": {
-    "target": "es2015",
-    "module": "commonjs",
-    "lib": ["es2015", "es2016"],
-    "composite": true,
-    "noEmit": true,
-    "noImplicitAny": false,
-    "strictNullChecks": true
-  },
-  "exclude": ["__tests__/fixtures/**/*.ts", "tmp", "build"],
-  "references": [
-    { "path": "../core/tsconfig.build.json" },
-    { "path": "../matchers/tsconfig.build.json" },
-    { "path": "../parser/tsconfig.build.json" }
-  ]
-}
diff --git a/packages/core/.eslintignore b/packages/core/.eslintignore
deleted file mode 100644
index 5498e0f4..00000000
--- a/packages/core/.eslintignore
+++ /dev/null
@@ -1,2 +0,0 @@
-build
-coverage
diff --git a/packages/core/README.md b/packages/core/README.md
index 63445ed9..cae3321f 100644
--- a/packages/core/README.md
+++ b/packages/core/README.md
@@ -13,7 +13,7 @@ $ npm install @codemod/core
 ## Usage
 
 ```ts
-import { transform } from '@codemod/core'
+import { transform } from '@codemod-esm/core'
 
 const result = transform('a ?? b', {
   plugins: ['@babel/plugin-proposal-nullish-coalescing-operator'],
diff --git a/packages/core/jest.config.js b/packages/core/jest.config.js
deleted file mode 100644
index 8ba8a00a..00000000
--- a/packages/core/jest.config.js
+++ /dev/null
@@ -1,10 +0,0 @@
-/* eslint-env node */
-
-/** @type {import('@jest/types').Config.InitialOptions} */
-module.exports = {
-  testEnvironment: 'node',
-  testRegex: '/__tests__/(test|.*\\.test)\\.ts$',
-  transform: {
-    '\\.ts$': 'esbuild-runner/jest',
-  },
-}
diff --git a/packages/core/package.json b/packages/core/package.json
index 75d9ec0f..d6f472d2 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,42 +1,39 @@
 {
-  "name": "@codemod/core",
-  "version": "2.2.0",
+  "name": "@codemod-esm/core",
+  "type": "module",
+  "version": "2.2.2",
   "description": "Runs babel plugins for codemods, i.e. by preserving formatting using Recast.",
-  "repository": "https://github.com/codemod-js/codemod",
-  "license": "Apache-2.0",
   "author": "Brian Donovan",
-  "main": "build/index.js",
-  "types": "build/index.d.ts",
+  "license": "Apache-2.0",
+  "repository": "https://github.com/codemod-js/codemod",
+  "exports": {
+    "default": "./dist/index.js"
+  },
+  "main": "dist/index.js",
+  "types": "dist/index.d.ts",
   "files": [
-    "build"
+    "dist"
   ],
   "scripts": {
     "build": "tsc --build tsconfig.build.json",
-    "clean": "rm -rf build tsconfig.build.tsbuildinfo",
+    "clean": "rimraf dist tsconfig.build.tsbuildinfo",
     "lint": "eslint .",
     "lint:fix": "eslint . --fix",
     "prepublishOnly": "pnpm clean && pnpm build",
-    "test": "is-ci test:coverage test:watch",
-    "test:coverage": "jest --coverage",
-    "test:watch": "jest --watch"
+    "test": "bun test"
   },
   "dependencies": {
-    "@babel/core": "^7.20.12",
-    "@babel/generator": "^7.20.14",
-    "@codemod/parser": "^1.4.0",
-    "is-ci-cli": "^2.2.0",
-    "recast": "^0.19.0",
-    "resolve": "^1.22.1"
+    "@babel/core": "^7.26.9",
+    "@babel/generator": "^7.26.9",
+    "@codemod-esm/parser": "workspace:^",
+    "recast": "^0.23.11"
   },
   "devDependencies": {
-    "@babel/types": "^7.20.7",
-    "@types/babel__core": "^7.1.16",
-    "@types/jest": "^25.1.0",
-    "@types/node": "^18.14.0",
-    "@types/prettier": "^2.0.0",
-    "@types/resolve": "^1.14.0",
-    "jest": "^27.3.1",
-    "typescript": "^4.9.5"
+    "@babel/types": "^7.26.9",
+    "@types/babel__core": "^7.20.5",
+    "@types/bun": "^1.2.4",
+    "@types/node": "^22.13.9",
+    "typescript": "^5.8.2"
   },
   "publishConfig": {
     "access": "public"
diff --git a/packages/core/src/AllSyntaxPlugin.ts b/packages/core/src/AllSyntaxPlugin.ts
index 3977d032..64b0f944 100644
--- a/packages/core/src/AllSyntaxPlugin.ts
+++ b/packages/core/src/AllSyntaxPlugin.ts
@@ -1,15 +1,16 @@
-import { buildOptions, ParserOptions } from '@codemod/parser'
-import { TransformOptions } from '.'
-import { BabelPlugin, PluginObj } from './BabelPluginTypes'
+import type { ParserOptions } from '@codemod-esm/parser'
+import type { TransformOptions } from '.'
+import type { BabelPlugin, PluginObj } from './BabelPluginTypes'
+import { buildOptions } from '@codemod-esm/parser'
 
 export function buildPlugin(
-  sourceType: ParserOptions['sourceType']
+  sourceType: ParserOptions['sourceType'],
 ): BabelPlugin {
   return function (): PluginObj {
     return {
       manipulateOptions(
-        opts: TransformOptions,
-        parserOpts: ParserOptions
+        _opts: TransformOptions,
+        parserOpts: ParserOptions,
       ): void {
         const options = buildOptions({
           ...parserOpts,
@@ -17,10 +18,7 @@ export function buildPlugin(
           plugins: parserOpts.plugins,
         })
 
-        for (const key of Object.keys(options)) {
-          // eslint-disable-next-line @typescript-eslint/no-explicit-any
-          ;(parserOpts as any)[key] = (options as any)[key]
-        }
+        Object.assign(parserOpts, options)
       },
     }
   }
diff --git a/packages/core/src/BabelPluginTypes.ts b/packages/core/src/BabelPluginTypes.ts
index 77f0be2a..a318dfc0 100644
--- a/packages/core/src/BabelPluginTypes.ts
+++ b/packages/core/src/BabelPluginTypes.ts
@@ -1,24 +1,24 @@
-import * as Babel from '@babel/core'
-import { File } from '@babel/types'
-import { ParserOptions } from '@codemod/parser'
+import type * as Babel from '@babel/core'
+import type { File } from '@babel/types'
+import type { ParserOptions } from '@codemod-esm/parser'
 
 /**
  * Fixes the `PluginObj` type from `@babel/core` by making all fields optional
  * and adding parser and generator override methods.
  */
 export interface PluginObj extends Partial> {
-  parserOverride?(
+  parserOverride?: (
     code: string,
     options: ParserOptions,
-    parse: (code: string, options: ParserOptions) => File
-  ): File
+    parse: (code: string, options: ParserOptions) => File,
+  ) => File
 
-  generatorOverride?(
+  generatorOverride?: (
     ast: File,
     options: Babel.GeneratorOptions,
     code: string,
-    generate: (ast: File, options: Babel.GeneratorOptions) => string
-  ): { code: string; map?: object }
+    generate: (ast: File, options: Babel.GeneratorOptions) => string,
+  ) => { code: string, map?: object }
 }
 
 export type RawBabelPlugin = (babel: typeof Babel) => PluginObj
diff --git a/packages/core/src/RecastPlugin.ts b/packages/core/src/RecastPlugin.ts
index 4bc3ace0..14f5bfff 100644
--- a/packages/core/src/RecastPlugin.ts
+++ b/packages/core/src/RecastPlugin.ts
@@ -1,12 +1,12 @@
-import { ParserOptions } from '@codemod/parser'
-import { File } from '@babel/types'
-import * as recast from 'recast'
-import { PluginObj } from './BabelPluginTypes'
+import type { File } from '@babel/types'
+import type { ParserOptions } from '@codemod-esm/parser'
+import type { PluginObj } from './BabelPluginTypes'
+import recast from 'recast'
 
 export function parse(
   code: string,
   options: ParserOptions,
-  parse: (code: string, options: ParserOptions) => File
+  parse: (code: string, options: ParserOptions) => File,
 ): File {
   return recast.parse(code, {
     parser: {
@@ -14,10 +14,11 @@ export function parse(
         return parse(code, { ...options, tokens: true })
       },
     },
-  })
+  }) as File
+  // Type guarded by parser.parse
 }
 
-export function generate(ast: File): { code: string; map?: object } {
+export function generate(ast: File): { code: string, map?: object } {
   return recast.print(ast, { sourceMapName: 'map.json' })
 }
 
diff --git a/packages/core/src/__tests__/test.ts b/packages/core/src/__tests__/index.test.ts
similarity index 65%
rename from packages/core/src/__tests__/test.ts
rename to packages/core/src/__tests__/index.test.ts
index fd7f6f40..49f641b2 100644
--- a/packages/core/src/__tests__/test.ts
+++ b/packages/core/src/__tests__/index.test.ts
@@ -1,4 +1,4 @@
-import { PluginItem } from '@babel/core'
+import type { PluginItem } from '@babel/core'
 import { transform } from '../transform'
 
 const incrementNumbersPlugin: PluginItem = {
@@ -9,18 +9,18 @@ const incrementNumbersPlugin: PluginItem = {
   },
 }
 
-test('preserves formatting', () => {
+it('preserves formatting', () => {
   expect(transform('var a=1;').code).toBe('var a=1;')
 })
 
-test('transforms using a custom babel plugin', () => {
+it('transforms using a custom babel plugin', () => {
   expect(
     transform('var a=1', {
       plugins: [incrementNumbersPlugin],
-    }).code
+    }).code,
   ).toBe('var a=2')
 })
 
-test('parses with as many parser plugins as possible', () => {
+it('parses with as many parser plugins as possible', () => {
   expect(() => transform('a ?? b').code).not.toThrow()
 })
diff --git a/packages/core/src/transform.ts b/packages/core/src/transform.ts
index 2b029197..bf9bb7bf 100644
--- a/packages/core/src/transform.ts
+++ b/packages/core/src/transform.ts
@@ -1,9 +1,9 @@
+import type { BabelFileResult, TransformOptions as BabelTransformOptions } from '@babel/core'
+import { strict as assert } from 'node:assert'
 import {
-  BabelFileResult,
-  TransformOptions as BabelTransformOptions,
+
   transformSync,
 } from '@babel/core'
-import { strict as assert } from 'assert'
 import { buildPlugin } from './AllSyntaxPlugin'
 import RecastPlugin from './RecastPlugin'
 
@@ -15,7 +15,7 @@ export type TransformOptions = BabelTransformOptions
  */
 export function transform(
   code: string,
-  options: TransformOptions = {}
+  options: TransformOptions = {},
 ): BabelFileResult {
   const result = transformSync(code, {
     ...options,
diff --git a/packages/core/tsconfig.build.json b/packages/core/tsconfig.build.json
index 77cbb310..f0f988db 100644
--- a/packages/core/tsconfig.build.json
+++ b/packages/core/tsconfig.build.json
@@ -1,13 +1,21 @@
 {
   "extends": "./tsconfig.json",
-  "include": ["src"],
-  "exclude": ["src/__tests__"],
   "compilerOptions": {
-    "noEmit": false,
-    "declaration": true,
-    "sourceMap": true,
     "rootDir": "src",
-    "outDir": "build"
+    "declaration": true,
+    "noEmit": false,
+    "outDir": "dist",
+    "sourceMap": true
   },
-  "references": [{ "path": "../parser/tsconfig.build.json" }]
+  "references": [
+    {
+      "path": "../parser/tsconfig.build.json"
+    }
+  ],
+  "include": [
+    "src"
+  ],
+  "exclude": [
+    "src/__tests__"
+  ]
 }
diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json
index ac7a13ab..f8769248 100644
--- a/packages/core/tsconfig.json
+++ b/packages/core/tsconfig.json
@@ -1,13 +1,7 @@
 {
-  "compilerOptions": {
-    "target": "es2015",
-    "module": "commonjs",
-    "lib": ["es2015", "es2016"],
-    "composite": true,
-    "noEmit": true,
-    "noImplicitAny": false,
-    "strict": true
-  },
-  "exclude": ["tmp", "build"],
-  "references": [{ "path": "../parser/tsconfig.build.json" }]
+  "extends": "../../tsconfig.json",
+  "exclude": [
+    "tmp",
+    "dist"
+  ]
 }
diff --git a/packages/matchers/.eslintignore b/packages/matchers/.eslintignore
deleted file mode 100644
index 6297e61a..00000000
--- a/packages/matchers/.eslintignore
+++ /dev/null
@@ -1,3 +0,0 @@
-build
-coverage
-src/matchers.ts
diff --git a/packages/matchers/README.md b/packages/matchers/README.md
index c102f8a1..b7891081 100644
--- a/packages/matchers/README.md
+++ b/packages/matchers/README.md
@@ -18,7 +18,7 @@ The easiest way to use this package is to use `@codemod/cli` like so:
 
 ```ts
 // `t` is `@babel/types`
-import { defineCodemod, t } from '@codemod/cli'
+import { defineCodemod, t } from '@codemod-esm/cli'
 
 // `m` is the `@codemod/matchers` module
 export default defineCodemod(({ m }) => ({
@@ -37,8 +37,8 @@ Just as you can build AST nodes with `@babel/types`, you can build AST node
 matchers to match an exact node with `@codemod/matchers`:
 
 ```ts
-import * as m from '@codemod/matchers'
 import * as t from '@babel/types'
+import * as m from '@codemod-esm/matchers'
 
 // `matcher` only matches Identifier nodes named 'test'
 const matcher = m.identifier('test')
@@ -52,8 +52,8 @@ matcher.match(t.identifier('test2')) // false
 Matching exact nodes is not usually what you want, however. `@codemod/matchers` can build matchers where only part of the data is specified:
 
 ```ts
-import * as m from '@codemod/matchers'
 import * as t from '@babel/types'
+import * as m from '@codemod-esm/matchers'
 
 // `matcher` matches any Identifier, regardless of name
 const matcher = m.identifier()
@@ -66,7 +66,7 @@ matcher.match(t.emptyStatement()) // false
 Here's a more complex example that matches any `console.log` calls. Assume that `expr` parses the given JS as an expression:
 
 ```ts
-import * as m from '@codemod/matchers'
+import * as m from '@codemod-esm/matchers'
 
 // `matcher` matches any `console.log(…)` call
 const matcher = m.callExpression(
@@ -82,7 +82,7 @@ matcher.match(expr('console.log')) // false
 There are a variety of fuzzy matchers that come with `@codemod/matchers`:
 
 ```ts
-import * as m from '@codemod/matchers'
+import * as m from '@codemod-esm/matchers'
 
 m.anyString().match('a string') // true
 m.anyString().match(1) // false
@@ -106,7 +106,7 @@ m.anyNode().match('a string') // false
 Often you'll want to capture part of the node that you've matched so that you can extract information from it or edit it.
 
 ```ts
-import * as m from '@codemod/matchers'
+import * as m from '@codemod-esm/matchers'
 
 // matches `console.(…)` calls
 const consoleMethod = m.capture(m.identifier())
@@ -137,7 +137,7 @@ if (matcher.match(expr('notAConsoleCall()'))) {
 Sometimes you'll want to refer to an earlier captured value in a later part of the matcher. For example, let's say you want to match a function expression which returns its argument:
 
 ```ts
-import * as m from '@codemod/matchers'
+import * as m from '@codemod-esm/matchers'
 
 const argumentMatcher = m.capture(m.identifier())
 const matcher = m.functionExpression(
@@ -158,6 +158,8 @@ matcher.match(expr('function(a) { return a + a; })')) // false
 All the previous examples have matchers testing a specific AST node. This is useful for illustration, but is not typically how you'd use them. Codemods written for `@codemod/cli` are [Babel plugins](https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md) and therefore use the visitor pattern to process ASTs. Here's the above example that identifies functions that do nothing but return their argument again, this time as a Babel plugin that replaces such functions with a global `IDENTITY` reference:
 
 ```ts
+import { NodePath } from '@babel/traverse'
+import * as t from '@babel/types'
 /**
  * Replaces identity functions with `IDENTITY`:
  *
@@ -167,9 +169,7 @@ All the previous examples have matchers testing a specific AST node. This is use
  *
  *   list.filter(IDENTITY);
  */
-import * as m from '@codemod/matchers'
-import * as t from '@babel/types'
-import { NodePath } from '@babel/traverse'
+import * as m from '@codemod-esm/matchers'
 
 export default function () {
   return {
@@ -194,6 +194,7 @@ export default function () {
 Here is the same plugin again without using `@codemod/matchers`:
 
 ```ts
+import { NodePath } from '@babel/traverse'
 /**
  * Replaces identity functions with `IDENTITY`:
  *
@@ -204,7 +205,6 @@ Here is the same plugin again without using `@codemod/matchers`:
  *   list.filter(IDENTITY);
  */
 import * as t from '@babel/types'
-import { NodePath } from '@babel/traverse'
 
 export default function () {
   return {
@@ -262,7 +262,7 @@ export default function () {
 Sometimes you know you want to match a node but don't know its depth in the tree, and thus can't hardcode a whole matching tree. To deal with this situation you can use the `containerOf` matcher. For example, this matcher will find the first `done` call inside a mocha test, accounting for whatever name might have been used for the parameter:
 
 ```ts
-import * as m from '@codemod/matchers'
+import * as m from '@codemod-esm/matchers'
 
 const doneParam = m.capture(m.identifier())
 const matcher = m.callExpression(m.identifier('test'), [
@@ -303,8 +303,8 @@ matcher.match(
 The easiest way to build custom matchers is simply by composing existing ones:
 
 ```ts
-import * as m from '@codemod/matchers'
 import * as t from '@babel/types'
+import * as m from '@codemod-esm/matchers'
 
 function plusEqualOne() {
   return m.assignmentExpression(
@@ -325,10 +325,10 @@ matcher.match(expr('a += 2')) // false
 You can build simple custom matchers easily using a predicate:
 
 ```ts
-import * as m from '@codemod/matchers'
+import * as m from '@codemod-esm/matchers'
 
 const oddNumberMatcher = m.matcher(
-  (value) => typeof value === 'number' && Math.abs(number % 2) === 1
+  value => typeof value === 'number' && Math.abs(number % 2) === 1
 )
 
 oddNumberMatcher.match(expr('-1')) // true
@@ -343,29 +343,29 @@ oddNumberMatcher.match(expr('NaN')) // false
 Such matchers are easily parameterized by wrapping it in a function:
 
 ```ts
-import * as m from '@codemod/matchers';
+import * as m from '@codemod-esm/matchers'
 
 function stringMatching(pattern: RegExp) {
   return m.matcher(
     value => typeof value === 'string' && pattern.test(value)
-  );
-)
+  )
+}
 
-const startsWithRun = stringMatching(/^run/);
+const startsWithRun = stringMatching(/^run/)
 
-startsWithRun.match('run');     // true
-startsWithRun.match('runner');  // true
-startsWithRun.match('running'); // true
-startsWithRun.match('ruining'); // false
-startsWithRun.match(' run');    // false
-startsWithRun.match('');        // false
-startsWithRun.match(1);         // false
+startsWithRun.match('run') // true
+startsWithRun.match('runner') // true
+startsWithRun.match('running') // true
+startsWithRun.match('ruining') // false
+startsWithRun.match(' run') // false
+startsWithRun.match('') // false
+startsWithRun.match(1) // false
 ```
 
 A common case where you think you'd need a custom matcher is when you want one of a few possible values. In such cases you can use the `or` matcher:
 
 ```ts
-import * as m from '@codemod/matchers'
+import * as m from '@codemod-esm/matchers'
 
 const matcher = m.or(m.anyString(), m.anyNumber())
 
@@ -378,6 +378,8 @@ matcher.match(expr('1')) // false
 Matching one of a few values is common when dealing with things such as functions, which could be arrow functions, function expressions, or function declarations. Here's a more general version of the `IDENTITY` codemod which uses the `or` matcher to also replace arrow functions:
 
 ```ts
+import { NodePath } from '@babel/traverse'
+import * as t from '@babel/types'
 /**
  * Replaces identity functions with `IDENTITY`:
  *
@@ -389,9 +391,7 @@ Matching one of a few values is common when dealing with things such as function
  *   list.filter(IDENTITY);
  *   list2.filter(IDENTITY);
  */
-import * as m from '@codemod/matchers'
-import * as t from '@babel/types'
-import { NodePath } from '@babel/traverse'
+import * as m from '@codemod-esm/matchers'
 
 export default function () {
   return {
@@ -418,8 +418,8 @@ export default function () {
 You probably won't need it, but you can build your own by subclassing `Matcher`. Here's the same `stringMatching` but as a subclass of `Matcher`:
 
 ```ts
-import * as m from '@codemod/matchers'
 import * as t from '@babel/types'
+import * as m from '@codemod-esm/matchers'
 
 // This is more ceremony than the simple predicate-based one above.
 class StringMatching extends m.Matcher {
diff --git a/packages/matchers/jest.config.js b/packages/matchers/jest.config.js
deleted file mode 100644
index 19578d5e..00000000
--- a/packages/matchers/jest.config.js
+++ /dev/null
@@ -1,17 +0,0 @@
-/* eslint-env node */
-
-/** @type {import('@jest/types').Config.InitialOptions} */
-module.exports = {
-  clearMocks: true,
-  moduleFileExtensions: ['ts', 'tsx', 'js'],
-  testEnvironment: 'node',
-  testRegex: '/__tests__/(test|.*\\.test)\\.ts$',
-  transform: {
-    '\\.ts$': 'esbuild-runner/jest',
-  },
-  collectCoverageFrom: [
-    'src/**/*.ts',
-    '!**/__tests__/**/*.ts',
-    '!src/matchers.ts',
-  ],
-}
diff --git a/packages/matchers/package.json b/packages/matchers/package.json
index ef3e3b5c..18027064 100644
--- a/packages/matchers/package.json
+++ b/packages/matchers/package.json
@@ -1,47 +1,45 @@
 {
-  "name": "@codemod/matchers",
-  "version": "1.7.0",
+  "name": "@codemod-esm/matchers",
+  "type": "module",
+  "version": "1.7.2",
   "description": "Matchers for JavaScript & TypeScript codemods.",
-  "repository": "https://github.com/codemod-js/codemod",
-  "license": "Apache-2.0",
   "author": "Brian Donovan",
-  "main": "build/index.js",
-  "types": "build/index.d.ts",
+  "license": "Apache-2.0",
+  "repository": "https://github.com/codemod-js/codemod",
+  "exports": {
+    "default": "./dist/index.js"
+  },
+  "main": "dist/index.js",
+  "types": "dist/index.d.ts",
   "files": [
-    "build"
+    "dist"
   ],
   "scripts": {
     "build": "tsc --build tsconfig.build.json",
-    "clean": "rm -rf build tsconfig.build.tsbuildinfo",
+    "clean": "rimraf dist tsconfig.build.tsbuildinfo",
     "lint": "eslint .",
     "lint:fix": "eslint . --fix",
     "prepublishOnly": "pnpm clean && pnpm build",
-    "test": "is-ci test:coverage test:watch",
-    "test:coverage": "jest --coverage",
-    "test:watch": "jest --watch"
+    "test": "bun test"
   },
   "dependencies": {
-    "@babel/types": "^7.20.7",
-    "@codemod/utils": "^1.1.0"
+    "@babel/types": "^7.26.9",
+    "@codemod-esm/utils": "workspace:^"
   },
   "devDependencies": {
-    "@babel/core": "^7.20.12",
-    "@babel/generator": "^7.20.14",
-    "@babel/traverse": "^7.20.13",
-    "@codemod/core": "^2.2.0",
-    "@codemod/parser": "^1.4.0",
-    "@types/babel__core": "^7.20.0",
-    "@types/babel__generator": "^7.6.4",
-    "@types/babel__template": "^7.4.1",
-    "@types/babel__traverse": "^7.18.3",
-    "@types/dedent": "^0.7.0",
-    "@types/jest": "^25.1.0",
-    "@types/node": "^18.14.0",
-    "@types/prettier": "^2.0.0",
-    "dedent": "^0.7.0",
-    "is-ci-cli": "^2.2.0",
-    "jest": "^27.3.1",
-    "typescript": "^4.9.5"
+    "@babel/core": "^7.26.9",
+    "@babel/generator": "^7.26.9",
+    "@babel/traverse": "^7.26.9",
+    "@codemod-esm/core": "workspace:^",
+    "@codemod-esm/parser": "workspace:^",
+    "@types/babel__core": "^7.20.5",
+    "@types/babel__generator": "^7.6.8",
+    "@types/babel__template": "^7.4.4",
+    "@types/babel__traverse": "^7.20.6",
+    "@types/bun": "^1.2.4",
+    "@types/node": "^22.13.9",
+    "dedent": "^1.5.3",
+    "typescript": "^5.8.2"
   },
   "publishConfig": {
     "access": "public"
diff --git a/packages/matchers/src/__tests__/distributeAcrossSlices.test.ts b/packages/matchers/src/__tests__/distributeAcrossSlices.test.ts
index 51c4a6a1..9edb21f5 100644
--- a/packages/matchers/src/__tests__/distributeAcrossSlices.test.ts
+++ b/packages/matchers/src/__tests__/distributeAcrossSlices.test.ts
@@ -1,43 +1,43 @@
 import { oneOrMore, slice, spacer, zeroOrMore } from '../matchers/slice'
 import { distributeAcrossSlices } from '../utils/distributeAcrossSlices'
 
-test('allocates nothing given an empty list of slices', () => {
+it('allocates nothing given an empty list of slices', () => {
   expect(Array.from(distributeAcrossSlices([], 1))).toEqual([[]])
 })
 
-test('allocates available to a single slice within its bounds', () => {
+it('allocates available to a single slice within its bounds', () => {
   expect(
-    Array.from(distributeAcrossSlices([slice({ min: 0, max: 3 })], 2))
+    Array.from(distributeAcrossSlices([slice({ min: 0, max: 3 })], 2)),
   ).toEqual([[2]])
   expect(
-    Array.from(distributeAcrossSlices([slice({ min: 2, max: 4 })], 3))
+    Array.from(distributeAcrossSlices([slice({ min: 2, max: 4 })], 3)),
   ).toEqual([[3]])
 })
 
-test('allocates nothing if available is outside single slice bounds', () => {
+it('allocates nothing if available is outside single slice bounds', () => {
   expect(
-    Array.from(distributeAcrossSlices([slice({ min: 2, max: 4 })], 1))
+    Array.from(distributeAcrossSlices([slice({ min: 2, max: 4 })], 1)),
   ).toEqual([])
   expect(
-    Array.from(distributeAcrossSlices([slice({ min: 2, max: 4 })], 5))
+    Array.from(distributeAcrossSlices([slice({ min: 2, max: 4 })], 5)),
   ).toEqual([])
 })
 
-test('allocates a single space across multiple slices', () => {
+it('allocates a single space across multiple slices', () => {
   expect(
     Array.from(
       distributeAcrossSlices(
         [slice({ min: 0, max: 1 }), slice({ min: 0, max: 1 })],
-        1
-      )
-    )
+        1,
+      ),
+    ),
   ).toEqual([
     [1, 0],
     [0, 1],
   ])
 })
 
-test('allocates multiple spaces across multiple slices', () => {
+it('allocates multiple spaces across multiple slices', () => {
   expect(
     Array.from(
       distributeAcrossSlices(
@@ -46,9 +46,9 @@ test('allocates multiple spaces across multiple slices', () => {
           slice({ min: 0, max: 1 }),
           slice({ min: 2, max: 3 }),
         ],
-        5
-      )
-    )
+        5,
+      ),
+    ),
   ).toEqual([
     [2, 1, 2],
     [2, 0, 3],
@@ -56,39 +56,45 @@ test('allocates multiple spaces across multiple slices', () => {
   ])
 })
 
-test('never allocates to empty slices', () => {
+it('never allocates to empty slices', () => {
   expect(
     Array.from(
       distributeAcrossSlices(
         [slice(0), slice({ min: 0, max: 1 }), slice({ min: 0, max: 1 })],
-        1
-      )
-    )
+        1,
+      ),
+    ),
   ).toEqual([
     [0, 1, 0],
     [0, 0, 1],
   ])
 })
 
-test('allocates correctly when slices have no upper bound', () => {
+it('allocates correctly when slices have no upper bound', () => {
   expect(Array.from(distributeAcrossSlices([zeroOrMore()], 2))).toEqual([[2]])
 })
 
-test('allocates correctly with a trailing unbounded slice', () => {
+it('allocates correctly with a trailing unbounded slice', () => {
   expect(
-    Array.from(distributeAcrossSlices([zeroOrMore(), slice(1), oneOrMore()], 1))
+    Array.from(
+      distributeAcrossSlices([zeroOrMore(), slice(1), oneOrMore()], 1),
+    ),
   ).toEqual([])
   expect(
-    Array.from(distributeAcrossSlices([zeroOrMore(), slice(1), oneOrMore()], 2))
+    Array.from(
+      distributeAcrossSlices([zeroOrMore(), slice(1), oneOrMore()], 2),
+    ),
   ).toEqual([[0, 1, 1]])
   expect(
-    Array.from(distributeAcrossSlices([zeroOrMore(), slice(1), oneOrMore()], 3))
+    Array.from(
+      distributeAcrossSlices([zeroOrMore(), slice(1), oneOrMore()], 3),
+    ),
   ).toEqual([
     [1, 1, 1],
     [0, 1, 2],
   ])
 })
 
-test('deprecated spacer() is an alias for slice(1)', () => {
+it('deprecated spacer() is an alias for slice(1)', () => {
   expect(spacer()).toEqual(slice(1))
 })
diff --git a/packages/matchers/src/__tests__/matchers.test.ts b/packages/matchers/src/__tests__/matchers.test.ts
index d744024f..3ce4104f 100644
--- a/packages/matchers/src/__tests__/matchers.test.ts
+++ b/packages/matchers/src/__tests__/matchers.test.ts
@@ -1,16 +1,15 @@
-import { js, t } from '@codemod/utils'
+import { js, t } from '@codemod-esm/utils'
 import * as m from '../matchers'
 
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-function expectType(value: T): void {
+function expectType(_value: T): void {
   // nothing to do
 }
 
-test('anyString matches strings', () => {
+it('anyString matches strings', () => {
   expect(m.anyString().match('')).toBeTruthy()
   expect(m.anyString().match('abc')).toBeTruthy()
   expect(m.anyString().match('hi\nthere')).toBeTruthy()
-  expect(m.anyString().match(new String('even this'))).toBeTruthy()
+  expect(m.anyString().match(String('even this'))).toBeTruthy()
 
   expect(m.anyString().match(0)).toBeFalsy()
   expect(m.anyString().match(null)).toBeFalsy()
@@ -21,14 +20,14 @@ test('anyString matches strings', () => {
   // verify `match` acts as a type assertion
   const value: unknown = undefined
   if (m.anyString().match(value)) {
-    ;() => value.toLowerCase()
+    void (() => value.toLowerCase())
   }
 })
 
-test('anyNumber matches numbers', () => {
+it('anyNumber matches numbers', () => {
   expect(m.anyNumber().match(0)).toBeTruthy()
-  expect(m.anyNumber().match(NaN)).toBeTruthy()
-  expect(m.anyNumber().match(new Number(1))).toBeTruthy()
+  expect(m.anyNumber().match(Number.NaN)).toBeTruthy()
+  expect(m.anyNumber().match(Number(1))).toBeTruthy()
   expect(m.anyNumber().match(Infinity)).toBeTruthy()
   expect(m.anyNumber().match(Number.MAX_VALUE)).toBeTruthy()
 
@@ -41,11 +40,11 @@ test('anyNumber matches numbers', () => {
   // verify `match` acts as a type assertion
   const value: unknown = undefined
   if (m.anyNumber().match(value)) {
-    ;() => value.toFixed()
+    void (() => value.toFixed())
   }
 })
 
-test('anything matches everything', () => {
+it('anything matches everything', () => {
   expect(m.anything().match(0)).toBeTruthy()
   expect(m.anything().match('')).toBeTruthy()
   expect(m.anything().match([])).toBeTruthy()
@@ -57,11 +56,11 @@ test('anything matches everything', () => {
   // verify `match` acts as a type assertion
   const value: unknown = undefined
   if (m.anything().match(value)) {
-    ;() => value.toFixed()
+    void (() => value.toFixed())
   }
 })
 
-test('arrayOf matches a variable-length homogenous array', () => {
+it('arrayOf matches a variable-length homogenous array', () => {
   expect(m.arrayOf(m.anyString()).match([])).toBeTruthy()
   expect(m.arrayOf(m.anyString()).match(['a', 'b'])).toBeTruthy()
   expect(m.arrayOf(m.arrayOf(m.anyString())).match([[], ['a']])).toBeTruthy()
@@ -77,15 +76,15 @@ test('arrayOf matches a variable-length homogenous array', () => {
   // verify `match` acts as a type assertion
   const value: unknown = undefined
   if (m.arrayOf(m.anyString()).match(value)) {
-    ;() => value.push('element')
+    void (() => value.push('element'))
   }
 })
 
-test('tupleOf matches a fixed-length array', () => {
+it('tupleOf matches a fixed-length array', () => {
   const stringNumberAnything = m.tupleOf(
     m.anyString(),
     m.anyNumber(),
-    m.anything()
+    m.anything(),
   )
 
   // happy path
@@ -103,11 +102,11 @@ test('tupleOf matches a fixed-length array', () => {
   // verify `match` acts as a type assertion
   const value: unknown = undefined
   if (stringNumberAnything.match(value)) {
-    ;() => value.length
+    void (() => value.length)
   }
 })
 
-test('oneOf matches a single-element array', () => {
+it('oneOf matches a single-element array', () => {
   expect(m.oneOf(m.anyString()).match([''])).toBeTruthy()
   expect(m.oneOf(m.anything()).match([{}])).toBeTruthy()
 
@@ -116,7 +115,7 @@ test('oneOf matches a single-element array', () => {
   expect(m.oneOf(m.anyString()).match([])).toBeFalsy()
 })
 
-test('anyNode matches any known AST node type', () => {
+it('anyNode matches any known AST node type', () => {
   expect(m.anyNode().match(t.identifier('abc'))).toBeTruthy()
   expect(m.anyNode().match(t.blockStatement([]))).toBeTruthy()
 
@@ -128,12 +127,12 @@ test('anyNode matches any known AST node type', () => {
   expect(m.anyNode().match(undefined)).toBeFalsy()
 })
 
-test('anyExpression matches any known AST expression node type', () => {
+it('anyExpression matches any known AST expression node type', () => {
   expect(m.anyExpression().match(t.identifier('abc'))).toBeTruthy()
   expect(
     m
       .anyExpression()
-      .match(t.functionExpression(null, [], t.blockStatement([])))
+      .match(t.functionExpression(null, [], t.blockStatement([]))),
   ).toBeTruthy()
 
   expect(m.anyExpression().match(t.file(t.program([]), [], []))).toBeFalsy()
@@ -143,7 +142,7 @@ test('anyExpression matches any known AST expression node type', () => {
   expect(m.anyExpression().match(t.blockStatement([]))).toBeFalsy()
 })
 
-test('anyStatement matches any known AST statement node type', () => {
+it('anyStatement matches any known AST statement node type', () => {
   expect(m.anyStatement().match(t.emptyStatement())).toBeTruthy()
   expect(m.anyStatement().match(t.returnStatement())).toBeTruthy()
   expect(m.anyStatement().match(t.blockStatement([]))).toBeTruthy()
@@ -154,39 +153,39 @@ test('anyStatement matches any known AST statement node type', () => {
   expect(m.anyStatement().match(t.program([]))).toBeFalsy()
 })
 
-test('m.function( matches any known function node type', () => {
+it('m.function( matches any known function node type', () => {
   expect(
-    m.function().match(t.functionDeclaration(null, [], t.blockStatement([])))
+    m.function().match(t.functionDeclaration(null, [], t.blockStatement([]))),
   ).toBeTruthy()
   expect(
-    m.function().match(t.functionExpression(null, [], t.blockStatement([])))
+    m.function().match(t.functionExpression(null, [], t.blockStatement([]))),
   ).toBeTruthy()
   expect(
-    m.function().match(t.arrowFunctionExpression([], t.blockStatement([])))
+    m.function().match(t.arrowFunctionExpression([], t.blockStatement([]))),
   ).toBeTruthy()
 
   expect(m.function().match(t.thisExpression())).toBeFalsy()
   expect(m.function().match(t.blockStatement([]))).toBeFalsy()
 })
 
-test('anyList reduces to tupleOf without any slices', () => {
+it('anyList reduces to tupleOf without any slices', () => {
   expect(m.anyList().match([])).toBeTruthy()
   expect(
-    m.anyList(m.anyString(), m.anyNumber()).match(['', 0])
+    m.anyList(m.anyString(), m.anyNumber()).match(['', 0]),
   ).toBeTruthy()
 })
 
-test('anyList with a fixed-width leading slice', () => {
+it('anyList with a fixed-width leading slice', () => {
   const list = m.anyList(m.slice(1), m.anyString())
   expect(list.match([''])).toBeFalsy()
   expect(list.match([{}, ''])).toBeTruthy()
   expect(list.match([{}, {}, ''])).toBeFalsy()
 })
 
-test('anyList with slices with specific matchers', () => {
+it('anyList with slices with specific matchers', () => {
   const list = m.anyList(
     m.slice({ min: 1, max: 2, matcher: m.anyNumber() }),
-    m.anyString()
+    m.anyString(),
   )
   expect(list.match([''])).toBeFalsy()
   expect(list.match([0, ''])).toBeTruthy()
@@ -196,57 +195,57 @@ test('anyList with slices with specific matchers', () => {
   const matcher = m.anyList(
     m.anyString(),
     m.oneOrMore(m.anyNumber()),
-    m.anyString()
+    m.anyString(),
   )
   expect(matcher.match(['', 1, 1, ''])).toBeTruthy()
   expect(matcher.match(['', 1, null, ''])).toBeFalsy()
 })
 
-test('anyList with a variable-width leading slice', () => {
+it('anyList with a variable-width leading slice', () => {
   const list = m.anyList(m.slice({ min: 0, max: 1 }), m.anyString())
   expect(list.match([''])).toBeTruthy()
   expect(list.match([{}, ''])).toBeTruthy()
   expect(list.match([{}, {}, ''])).toBeFalsy()
 })
 
-test('anyList with a zero-width leading slice', () => {
+it('anyList with a zero-width leading slice', () => {
   const list = m.anyList(m.slice(0), m.anyString())
   expect(list.match([''])).toBeTruthy()
   expect(list.match([{}, ''])).toBeFalsy()
   expect(list.match([{}, {}, ''])).toBeFalsy()
 })
 
-test('anyList with a fixed-width trailing slice', () => {
+it('anyList with a fixed-width trailing slice', () => {
   const list = m.anyList(m.anyString(), m.slice(1))
   expect(list.match([''])).toBeFalsy()
   expect(list.match(['', ''])).toBeTruthy()
   expect(list.match(['', '', ''])).toBeFalsy()
 })
 
-test('anyList with multiple fixed slices', () => {
+it('anyList with multiple fixed slices', () => {
   const list = m.anyList(
     m.slice(1),
     m.anyString(),
     m.slice(1),
     m.anyNumber(),
-    m.slice(1)
+    m.slice(1),
   )
   expect(list.match([])).toBeFalsy()
   expect(list.match([1, '', 2, 3, ''])).toBeTruthy()
   expect(list.match([1, {}, 2, 3, ''])).toBeFalsy()
 })
 
-test('anyList with multiple dynamic slices', () => {
+it('anyList with multiple dynamic slices', () => {
   const list = m.anyList(
     m.zeroOrMore(),
     m.returnStatement(),
-    m.oneOrMore()
+    m.oneOrMore(),
   )
   expect(list.match(js('return;').program.body)).toBeFalsy()
   expect(list.match(js('return; foo();').program.body)).toBeTruthy()
 })
 
-test('or matches one of the values', () => {
+it('or matches one of the values', () => {
   const mString = m.or(m.anyString())
   const mStringOrNumber = m.or(m.anyString(), m.anyNumber())
   const mStringOrNumberOrNull = m.or(m.anyString(), m.anyNumber(), null)
@@ -262,7 +261,7 @@ test('or matches one of the values', () => {
   expect(mStringOrNumber.match({})).toBeFalsy()
 })
 
-test('or matches literal values', () => {
+it('or matches literal values', () => {
   const m1 = m.or(1)
   const m1or2 = m.or(1, 2)
   expectType>(m1)
@@ -275,27 +274,27 @@ test('or matches literal values', () => {
   expect(m1or2.match({})).toBeFalsy()
 })
 
-test('or matches mixed literal values and matchers', () => {
+it('or matches mixed literal values and matchers', () => {
   expect(m.or(1, m.anyString()).match(1)).toBeTruthy()
   expect(m.or(1, m.anyString()).match('')).toBeTruthy()
   expect(m.or(1, m.anyString()).match({})).toBeFalsy()
 })
 
-test('containerOf recurses to find a node matching the pattern', () => {
+it('containerOf recurses to find a node matching the pattern', () => {
   expect(
     m
       .containerOf(m.binaryExpression('+', m.identifier(), m.numericLiteral()))
-      .match(js('return a + 1'))
+      .match(js('return a + 1')),
   ).toBeTruthy()
   expect(
-    m.containerOf(m.numericLiteral()).match(js('return a + 1'))
+    m.containerOf(m.numericLiteral()).match(js('return a + 1')),
   ).toBeTruthy()
   expect(
-    m.containerOf(m.numericLiteral()).match(js('return a + b'))
+    m.containerOf(m.numericLiteral()).match(js('return a + b')),
   ).toBeFalsy()
 })
 
-test('containerOf captures the first matching value', () => {
+it('containerOf captures the first matching value', () => {
   const plusMatcher = m.containerOf(m.binaryExpression('+'))
 
   expect(plusMatcher.match(js('console.log(a + b + c);'))).toBeTruthy()
@@ -307,28 +306,28 @@ test('containerOf captures the first matching value', () => {
     current: t.binaryExpression(
       '+',
       t.binaryExpression('+', t.identifier('a'), t.identifier('b')),
-      t.identifier('c')
+      t.identifier('c'),
     ),
   })
 })
 
-test('containerOf can be used in a nested matcher', () => {
+it('containerOf can be used in a nested matcher', () => {
   expect(
     m
       .containerOf(
         m.functionDeclaration(
           m.anything(),
           m.anything(),
-          m.containerOf(m.thisExpression())
-        )
+          m.containerOf(m.thisExpression()),
+        ),
       )
-      .match(js('function returnThis() { return this; }'))
+      .match(js('function returnThis() { return this; }')),
   ).toBeTruthy()
 })
 
-test('matcher builds a matcher based on a predicate', () => {
+it('matcher builds a matcher based on a predicate', () => {
   const matcher = m.matcher(
-    (value) => typeof value === 'string' && value.startsWith('no')
+    value => typeof value === 'string' && value.startsWith('no'),
   )
 
   expect(matcher.match('no')).toBeTruthy()
@@ -342,7 +341,7 @@ test('matcher builds a matcher based on a predicate', () => {
   expect(matcher.match(42)).toBeFalsy()
 })
 
-test('fromCapture builds a matcher based on a capturing matcher', () => {
+it('fromCapture builds a matcher based on a capturing matcher', () => {
   const capture = m.capture(m.identifier())
   const matcher = m.fromCapture(capture)
   const id = t.identifier('a')
@@ -360,3 +359,11 @@ test('fromCapture builds a matcher based on a capturing matcher', () => {
   expect(matcher.match(idUnequivalent)).toBeFalsy()
   expect(matcher.match(9)).toBeFalsy()
 })
+
+it('regression: #921', () => {
+  expect(
+    m
+      .functionExpression(m.anything())
+      .match(t.functionExpression(null, [], t.blockStatement([]))),
+  ).toBeTruthy()
+})
diff --git a/packages/matchers/src/matchers/Matcher.ts b/packages/matchers/src/matchers/Matcher.ts
index 238d48b5..8ec45c7d 100644
--- a/packages/matchers/src/matchers/Matcher.ts
+++ b/packages/matchers/src/matchers/Matcher.ts
@@ -4,11 +4,11 @@ export class Matcher {
   }
 
   matchValue(
-    /* eslint-disable @typescript-eslint/no-unused-vars */
     value: unknown,
-    keys: ReadonlyArray
-    /* eslint-enable @typescript-eslint/no-unused-vars */
+    keys: ReadonlyArray,
+
   ): value is T {
+    void keys
     throw new Error(`${this.constructor.name}#matchValue is not implemented`)
   }
 }
diff --git a/packages/matchers/src/matchers/anyList.ts b/packages/matchers/src/matchers/anyList.ts
index ad791c31..064751a9 100644
--- a/packages/matchers/src/matchers/anyList.ts
+++ b/packages/matchers/src/matchers/anyList.ts
@@ -10,14 +10,14 @@ export class AnyListMatcher extends Matcher> {
 
     for (const matcher of matchers) {
       if (matcher instanceof SliceMatcher) {
-        this.sliceMatchers.push(matcher)
+        this.sliceMatchers.push(matcher as SliceMatcher)
       }
     }
   }
 
   matchValue(
     array: unknown,
-    keys: ReadonlyArray
+    keys: ReadonlyArray,
   ): array is Array {
     if (!Array.isArray(array)) {
       return false
@@ -29,17 +29,17 @@ export class AnyListMatcher extends Matcher> {
 
     const spacerAllocations = distributeAcrossSlices(
       this.sliceMatchers,
-      array.length - this.matchers.length + this.sliceMatchers.length
+      array.length - this.matchers.length + this.sliceMatchers.length,
     )
 
     for (const allocations of spacerAllocations) {
-      const valuesToMatch: Array = array.slice()
+      const valuesToMatch: Array = array.slice() as T[]
       let matchedAll = true
       let key = 0
 
       for (const matcher of this.matchers) {
         if (matcher instanceof SliceMatcher) {
-          let sliceValueCount = allocations.shift() || 0
+          let sliceValueCount = allocations.shift() ?? 0
 
           while (sliceValueCount > 0) {
             const valueToMatch = valuesToMatch.shift()
@@ -50,10 +50,12 @@ export class AnyListMatcher extends Matcher> {
             sliceValueCount--
             key++
           }
-        } else if (!matcher.matchValue(valuesToMatch.shift(), [...keys, key])) {
+        }
+        else if (!matcher.matchValue(valuesToMatch.shift(), [...keys, key])) {
           matchedAll = false
           break
-        } else {
+        }
+        else {
           key++
         }
       }
@@ -61,7 +63,7 @@ export class AnyListMatcher extends Matcher> {
       if (matchedAll) {
         if (valuesToMatch.length > 0) {
           throw new Error(
-            `expected to consume all elements to match but ${valuesToMatch.length} remain!`
+            `expected to consume all elements to match but ${valuesToMatch.length} remain!`,
           )
         }
 
diff --git a/packages/matchers/src/matchers/anything.ts b/packages/matchers/src/matchers/anything.ts
index 537e3b0f..cea32f60 100644
--- a/packages/matchers/src/matchers/anything.ts
+++ b/packages/matchers/src/matchers/anything.ts
@@ -1,7 +1,6 @@
 import { Matcher } from './Matcher'
 
 export class AnythingMatcher extends Matcher {
-  // eslint-disable-next-line @typescript-eslint/no-unused-vars
   matchValue(value: unknown): value is T {
     return true
   }
diff --git a/packages/matchers/src/matchers/arrayOf.ts b/packages/matchers/src/matchers/arrayOf.ts
index dc7f64d9..84db15ab 100644
--- a/packages/matchers/src/matchers/arrayOf.ts
+++ b/packages/matchers/src/matchers/arrayOf.ts
@@ -7,7 +7,7 @@ export class ArrayOfMatcher extends Matcher> {
 
   matchValue(
     value: unknown,
-    keys: ReadonlyArray
+    keys: ReadonlyArray,
   ): value is Array {
     if (!Array.isArray(value)) {
       return false
diff --git a/packages/matchers/src/matchers/capture.ts b/packages/matchers/src/matchers/capture.ts
index 606f6455..ec21b572 100644
--- a/packages/matchers/src/matchers/capture.ts
+++ b/packages/matchers/src/matchers/capture.ts
@@ -2,7 +2,7 @@ import { anything } from './anything'
 import { Matcher } from './Matcher'
 
 export interface CaptureBase {
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+
   [key: string]: any
 }
 
@@ -26,7 +26,8 @@ export class CapturedMatcher extends Matcher {
     if (this.matcher.matchValue(value, keys)) {
       this.capture(value as unknown as C, keys)
       return true
-    } else {
+    }
+    else {
       return false
     }
   }
diff --git a/packages/matchers/src/matchers/containerOf.ts b/packages/matchers/src/matchers/containerOf.ts
index 6dcf14f6..b788d1b7 100644
--- a/packages/matchers/src/matchers/containerOf.ts
+++ b/packages/matchers/src/matchers/containerOf.ts
@@ -1,5 +1,5 @@
+import type { Matcher } from './Matcher'
 import * as t from '@babel/types'
-import { Matcher } from './Matcher'
 import { CapturedMatcher } from './capture'
 
 /**
@@ -9,7 +9,7 @@ import { CapturedMatcher } from './capture'
  */
 export class ContainerOfMatcher<
   C extends t.Node,
-  M extends t.Node = C
+  M extends t.Node = C,
 > extends CapturedMatcher {
   constructor(private readonly containedMatcher: Matcher) {
     super()
@@ -33,7 +33,8 @@ export class ContainerOfMatcher<
             return true
           }
         }
-      } else if (this.matchValue(valueAtKey, [...keys, key])) {
+      }
+      else if (this.matchValue(valueAtKey, [...keys, key])) {
         return true
       }
     }
@@ -43,7 +44,7 @@ export class ContainerOfMatcher<
 }
 
 export function containerOf(
-  containedMatcher: Matcher
+  containedMatcher: Matcher,
 ): ContainerOfMatcher {
   return new ContainerOfMatcher(containedMatcher)
 }
diff --git a/packages/matchers/src/matchers/fromCapture.ts b/packages/matchers/src/matchers/fromCapture.ts
index bf62cf5a..e3977928 100644
--- a/packages/matchers/src/matchers/fromCapture.ts
+++ b/packages/matchers/src/matchers/fromCapture.ts
@@ -1,5 +1,5 @@
-import { nodesEquivalent, t } from '@codemod/utils'
-import { CapturedMatcher } from './capture'
+import type { CapturedMatcher } from './capture'
+import { nodesEquivalent, t } from '@codemod-esm/utils'
 import { Matcher } from './Matcher'
 
 export class FromCaptureMatcher extends Matcher {
@@ -16,7 +16,7 @@ export class FromCaptureMatcher extends Matcher {
 }
 
 export function fromCapture(
-  capturedMatcher: CapturedMatcher
+  capturedMatcher: CapturedMatcher,
 ): Matcher {
   return new FromCaptureMatcher(capturedMatcher)
 }
diff --git a/packages/matchers/src/matchers/function.ts b/packages/matchers/src/matchers/function.ts
index 2779e630..aec7d77e 100644
--- a/packages/matchers/src/matchers/function.ts
+++ b/packages/matchers/src/matchers/function.ts
@@ -5,14 +5,14 @@ import { tupleOf } from './tupleOf'
 export class FunctionMatcher extends Matcher {
   constructor(
     private readonly params?: Matcher> | Array>,
-    private readonly body?: Matcher
+    private readonly body?: Matcher,
   ) {
     super()
   }
 
   matchValue(
     value: unknown,
-    keys: ReadonlyArray
+    keys: ReadonlyArray,
   ): value is t.Function {
     if (!t.isNode(value) || !t.isFunction(value)) {
       return false
@@ -25,7 +25,8 @@ export class FunctionMatcher extends Matcher {
         ) {
           return false
         }
-      } else if (!this.params.matchValue(value.params, [...keys, 'params'])) {
+      }
+      else if (!this.params.matchValue(value.params, [...keys, 'params'])) {
         return false
       }
     }
@@ -40,7 +41,7 @@ export class FunctionMatcher extends Matcher {
 
 export function Function(
   params?: Matcher> | Array>,
-  body?: Matcher
+  body?: Matcher,
 ): Matcher {
   return new FunctionMatcher(params, body)
 }
diff --git a/packages/matchers/src/matchers/generated.ts b/packages/matchers/src/matchers/generated.ts
index cbb7d761..6b497097 100644
--- a/packages/matchers/src/matchers/generated.ts
+++ b/packages/matchers/src/matchers/generated.ts
@@ -1,177 +1,177 @@
 /* DO NOT EDIT. This file was generated by 'script/rebuild' */
 /* eslint-disable */
-import * as t from '@babel/types'
+import * as t from "@babel/types";
 
-import { tupleOf } from './tupleOf'
-import { Matcher } from './Matcher'
+import { tupleOf } from "./tupleOf";
+import { Matcher } from "./Matcher";
 
 // aliases for keyword-named functions
-export { Import as import }
-export { Super as super }
+export { Import as import };
+export { Super as super };
 
 export class AnyTypeAnnotationMatcher extends Matcher {
   constructor() {
-    super()
+    super();
   }
 
   matchValue(
     node: unknown,
-    keys: ReadonlyArray
+    keys: ReadonlyArray,
   ): node is t.AnyTypeAnnotation {
     if (!t.isNode(node) || !t.isAnyTypeAnnotation(node)) {
-      return false
+      return false;
     }
 
-    return true
+    return true;
   }
 }
 
 export function anyTypeAnnotation(): Matcher {
-  return new AnyTypeAnnotationMatcher()
+  return new AnyTypeAnnotationMatcher();
 }
 
 export class ArgumentPlaceholderMatcher extends Matcher {
   constructor() {
-    super()
+    super();
   }
 
   matchValue(
     node: unknown,
-    keys: ReadonlyArray
+    keys: ReadonlyArray,
   ): node is t.ArgumentPlaceholder {
     if (!t.isNode(node) || !t.isArgumentPlaceholder(node)) {
-      return false
+      return false;
     }
 
-    return true
+    return true;
   }
 }
 
 export function argumentPlaceholder(): Matcher {
-  return new ArgumentPlaceholderMatcher()
+  return new ArgumentPlaceholderMatcher();
 }
 
 export class ArrayExpressionMatcher extends Matcher {
   constructor(
     private readonly elements?:
       | Matcher>
-      | Array | Matcher | Matcher>
+      | Array | Matcher | Matcher>,
   ) {
-    super()
+    super();
   }
 
   matchValue(
     node: unknown,
-    keys: ReadonlyArray
+    keys: ReadonlyArray,
   ): node is t.ArrayExpression {
     if (!t.isNode(node) || !t.isArrayExpression(node)) {
-      return false
+      return false;
     }
 
-    if (typeof this.elements === 'undefined') {
+    if (typeof this.elements === "undefined") {
       // undefined matcher means anything matches
     } else if (Array.isArray(this.elements)) {
       if (
         !tupleOf(...this.elements).matchValue(node.elements, [
           ...keys,
-          'elements',
+          "elements",
         ])
       ) {
-        return false
+        return false;
       }
     } else if (
-      !this.elements.matchValue(node.elements, [...keys, 'elements'])
+      !this.elements.matchValue(node.elements, [...keys, "elements"])
     ) {
-      return false
+      return false;
     }
 
-    return true
+    return true;
   }
 }
 
 export function arrayExpression(
   elements?:
     | Matcher>
-    | Array | Matcher | Matcher>
+    | Array | Matcher | Matcher>,
 ): Matcher {
-  return new ArrayExpressionMatcher(elements)
+  return new ArrayExpressionMatcher(elements);
 }
 
 export class ArrayPatternMatcher extends Matcher {
   constructor(
     private readonly elements?:
       | Matcher>
-      | Array | Matcher | Matcher>
+      | Array | Matcher | Matcher>,
   ) {
-    super()
+    super();
   }
 
   matchValue(
     node: unknown,
-    keys: ReadonlyArray
+    keys: ReadonlyArray,
   ): node is t.ArrayPattern {
     if (!t.isNode(node) || !t.isArrayPattern(node)) {
-      return false
+      return false;
     }
 
-    if (typeof this.elements === 'undefined') {
+    if (typeof this.elements === "undefined") {
       // undefined matcher means anything matches
     } else if (Array.isArray(this.elements)) {
       if (
         !tupleOf(...this.elements).matchValue(node.elements, [
           ...keys,
-          'elements',
+          "elements",
         ])
       ) {
-        return false
+        return false;
       }
     } else if (
-      !this.elements.matchValue(node.elements, [...keys, 'elements'])
+      !this.elements.matchValue(node.elements, [...keys, "elements"])
     ) {
-      return false
+      return false;
     }
 
-    return true
+    return true;
   }
 }
 
 export function arrayPattern(
   elements?:
     | Matcher>
-    | Array | Matcher | Matcher>
+    | Array | Matcher | Matcher>,
 ): Matcher {
-  return new ArrayPatternMatcher(elements)
+  return new ArrayPatternMatcher(elements);
 }
 
 export class ArrayTypeAnnotationMatcher extends Matcher {
   constructor(private readonly elementType?: Matcher) {
-    super()
+    super();
   }
 
   matchValue(
     node: unknown,
-    keys: ReadonlyArray
+    keys: ReadonlyArray,
   ): node is t.ArrayTypeAnnotation {
     if (!t.isNode(node) || !t.isArrayTypeAnnotation(node)) {
-      return false
+      return false;
     }
 
-    if (typeof this.elementType === 'undefined') {
+    if (typeof this.elementType === "undefined") {
       // undefined matcher means anything matches
     } else if (
-      !this.elementType.matchValue(node.elementType, [...keys, 'elementType'])
+      !this.elementType.matchValue(node.elementType, [...keys, "elementType"])
     ) {
-      return false
+      return false;
     }
 
-    return true
+    return true;
   }
 }
 
 export function arrayTypeAnnotation(
-  elementType?: Matcher
+  elementType?: Matcher,
 ): Matcher {
-  return new ArrayTypeAnnotationMatcher(elementType)
+  return new ArrayTypeAnnotationMatcher(elementType);
 }
 
 export class ArrowFunctionExpressionMatcher extends Matcher