codefast is a small, dependency-light CLI toolkit for TypeScript projects — a pnpm workspace or a single
package. It reorders Tailwind classes, generates package.json#exports from a package's built dist/, slims the npm
tarball at publish time, stamps @since on your public API, and audits a handful of source and documentation
conventions.
It was built for — and is exercised daily by — the codefast monorepo, but nothing here is codefast-only: run it in any pnpm workspace, or in a standalone package, and it works. A few audits encode an opinionated house style (called out below) that you can adopt, ignore, or narrow with an allowlist.
Design principles
- Safe by default. Every writing command supports
--dry-run, and every audit is read-only exceptaudit comments --fix. - Scriptable.
--jsonprints one JSON object on stdout instead of the human-readable output. - CI-ready. Audits exit non-zero when findings remain, so they gate a pipeline with no extra glue.
- Configurable. An optional
codefast.config.*file, validated by a strict schema, adjusts every command.
Requirements
- Node.js ≥ 22.12 (the CLI is published as ESM).
- TypeScript ≥ 7 to type-check a config written with
defineConfig, and forarrange simplify --fold-variant-classname, the one command that loadstypescript(an optional peer). - A project root — workspace or single package. Commands resolve their root by walking up from the current
directory: the nearest
pnpm-workspace.yamlmarks a workspace (every package under it is in scope), and with no workspace file the nearestpackage.jsonmarks a single package (that one package is the whole scope). Onlyarrange groupneeds no project at all — it just formats a string you paste in. - pnpm is not required to run the CLI — npm, npx, or a plain
nodeinvocation are all fine. pnpm matters only for workspace-wide behavior, which is keyed offpnpm-workspace.yaml.
Install
Install it globally, or run it once without installing:
# global install (pick your package manager)
pnpm add -g @codefast/cli
npm install -g @codefast/cli
# one-off, no install
pnpm dlx @codefast/cli --help
npx @codefast/cli --helpAs a project dev dependency:
pnpm add -D @codefast/cli
pnpm exec codefast --helpThe package is published on the 0.x line and versioned on its own track: breaking changes ship as minor versions,
so pin the minor (@codefast/cli@~0.9.0) when you need stability.
Quick start
codefast --help # list commands
codefast arrange group "flex h-10 w-full rounded-md bg-primary" # works anywhere, no project needed
# in a workspace or a single-package project:
codefast arrange inspect packages/ui/src # read-only report of arrange findings
codefast arrange --dry-run packages/ui/src # preview a class reorder
codefast mirror --dry-run # preview generated exports for each package in scope
codefast audit links # find broken markdown cross-referencesGlobal options and conventions
codefast --helplists the commands, and--helpon any command shows its usage;codefast --versionprints the installed version.- The global
--no-colorflag must come before the command name —codefast --no-color mirror, notcodefast mirror --no-color. - Writing commands write by default; pass
--dry-runto preview. The audits are read-only — the one exception isaudit comments --fix, which repairs section dividers in place. --jsonprints a single JSON object on stdout instead of the human-readable output, so any command can gate a script or a CI job.- Set
CODEFAST_VERBOSE=1(ortrue) for verbose error diagnostics. On an unexpected infrastructure failure the CLI also prints the underlying stack trace. This is distinct frommirror --verbose, which adds per-package progress detail to that one command.
Commands at a glance
codefast # Codefast monorepo developer CLI
│ ├─ --no-color # disable ANSI color output
│ └─ --version
│
├─ arrange [target] # regroup Tailwind classes in cn()/tv() in render-pipeline order
│ │ ├─ --dry-run # preview, without writing files
│ │ ├─ --with-class-name # append className as the final cn() argument (alias: --with-classname)
│ │ ├─ --cn-import <spec> # override the module specifier when adding a cn import
│ │ └─ --json
│ │
│ ├─ inspect [target] # report long strings, cn nested in tv() (read-only)
│ │ └─ --json
│ ├─ simplify [target] # flatten grouped arrays & static cn() to plain strings in tv() slots
│ │ ├─ --dry-run
│ │ └─ --fold-variant-class-name # fold cn() overrides into the variant's className option (uses the TS type server)
│ └─ group <tokens...> # group a pasted class string (stdout: cn(...) or a tv() array with --tv)
│ ├─ --tv # emit a tv()-style array instead of a cn() call
│ ├─ --with-class-name
│ └─ --json
│
├─ audit # source audits (read-only source scans)
│ ├─ rtl [target] # physical-direction Tailwind classes to make logical / rtl:-paired
│ ├─ links [target] # markdown links pointing at a missing path/anchor
│ ├─ imports [target] # banned import forms (React by-name, Zod namespace in front-end, …)
│ ├─ assertions [target] # double type assertions through unknown/any (x as unknown as T)
│ ├─ display-names [target] # token()/tag()/module names breaking the <namespace>:<Name> convention
│ ├─ props [target] # *Props aliases joined with & that an interface could express (TS type server)
│ ├─ publish [target] # what breaks a consumer's install: #/ imports, unshipped targets, @source paths
│ ├─ engines [target] # Node floors (engines.node) outside a dependency's engines or their @types/node line
│ ├─ layers [target] # value imports pointing up a package's configured layers
│ └─ comments [target] # section dividers not in the one allowed form
│ └─ --fix # rewrite every fixable divider in place (the only audit that writes)
│ (each audit also takes [target] + --json)
│
├─ mirror [package] # write package.json#exports from dist/ for workspace packages
│ ├─ --dry-run
│ ├─ -v, --verbose
│ └─ --json
│
├─ pack-slim [package] # strip src, source conditions, dev-only scripts/devDeps, source maps before publish
│ ├─ --dry-run
│ ├─ --force # run even if the git working tree has uncommitted changes
│ └─ --json
│
└─ tag [target] # add @since <version> to exported declarations
├─ --dry-run
└─ --jsonEvery command also responds to --help; each command's section below explains what its flags do.
| Command | What it does | Writes? |
|---|---|---|
arrange |
Regroup Tailwind classes in cn() / tv() calls in render-pipeline order |
yes (--dry-run) |
mirror |
Write each package's package.json#exports from its built dist/ |
yes (--dry-run) |
pack-slim |
Strip the dev-only surface from a package right before publish | yes (--dry-run) |
tag |
Stamp @since <version> on exported declarations that lack one |
yes (--dry-run) |
audit rtl |
Report physical-direction Tailwind classes that should be logical | no |
audit links |
Report markdown cross-references that resolve to nothing | no |
audit imports |
Enforce the import policy (React by-name, Zod namespace in front-end) | no (report only) |
audit assertions |
Report double type assertions through unknown / any, tests included |
no |
audit display-names |
Enforce the namespace:Name display-name convention |
no |
audit props |
Report props types joined with & that an interface could express |
no |
audit publish |
Report what would break a consumer's install of a published package | no |
audit engines |
Report each package's Node floor that does not hold | no |
audit layers |
Report value imports that point up a package's configured layers | no |
audit comments |
Check doc-comment conventions; repair section dividers | --fix only |
Which of these are for you? arrange, mirror, pack-slim, tag, audit links, audit assertions,
audit layers, audit publish, and audit engines are general-purpose — they work for any pnpm workspace or single
package that builds with tsc. The other five audits encode codefast's own house style (logical Tailwind directions,
named React imports, a specific comment/divider grammar, a namespace:Name scheme for @codefast/di tokens, interfaces
for component props). Adopt them if they fit your project; otherwise skip them, or use an allowlist to narrow their
scope.
arrange
Rewrites Tailwind class strings inside cn() and tv() calls, regrouping utilities in render-pipeline order —
existence, position, layout, sizing, spacing, shape, background, shadow, typography, composite, motion, starting,
behavior, state, selector — rather than alphabetically. (This is codefast's ordering, deliberately different from the
official Prettier Tailwind plugin's sort.)
codefast arrange inspect packages/ui/src # read-only report
codefast arrange --dry-run packages/ui/src # preview the rewrite
codefast arrange packages/ui/src # writeWhen [target] is omitted, arrange uses the nearest directory with a package.json, walking up from the current
directory. Directory scans skip test files (*.test.* / *.spec.*), because a cn() inside an assertion is
intentional; pass such a file explicitly to process it.
arrange rewrites a cn() / tv() call only when its binding is imported from a recognized module — clsx,
class-variance-authority, tailwind-variants, @codefast/tailwind-variants, a @/lib/utils / ~/lib/utils /
#lib/utils re-export, any …/utils path, or a dedicated cn.ts module — so an unrelated local cn is left alone.
Long static JSX className strings are regrouped regardless of where cn comes from.
Exits 1 when the arrange.onAfterWrite hook fails, 0 otherwise.
arrange inspect [target]
Read-only report of long strings, nested cn inside tv(), and related findings. Accepts --json.
arrange simplify [target]
Flattens grouped arrays and static-only cn() calls back to plain strings in tv() slots — the inverse cleanup pass.
In a mixed cn() call it coalesces only adjacent static literals and keeps argument order, so tailwind-merge
precedence is unchanged (a later argument still overrides an earlier one).
With --fold-variant-classname, cn(buttonVariants({ size: "sm" }), "flex-1") becomes
buttonVariants({ size: "sm", className: "flex-1" }), and a dynamic or multi-part override folds into a className
array. The fold fires only when the native TypeScript type server confirms the callee's options accept a className (or
class) of the right shape, so it loads the typescript package (an optional peer, v7) and needs the target inside a
tsconfig; files outside a project keep the base pass. Run a formatter afterward — a folded call can exceed the print
width until it is wrapped.
arrange group <tokens...>
Groups a pasted class string without touching the filesystem — the one command that needs no workspace, useful for checking how classes would be bucketed:
codefast arrange group "relative flex h-10 w-full items-center rounded-md bg-primary"
codefast arrange group --tv "flex items-center gap-2"With --json, that object is { schemaVersion, primaryLine, bucketsCommentLine }.
mirror
Scans each package's built dist/ tree and writes its package.json#exports map, plus top-level main, module, and
types mirrored from the root export and a files entry for dist. In a workspace it processes every package under
pnpm-workspace.yaml; in a single-package project it processes that one package. Build first — mirror reads
dist/, and stale output produces stale exports.
codefast mirror # all workspace packages
codefast mirror packages/ui # one package (path relative to repo root)
codefast mirror --dry-run # report changes without writingExits 1 when any package fails, 0 otherwise.
Per-package mirror configuration
The mirror config is a record keyed by package name, set under mirror in codefast.config.*. Set a package to
false to skip it entirely; omit a package to process it with defaults. For a package you do configure, these keys
apply (see the Configuration example for their shape):
source(boolean | string, defaulttrue) — emit asourcecondition pointing at the original.tsso a consumer using thesourcecondition resolves yoursrc/. A string overrides the root-export source path explicitly.types(boolean, defaulttrue) — emit thetypescondition when a matching.d.tsexists.import(boolean, defaulttrue) — emit theimportcondition.preserve(boolean) — keep the existingpackage.json#exportsmap as written and only fill in the missingsource/types/importconditions; nodist/scan runs, so the public surface stays exactly what you declared.strip(string) — adist/path prefix to flatten out of the generated specifiers, so./components/buttonis published as./buttonrather than leaking the internal folder.exclude(string[]) — specifiers to leave out of the generated map, making a package's public surface a decision rather than a consequence of itsdist/layout. Matched against the specifier as it appears inexports(afterstrip); a trailing/*excludes a whole subtree. The root export and./package.jsonare never excluded.exports(Record<string, string>) — extra or overriding entries merged into the generated map, for specifiers thedist/scan does not produce (for example a raw CSS source path).css(boolean | { enabled?, forceExportFiles?, customExports? }) — how CSS files indist/become exports:trueenables the default wildcard handling, and the object form tunes it (enabledtoggles it,forceExportFilesadds them tofiles,customExportssets explicit per-file CSS entries).
source, types, and import default to true, so an empty config object still emits all three.
pack-slim
Slims a published package down to what a consumer's tsc and Node actually read, so the npm tarball ships dist
runtime and types, plus any src subtree an export still points into (a raw stylesheet export such as ./css/*). Where
mirror writes the full exports — including the source condition — for local development, pack-slim removes that
development lane for publish: it drops the rest of src from files, every source condition from
exports/imports, every imports entry left pointing outside files, every script that is not an install or publish
lifecycle hook, devDependencies, and the dist source maps plus their dangling sourceMappingURL directives. Private
packages are skipped. It is meant to run on an ephemeral CI checkout right before publish, so its result is never
committed.
Because that result must never be committed, pack-slim refuses to write when the git working tree has uncommitted
tracked changes — a guard against an accidental local run landing on real work. --dry-run is exempt (it writes
nothing) and --force overrides the guard. In CI the guard is invisible: dist is gitignored, so the tree is already
clean when the release workflow runs pack-slim.
codefast pack-slim # every published package
codefast pack-slim packages/ui # one package (path relative to repo root)
codefast pack-slim --dry-run # report what would be stripped without touching a fileExits 1 when any package fails, 0 otherwise.
tag
Adds @since <version> tags to the doc comments of exported declarations that lack one, creating the doc block when
there is none. Overload signatures are declarations too, each stamped on its own, because the .d.ts keeps every
overload's doc block and drops the implementation's. The version comes from the nearest package.json above each target
file, and declarations that already carry @since are left alone. Run it at release time so published APIs carry
accurate version metadata — never hand-write @since.
A declaration with no doc block and a // comment on the line above it is reported instead: a block written there would
stack under a note, which audit comments rejects, or split a directive from the code it governs. Its file is left as
it is, so every reported line stays accurate. Write the doc block by hand before the release that ships the declaration,
since a later run can only stamp a version that did not introduce it.
codefast tag # auto-discover packages from cwd (or the single package)
codefast tag packages/ui/src # tag one directory or file
codefast tag --dry-run # summary only, no writesExits 1 when no target is selected, when any target fails, when a declaration is left unstamped, or when the
tag.onAfterWrite hook fails.
audit
Every audit is read-only, exits non-zero when findings remain (so it gates a CI pipeline with no extra glue), and takes
an optional [target] plus --json. Each also reads an allowlist from configuration for intentional exceptions.
audit links
General-purpose. Scans markdown for cross-references that point at nothing: a relative path that does not exist, an
in-document anchor with no matching heading or <a id>, and an anchor into another document that the target does not
offer. That last case is the reason this exists — a browser fails it silently by scrolling to the top. External URLs are
not checked, and links inside fenced code are treated as examples rather than references.
codefast audit links # whole repo
codefast audit links packages/di # explicit target
codefast audit links --json # machine-readable summaryConfigure exceptions via audit.links.allowlist — each entry is a bare link target or repo/relative/doc.md:target.
audit rtl
House style. Scans for physical-direction Tailwind classes (e.g. ml-*, left-*, text-left) that should use
logical equivalents (ms-*, start-*, text-start) or an rtl: companion (translate-x, space-x, resize cursors).
codefast audit rtl # uses audit.rtl.target from config
codefast audit rtl packages/ui/src # explicit target
codefast audit rtl --json # machine-readable summaryWith no [target], the scan root is audit.rtl.target from the config; when neither is set the command fails.
Configure exceptions via audit.rtl.allowlist — each entry is a bare class token or repo/relative/path.tsx:token.
audit imports
House style. Enforces the monorepo's import policy over .ts/.tsx files:
- React — members must be imported by name. Flags
import * as Reactand defaultReactimports (type-only included), plus an implicitReact.*UMD-global type reference (e: React.FormEventwith no import) thattscaccepts silently through theexport as namespace Reactdeclaration in@types/react. - Zod (front-end packages only) — flags a named
import { z } from "zod", which pins Zod's full locale set into the bundle;import * as z from "zod"lets bundlers tree-shake it.
codefast audit imports # whole repo
codefast audit imports apps/web/src # explicit target
codefast audit imports --json # machine-readable summaryConfigure exceptions via audit.imports.allowlist — each entry is the offending source text as written or
repo/relative/path.tsx:<text>.
audit assertions
Reports every double type assertion through unknown or any in .ts/.tsx files, tests included —
x as unknown as T, (x as unknown) as T, <T><unknown>x and the any spellings. The pair tells the compiler two
types are unrelated and silences it anyway; no oxlint rule targets it (typescript/no-unnecessary-type-assertion
catches the redundant ones only). Make the types agree, narrow with a type guard, or — where the erasure is the point —
keep it with a directive that states why, on the assertion's line or the line above:
// codefast-allow-double-assertion: the host stores each plugin's config untyped; the plugin owns its shape
const config = host.configFor(plugin.id) as unknown as PluginConfig;A directive with no reason, or one that keeps no assertion, is itself reported, so a kept assertion cannot outlive its cause.
codefast audit assertions # whole repo
codefast audit assertions packages/di # explicit target
codefast audit assertions --json # machine-readable summaryConfigure exceptions via audit.assertions.allowlist — each entry is the assertion as written or
repo/relative/path.ts:<assertion> — though the inline directive keeps the reason beside the code it excuses.
audit props
Reports every *Props type alias joined with & that an interface could express instead — at the alias's top level
(type ButtonProps = ComponentProps<"button"> & ButtonVariants & { … }) or passed to the type it wraps
(type ScrollAreaProps = ScopedProps<RootProps & ContextValue>) — and every component parameter annotated inline with
one: in a .tsx file, the first parameter of a component whose annotation is an intersection at its top level
(function ListItem({ href, ...props }: ComponentProps<"li"> & { href: string })), which wants naming as an interface.
A component is a module-level function with a capitalised name, as JSX renders one — a function declaration, or a
const holding an arrow or function expression, directly or through a call such as memo(…). A function nested in
another (an event handler, a callback) takes no props, so its parameters are never read. An interface checks its members
against every base where it is declared, so a conflicting prop fails there (TS2430, or TS2320 between two bases); &
intersects the two silently and the narrowed prop only fails at a call site.
Whether a part can be extended is the type checker's call, not a name list: the audit asks the native TypeScript type
server (typescript/unstable/sync) what each part resolves to, and keeps the & when one is a union (only &
distributes over its members), a type parameter or a mapped type over one (an interface cannot extend either), or no
object at all. A part that does not resolve is reported, since nothing can be said about it — on a workspace whose
packages consume each other's dist/, build first. The type server starts only once a file holds a candidate, and needs
the typescript package (v7) resolvable from the target and a tsconfig.json that includes each file.
codefast audit props # whole repo
codefast audit props packages/ui/src # explicit target
codefast audit props --json # machine-readable summaryConfigure exceptions via audit.props.allowlist — each entry is the alias's, or the parameter annotation's, first line
as written or repo/relative/path.tsx:<line> — though a type that must stay an intersection needs none.
audit publish
Reports what would break a consumer's install of a published package, while it can still be fixed:
- a
#/-prefixed internal import, which Node's ESM resolver rejects on Node 24 before 24.14 while every in-repo runner accepts it; - an
exports/importstarget the slimmed manifest does not ship, found by applying the same slim aspack-slim; - a shipped stylesheet whose Tailwind
@sourcepaths reach none of the files the tarball ships. A workspace still resolves them againstsrc, so only the published layout shows it: the consumer's Tailwind registers no class names and the components render unstyled.
It reads the built output, so run it after the build; a files entry missing on disk, such as an unbuilt dist, is
named in the report. Private packages are skipped, and it takes no allowlist.
codefast audit publish # every published package in the workspace
codefast audit publish --json # machine-readable summaryaudit engines
General-purpose. Holds each workspace package's own Node floor — its engines.node — to what actually runs on it, so
packages can sit on different floors, each as low as the package can go. It reports:
- a published package that declares no floor, and a floor not written
>=X.Y.Z; - a release the floor claims that a dependency's
engines.nodeleaves out, named with the dependency, its range and the sub-range it misses. A runtime dependency (dependencies,peerDependencies,optionalDependencies) a consumer installs must admit every release from the floor up — the odd majors below the one.node-versionpins aside, since they reach end-of-life without becoming LTS — so^22.18.0 || >=24.11.0fails a>=22.18.0floor on 24.0–24.10. A dev dependency only runs the package's tests, so it must admit the floor and the.node-versionrelease CI runs them on. A workspace dependency is read from its manifest, any other from the package's ownnode_modules, so run the audit after the install; - an
@types/nodethe lockfile (pnpm-lock.yaml) resolves on another major than the floor or on a newer minor, where the compiler would accept APIs the floor lacks.
The workspace root's own engines.node, when written >=X.Y.Z, is the contributors' floor rather than one any package
inherits: a contributor on it builds and tests every package, so every dependency any of them installs, runtime and dev
alike, must admit every release from the root floor up, short-lived odd lines aside. Each gap is reported once per
dependency range, naming the packages that install it. The root is never listed among the floors and is checked only
when the target reaches it. A narrower [target] checks only the packages it reaches, still against every floor they
depend on. --json lists every floor with its package directory, which is enough to plan one CI leg per floor. It takes
no allowlist.
codefast audit engines # every package in the workspace
codefast audit engines packages/di # one package
codefast audit engines --json # machine-readable summary, floors includedaudit layers
General-purpose. Holds a package's src/ to the layering its architecture states. You list the layers in
configuration, bottom to top, each naming the families directly under the root — a directory, or a lone module sitting
flat (errors.ts) — and the audit reports every value import or re-export that points up the list, plus every module no
layer places. Type-only imports erase at build time and couple nothing, so they may point anywhere; a dynamic import()
counts as a value import wherever it sits.
// codefast.config.js
export default {
audit: {
layers: {
packages: {
"@acme/di": {
root: "src", // where the layers sit, relative to the package (default)
layers: [["core", "errors.ts"], ["engine"], ["container"], ["index.ts"]],
},
},
allowlist: [],
},
},
};codefast audit layers # every package audit.layers.packages names
codefast audit layers packages/di # one package, or a directory beneath its root
codefast audit layers --json # machine-readable summaryA configured package name the workspace does not hold, a layer entry nested below the root, and a family placed twice
are refused before anything is scanned. Configure exceptions via audit.layers.allowlist — each entry is the offending
import as written or repo/relative/path.ts:<import>.
audit comments
House style. Checks doc-comment conventions. Section dividers not in the one allowed form are mechanical, so --fix
rewrites them in place. The rest is reported for a person to fix: TSDoc grammar errors, JSDoc {type} payloads,
comments pointing at repo documents, @param lists that name some parameters but not all, @param descriptions without
the - separator, @since tags out of position or naming a version the package has not reached, and comment links to
missing paths.
codefast audit comments # whole repo
codefast audit comments packages/di/src # explicit target
codefast audit comments --fix # rewrite fixable dividers in place
codefast audit comments --json # machine-readable summaryConfigure exceptions via audit.comments.allowlist — each entry is a divider line as written or
repo/relative/path.ts:<divider>.
audit display-names
House style. Enforces the display-name convention for every string a token(), tag(), or module factory takes: a
name is spelled like the TS symbol it stands for, under its owner's namespace — namespace:Name. The namespace is a
kebab-case package, app, or feature slug (or a scoped package name); a token or module name is PascalCase, a tag key is
camelCase. It scans TypeScript and markdown alike, since a doc sample is what a reader copies, and skips tests/,
benchmarks/, .changeset/, and CHANGELOG.md.
codefast audit display-names # whole repo
codefast audit display-names packages/di/examples # explicit target
codefast audit display-names --json # machine-readable summaryConfigure exceptions via audit.displayNames.allowlist — each entry is the call as written, through its closing quote
(or parenthesis when the name is the only argument), or repo/relative/path.ts:<call>.
Configuration
You do not need a config file. Every command has sensible defaults and works with none. Add a codefast.config.*
file at your project root only to change a default — and add only the sections for the commands you actually use.
Where it goes and how it loads. The CLI walks up from the working directory and uses the first match, checking
codefast.config.mjs, codefast.config.js, codefast.config.cjs, then codefast.config.json in each directory. JS
configs are loaded via jiti — so only run the CLI in repositories you trust, and
note that only a JS config can define onAfterWrite hooks (JSON can't hold functions). The schema is strict: an
unknown key is an error, which catches typos immediately.
Start small
The smallest valid config is empty. Grow it one section at a time — each top-level key configures one command:
// codefast.config.js
export default {};| Key | Command | What it configures |
|---|---|---|
mirror |
mirror |
per-package exports generation — see per-package config |
tag |
tag |
package names to skip, and a hook to run after writing |
arrange |
arrange |
a hook to run after writing |
audit |
audit * |
each audit's default scan target and its allowlist of accepted exceptions |
Author it with types
Don't memorize the shape. Import defineConfig (or annotate with the CodefastConfig type) and your editor completes
every key, checks the values, and catches typos before you run anything — the types are the reference for what's
valid, and the strict runtime schema is the backstop.
// codefast.config.ts
import { defineConfig } from "@codefast/cli";
export default defineConfig({
mirror: { "@acme/ui": { strip: "./components/" } }, // autocomplete: strip, exclude, source, types, css, …
});A plain .js config gets the same help through a JSDoc type — no build step, no .ts:
// codefast.config.js
/** @type {import("@codefast/cli").CodefastConfig} */
export default {
mirror: { "@acme/ui": { strip: "./components/" } },
};Common recipes
Run a formatter after a command rewrites files. tag and arrange take an onAfterWrite hook (sync or async). It
runs only when files were actually written — never on --dry-run:
// codefast.config.js
import { execSync } from "node:child_process";
const format = ({ files }) => execSync(`oxfmt ${files.join(" ")}`, { stdio: "inherit" });
export default {
tag: { onAfterWrite: format },
arrange: { onAfterWrite: format },
};Skip packages. tag.skipPackages takes globs matched against package names; mirror skips any package set to
false:
export default {
tag: { skipPackages: ["@acme/internal", "@apps/*"] },
mirror: { "@acme/internal": false },
};Accept a known audit finding. Every audit takes an allowlist. An entry is the offending text exactly as it
appears, or repo/relative/path:<text> to scope it to a single file:
export default {
audit: {
imports: { allowlist: [`packages/legacy/src/x.ts:import { z } from "zod";`] },
rtl: { allowlist: ["packages/ui/src/variants/sheet.ts:data-open:slide-in-from-left-10"] },
},
};Complete reference
Every section together — see per-package mirror configuration for the mirror
keys:
// codefast.config.js
import { execSync } from "node:child_process";
export default {
// Keyed by package name; `false` skips the package, omitted packages use defaults.
mirror: {
"@acme/ui": {
strip: "./components/", // flatten a dist/ prefix out of public specifiers
exclude: ["./internal/*"], // specifiers to leave out of the generated map
exports: { "./css/*": "./src/css/*" }, // extra or overriding entries
source: true, // add a `source` condition (a string overrides the root path)
types: true, // add `types` when a .d.ts exists
import: true, // add the `import` condition
css: true, // boolean or { enabled, forceExportFiles, customExports }
},
"@acme/tailwind-variants": { preserve: true }, // keep exports as-is, only fill missing conditions
"@acme/internal": false,
},
tag: {
skipPackages: ["@acme/internal", "@apps/*"], // glob patterns matched against package names
onAfterWrite: ({ files }) => execSync(`oxfmt ${files.join(" ")}`, { stdio: "inherit" }),
},
arrange: {
onAfterWrite: ({ files }) => execSync(`oxfmt ${files.join(" ")}`, { stdio: "inherit" }),
},
audit: {
rtl: {
target: "packages/ui/src", // default scan root when no CLI arg is passed
allowlist: [
// bare token, or `repo/relative/path.tsx:token`
"packages/ui/src/variants/sheet.ts:data-open:slide-in-from-left-10",
],
},
links: { allowlist: [] }, // bare link target, or `repo/relative/doc.md:target`
comments: { allowlist: [] }, // divider as written, or `repo/relative/path.ts:<divider>`
imports: { allowlist: [] }, // offending import text as written, or `repo/relative/path.tsx:<text>`
assertions: { allowlist: [] }, // assertion as written, or `repo/relative/path.ts:<assertion>`
displayNames: { allowlist: [] }, // call as written, or `repo/relative/path.ts:<call>`
layers: {
// per package: its layers bottom to top, each entry a directory or module file under `root` (default `src`)
packages: { "@acme/di": { layers: [["core", "errors.ts"], ["engine"], ["index.ts"]] } },
allowlist: [], // import as written, or `repo/relative/path.ts:<import>`
},
},
};source, types, and import default to true, so an empty mirror entry still emits all three. The onAfterWrite
hooks run only when files were actually written — never on --dry-run; a hook failure is reported on stderr and the
command exits 1.
Exit codes
| Code | Meaning |
|---|---|
0 |
Success. |
1 |
General failure (missing paths, failed packages, failed hooks). |
2 |
Invalid arguments or configuration. |
Programmatic use
@codefast/cli is importable as well as executable. runCli runs the same CLI in-process and resolves to the exit code
it would have exited with — the codefast binary is a thin wrapper around it.
import { runCli } from "@codefast/cli";
// `argv` follows the `process.argv` layout: the first two entries are ignored,
// exactly as when Node runs the binary.
const exitCode = await runCli(["node", "codefast", "mirror", "--dry-run", "--json"]);
if (exitCode !== 0) {
throw new Error(`codefast exited with ${exitCode}`);
}The command still writes its human or --json output to stdout/stderr; runCli does not capture it. Read stdout
yourself when you need the structured summary.
How the codefast monorepo uses it
The tool is general; the codefast monorepo just wires convenience scripts and a release step around it — a good template
if you adopt the CLI in your own workspace. It runs from the built output via root package.json scripts:
pnpm run codefast <command> # generic entry: node ./packages/cli/dist/bin.js
pnpm run cli:arrange # codefast arrange
pnpm run cli:mirror # codefast mirror
pnpm run cli:audit:links # codefast audit links
pnpm run cli:audit:rtl # codefast audit rtl
pnpm run cli:audit:comments # codefast audit comments
pnpm run cli:audit:imports # codefast audit imports
pnpm run cli:audit:assertions # codefast audit assertions
pnpm run cli:audit:display-names # codefast audit display-names
pnpm run cli:audit:layers # codefast audit layers
pnpm run cli:audit:publish # codefast audit publish
pnpm run cli:audit:engines # codefast audit enginespnpm run version-packages runs changeset version and then codefast tag, so published APIs are stamped at release,
and the release workflow runs codefast pack-slim as its publish step on a clean CI checkout.
Documentation
- codefastlabs.com/docs/cli — this document, rendered.
ARCHITECTURE.md— how the package is laid out: command wiring, theResulttype, and the filesystem port.DECISIONS.md— the design decisions that shape the package and the reasons behind them.CHANGELOG.md— release notes for every published version.
Contributing
The package is developed in the codefast monorepo; the repo-wide contributing guide covers setup, the test taxonomy, and the release flow.
License
Released under the MIT License.