CAC meets its no-dependency claim by inlining mri, and configures two git hook managers at once
Simple yet powerful framework for building command-line apps.
At a glance
- What is it?
- cacjs/cac is a MIT licensed single-file argument parser in TypeScript, built on tsdown and published to both npm and the JSR registry for Deno. The manifest declares one dependency and inlines it, exports a single entry point, and configures both Husky and simple-git-hooks.
- Who is it for?
- CAC suits a small tool where the argument parser should not become a dependency problem, and the surface is genuinely four methods for a basic CLI with everything else opt-in. Three things to know.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 17 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 5, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The no-dependency claim is met by inlining mri
The feature list leads with a weight claim: no dependency, just a single file.
The manifest is more specific about how that is achieved. There is a dedicated field:
"inlinedDependencies": {
"mri": "1.2.0"
}So the library does have a dependency, `mri` at version 1.2.0, and it is declared twice: once in that inlining list, and once in the development dependencies so the build can compile it. The bundler inlines it, and what reaches a consumer is a single file with no imports of its own.
That is a legitimate way to keep the promise, and it has a consequence worth naming. The version of `mri` that ships is frozen at the version listed in that field, so a consumer cannot pick up a security fix in the underlying parser through a dependency update. They get it when the bundle is rebuilt and republished.
The same pattern governs what is published. The `files` array contains one entry, `dist`, and the export map has two:
"exports": {
".": "./dist/index.js",
"./package.json": "./package.json"
}One entry point and the package manifest. There is no subpath, so importing anything deeper than the root is not possible.
Two git hook managers are configured in the same manifest and only one is installed
The manifest declares two pre-commit hooks under two different systems:
"pre-commit": "pnpm test && lint-staged"
"pre-commit": "pnpm lint-staged && pnpm typecheck"The first sits under a `husky` key. The second sits under `simple-git-hooks`. Both objects are present in the same file.
Then look at the development dependencies. `simple-git-hooks` is there, at version 2.13.1. Husky is not.
So the configuration for a hook manager nobody installs is still in the manifest, and the two hooks do different work. The Husky one runs the test suite and then the staged linter. The simple-git-hooks one runs the staged linter and then the type checker. Under the second, the tests never run on commit. Under the first, the type checker never does.
The `lint-staged` call in both is also written as `pnpm lint-staged`, which is a script name the manifest never defines; the declared scripts are lint, lint:fix, build, dev, test, typecheck, format, release, and prepublishOnly. So the hook calls a script that is not there, in whichever system is actually installed.
Typechecking runs a dated nightly compiler while the declared compiler is TypeScript 6
The typecheck script is `tsgo --noEmit`. There is no `tsgo` script and no tsgo package in the manifest; the tool that provides it is a dev dependency with a pinned nightly version:
`@typescript/native-preview` at `7.0.0-dev.20260707.2`.
Meanwhile `typescript` itself is declared at `^6.0.3`. So the compiler used for the library's build is the stable 6.x line, and the compiler that checks types is a native preview build from a specific day in July 2026. Those are two different compilers with two different version numbers, and only one of them is a caret range.
The rest of the toolchain is built from the maintainer's own shared packages: `@sxzz/eslint-config` and `@sxzz/prettier-config` come from the same scope, and the bundler comes with a matching preset called `tsdown-preset-sxzz`. ESLint is at version 10 with a flat config file at the root, Vitest is at 4 with a v8 coverage plugin, and the release script is `bumpp` with a `prepublishOnly` that runs the build.
Versioning is declared too: `[email protected]`, with a workspace file and a lockfile at the root. The runtime floor is `node >=20.19.0`.
The CircleCI badge points at the master branch and the default branch is main
The badge row at the top of the file links continuous integration at `circleci.com/gh/cacjs/cac/tree/master`.
The repository's default branch is `main`. So the badge links to a build view for a branch name that is not the one the project works on, which means the status shown on the front page is reporting on something other than the code in front of it.
The same badge row has a second problem. The npm badge appears twice, both links pointing at the same package page. Six badge links are listed and five are distinct destinations, one of which is a donation link rather than anything about the project.
None of this affects the library. It is the kind of thing that stays in a file for years because badges are added once and the branch gets renamed later.
It is worth pairing with the release history. The newest tags are v7.0.0 on 2026-02-27 and v7.0.0-beta.1 a week before it. Before those, the previous visible tag is v6.7.14 from 2022-08-29. So the 6.x line ran for three and a half years after its last release, then a major version arrived, and the branch has been pushed since without a tag following it.
The README imports cac three different ways
Read the import line at the top of each example and three forms appear.
Most examples use a default import:
import cac from 'cac'The TypeScript section uses a named import:
import { cac } from 'cac'And the Deno section uses a named import from a different registry entirely:
import { cac } from 'jsr:@cac/cac'The third one is not a variation, it is a different package specifier. The manifest is `type: module` with a single export map, and the repository root carries a `jsr.json`, so the package is published to the JSR registry as well as to npm. That is a deliberate choice, and it means the Deno example is importing from a second distribution channel rather than from npm.
The two npm forms are a different matter. The same package is shown as a default export in the main examples and as a named export in the TypeScript guidance, and the file does not say which one is correct. Anyone copying the TypeScript section into a JavaScript file, or the reverse, gets a different answer from the two halves of the document.
The example comments name .js files and the repository ships .ts files
Two examples carry a path comment at the top of the code block. One reads `// examples/basic-usage.js` and the other reads `// examples/help.js`.
The example directory contains neither of those files. It contains `examples/basic-usage.ts` and `examples/help.ts`, alongside `command-examples.ts`, `command-options.ts`, `default-command.ts`, `default-command-inverted.ts`, `deno.ts`, `dot-nested-options.ts`, `negated-option.ts`, `sub-command.ts`, `variadic-arguments.ts`, `ignore-default-value.ts`, and a `browser` directory.
So the comments are stale by one rename, from when the examples were JavaScript. Thirteen example files and a browser directory exist; two of them are referenced by a path that does not match.
The browser directory and the runtime type packages point the same way as the Deno example. There are type definitions for Node, for Bun, and for Deno in the development dependencies, which is three runtimes typed for a library whose own engine requirement names only Node at 20.19 or newer.
Unknown options are only reported when a command has an action attached
The documentation on command-specific options contains a caveat that is easy to skim past.
A command's options are validated when the command is used, and any unknown options are reported as an error. However, if an action-based command does not define an action, then the options are not validated.
Read plainly: attaching options to a command is not enough. The validation is tied to having an action, and a command registered with options but no action accepts anything. There is a documented escape hatch for the cases where that is what you want, the `allowUnknownOptions` method on a command, which suggests the default was chosen to be strict and the exception has to be requested.
The rest of the option surface follows the same deliberate pattern. Kebab-case and camelCase both map to the camelCase property, so `--clear-screen` and `--clearScreen` reach the same value. A negated option has to be declared by hand, and declaring `--no-config` makes `config` default to true. Brackets carry two separate meanings depending on where they appear: angled brackets in a command name mean a required argument and square brackets mean optional, while in an option name angled brackets mean a required value and square brackets mean the value may also be the boolean true.
A single dist directory means there is nothing to import but the root
The published shape of this package is unusually small and worth spelling out.
The `files` array is `dist`. The export map offers the root and the package manifest, nothing else. `sideEffects` is declared false, which tells a bundler it can drop imports whose bindings are unused. And the build is a single command, `tsdown`, with a watch mode for development.
That combination means there is no deep import path at all. A consumer who wants a sub-part of the library cannot reach it through the package name, because the export map does not offer one. Everything is either the root entry point or the manifest.
For a parser whose public surface is four methods, `cli.option`, `cli.version`, `cli.help`, and `cli.parse`, that is a defensible design. The extra features beyond those four, default commands, git-style subcommands, validation of required arguments, variadic arguments, dot-nested options, and generated help text, are all reached through methods on the same object.
One loose end sits in the repository root rather than the package: a `skills/` directory alongside the source, tests, and configuration. Nothing in the README mentions it.
Editorial conclusion
CAC suits a small tool where the argument parser should not become a dependency problem, and the surface is genuinely four methods for a basic CLI with everything else opt-in. Three things to know. The no-dependency promise is delivered by a bundling step that inlines mri, so the published artefact is one file but the repository is not dependency-free. Validation of unknown options only happens on commands that have an action attached, which is a quiet gap rather than a loud one. And the manifest configures two git hook managers, so a contributor installing only what the manifest lists as a dev dependency gets one of them and not the other.
Frequently asked questions
What is CAC and how do I install it?
CAC is a MIT licensed JavaScript and TypeScript library for building command line apps, standing for Command And Conquer. It is installed with npm i cac. The basic path uses cli.option, cli.version, cli.help, and cli.parse, and it requires Node 20.19.0 or newer.
Does CAC have any dependencies?
The feature list claims no dependency and a single file. The manifest inlines one dependency, mri at version 1.2.0, through an inlinedDependencies field, and declares mri in the development dependencies so the bundle can be built. Consumers get one file with no runtime imports.
How do I use CAC with TypeScript or Deno?
For TypeScript, install @types/node as a dev dependency and import { cac } from 'cac'. For Deno the README imports { cac } from jsr:@cac/cac, and the repository root carries a jsr.json, so the package is published to the JSR registry as well as to npm.
Does CAC report unknown command options?
A command's options are validated when the command is used and unknown options are reported as an error. However, if an action based command does not define an action, then the options are not validated. The documented escape hatch is command.allowUnknownOptions.
Which projects use CAC?
The README names Vite, Vitest, tsdown, VuePress, DocPad, bili, Lad, Lass, Foy, and Vuese. Releases include v7.0.0 on 2026-02-27 and v7.0.0-beta.1 a week earlier, after v6.7.14 from 2022-08-29.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/cacjs-cac)