◦◦ ◦◦ ◦◦ ◦◦ ◦◦◦◦◦◦◦ ◦◦◦◦◦◦◦ ◦◦ ◦◦ ◦◦ ◦◦◦◦◦◦ ◦◦◦◦◦◦◦◦ ◦◦◦◦◦◦
◦◦ ◦◦ ◦◦◦ ◦◦ ◦◦ ◦◦ ◦◦ ◦◦ ◦◦◦◦ ◦◦◦ ◦◦ ◦◦ ◦◦ ◦◦ ◦◦ ◦◦
◦◦ ◦◦ ◦◦◦◦ ◦◦ ◦◦ ◦◦ ◦◦ ◦◦ ◦◦ ◦◦ ◦◦◦◦ ◦◦ ◦◦ ◦◦ ◦◦ ◦◦ ◦◦
◦◦ ◦◦ ◦◦ ◦◦ ◦◦ ◦◦◦◦◦◦◦ ◦◦◦◦◦◦◦ ◦◦ ◦◦ ◦◦ ◦◦ ◦◦ ◦◦ ◦◦ ◦◦◦◦◦◦ ◦◦ ◦◦
◦◦ ◦◦ ◦◦ ◦◦◦◦ ◦◦ ◦◦ ◦◦ ◦◦ ◦◦◦◦◦◦◦◦ ◦◦ ◦◦◦◦ ◦◦ ◦◦ ◦◦ ◦◦ ◦◦
◦◦ ◦◦ ◦◦ ◦◦◦ ◦◦ ◦◦ ◦◦ ◦◦ ◦◦ ◦◦ ◦◦ ◦◦◦ ◦◦ ◦◦ ◦◦ ◦◦ ◦◦
◦◦◦◦◦◦ ◦◦ ◦◦ ◦◦◦◦◦◦◦ ◦◦ ◦◦ ◦◦ ◦◦ ◦◦ ◦◦ ◦◦◦◦◦◦ ◦◦◦◦◦◦◦◦ ◦◦◦◦◦◦
u n b r a n d e d ◦ s t a r t e r
Add your preferred tooling to any project, new or existing, using the package manager you already have.
Sixteen à la carte units, pinned for reproducibility, that merge into what's already there instead of overwriting it.
- Why unbranded
- Quickstart
- What You Can Install
- Beyond Day One
- Commands and Flags
- Non-Interactive Runs
- Preview a Run
- How It Works
- Philosophy
- Development
- License
Most scaffolders are a one-time event. create-next-app, create-t3-app, and the rest generate a fresh project on day zero and then they're done. The config they leave behind is yours to maintain by hand forever after, and if you're not starting from scratch, they don't help at all.
But most of the repos you touch already exist, and the tooling questions never really stop. unbranded is built for that. It runs on a brand-new directory or a repo with ten thousand commits, adds only the units you pick, and keeps earning its keep long after the first run.
- It works on repos that already exist. Point it at a live project and it augments in place, folding into your
package.jsonand config files rather than clobbering them. A real conflict stops for an overwrite-or-skip prompt with a diff. A dependency you've already pinned to something else is a conflict too, and it prompts the same way with both specs on screen. - It uses your package manager. npm, pnpm, yarn, or bun, detected from your lockfile. No tool forces its own on you.
- À la carte, not a monolith. Pick the units you want and a resolver pulls in whatever they depend on, showing you the full set before it writes anything. No eject, no all-or-nothing template.
- Reproducible by default. Every version is pinned;
--latestopts out per run, and any interactive run saves as a recipe to replay in CI or on the next project. Automation re-checks the pins weekly withunbranded outdatedand opens one bump PR that merges itself when CI is green, with per-unit PRs on demand when a week goes red, so reproducible never quietly goes stale. - It stays useful past day one. Every run records what it wrote, so
unbranded diffshows how far a project has drifted from its scaffold andunbranded doctoraudits any repo and names the exact unit that closes each gap.
The name is the point: no framework lock-in, no house brand.
Node 22 or newer, plus whichever package manager you already use: npm, pnpm, yarn, or bun.
npm create unbranded@latest # or: pnpm create unbranded · bun create unbrandedAny of those drops you into the interactive flow. npx unbranded is the same thing without the launcher:
npx unbrandedIn a hurry? Skip the picker and start from a shipped recipe:
npx unbranded --preset node-lib --pm pnpmRun it inside a directory that already has a package.json and it augments that project in place; run it anywhere else and it asks for a project name, then creates and enters a new directory. Either way it detects your package manager from the lockfile (pnpm → bun → yarn → npm) and asks what to install.
A run looks roughly like this:
┌ unbranded
│
● Target: ~/code/my-app (augment)
● Package manager: pnpm
│
◇ What do you want to install?
│ [Foundation] EditorConfig
│ [Foundation] Node version pin
│ [Linting] oxlint + oxfmt
│ [TypeScript] TypeScript
│ …
│
□ Plan
│ • oxlint + oxfmt
│ • TypeScript (auto)
│ 2 units · 4 files · 4 deps · install via pnpm
│
◇ Apply? Yes
│
● Files: 4 written, 0 overwritten, 0 merged, 0 appended, 0 skipped.
│
○ Installing dependencies via pnpm
│
└ Done.
In the picker, a row with options shows each option's current value on its tag. ←/→ cycles the row's first option, and Shift+←/→ cycles its second, so on the TypeScript row ←/→ changes the compiler line and Shift+←/→ changes the lib types.
Sixteen units, grouped by category. Selecting one can pull in others: either lint unit (oxlint + oxfmt or ESLint) implies TypeScript, PostCSS and shadcn/ui imply Tailwind, and the GitHub Actions unit implies the lint and test units its workflow runs. Auto-added units are tagged (auto) in the plan.
Foundation
- EditorConfig — cross-editor whitespace and charset rules.
- Git attributes — normalizes line endings to LF and marks the common binaries so diffs and merges stay clean.
- Node version pin — writes
.nvmrc,engines.node, and the CorepackpackageManagerfield from your running toolchain, so all three agree instead of drifting apart.
Linting
- oxlint + oxfmt—the default. oxlint and oxfmt in a base, react, or next flavor. Base is TypeScript-only, for Node libraries and CLIs; react adds React and jsx-a11y rules, and next adds Next.js rules on top. Both tools ship as native binaries, so there are no plugins to install. The unit also turns on type-aware rules, described below the ESLint entry. Either lint unit works on TypeScript 6 or 7 (see the TypeScript unit).
- ESLint—the alternative, for projects that want
@antfu/eslint-configin a base, react, or next flavor. Base is TypeScript-only; react and next layer on React, hooks, and a strict jsx-a11y block, and next adds Next's performance rules. Tabs, single quotes, arrow parens, with dprint formatting the non-code files. typescript-eslint doesn't support TS 7 yet, so this unit needscore-typescript:6; a barecore-eslintselects it for you, andcore-typescript:7beside it exits 1. It doesn't run type-aware rules such asno-floating-promises; oxlint + oxfmt does. It excludes oxlint + oxfmt, so pick one of the two. To move an existingcore-eslintproject over, follow docs/switching-to-oxc.md.
The oxlint + oxfmt unit installs oxlint-tsgolint and turns on type-aware rules, such as no-floating-promises, on both TypeScript lines. To turn them off, delete "typeAware": true from .oxlintrc.json. tsgolint type-checks with its own bundled TypeScript 7 on both lines and ignores the project's typescript. On TypeScript 6, this breaks lint when the tsconfig sets an option TypeScript 7 removed, such as baseUrl, target: "es5", moduleResolution: "node10", or esModuleInterop: false. pnpm lint then exits 1 with a typescript(tsconfig-error) naming the option, even though TypeScript 6 accepts it under "ignoreDeprecations": "6.0". The shipped tsconfigs set none of these, so if you add one, remove it or turn the type-aware rules off. Files outside tsconfig.json's include still get linted, but with less type information, so some findings differ or don't appear. With opt-monorepo, a package that has no tsconfig.json of its own still passes pnpm lint when its code is clean, with less type information; adding a packages/<name>/tsconfig.json that extends ../../tsconfig.base.json gives it full types.
TypeScript
- TypeScript—the full strict suite plus
noUncheckedIndexedAccessand the rest. ThetypescriptLineoption picks the compiler line:core-typescript:6orcore-typescript:7, default 7. Either lint unit works on 6. ThetsFlavoroption picks the lib types:node, the default, loadsES2022alone, so code that touchesdocumentfails typecheck, anddomaddsDOMandDOM.Iterablefor browser code. Thenodeflavor also setsexactOptionalPropertyTypes, so passingundefinedto an optional property fails typecheck, and code says a value is absent by leaving its key out.domleaves the flag off. The node-lib and cli presets picknode, and next-app picksdom. To set both options inline, give both values in either order, as incore-typescript:6:dom. When a run names no flavor, it keeps the one.unbranded.jsonrecords. Without a recorded flavor, a project that already trackscore-typescriptor depends on React or Next getsdom, and anything else getsnode. Installs from before 3.0 had the DOM libs, andupdateand later runs keep them until you choosenode, for example with--units core-typescript:node.
Styles
- Stylelint — standard config with a Tailwind-aware preset.
- Tailwind v4 — no JS config; add
@import "tailwindcss";to your stylesheet. - PostCSS (non-Vite Tailwind) — a one-line
postcss.config.mjsthat loads@tailwindcss/postcss. Next.js and other non-Vite bundlers pick up Tailwind v4 through that config. Vite projects (Astro, SvelteKit, Nuxt) use@tailwindcss/viteinstead; adding this unit on top of it processes Tailwind twice.
Testing
- Vitest — jsdom environment with the common excludes.
End-to-end
- Playwright + axe — mobile-first device matrix with
@axe-core/playwrightwired up.
UI
- shadcn/ui —
components.jsonplus thecn()utility atsrc/lib/utils.ts.
Git hooks
- Husky + lint-staged — a pre-commit hook that runs lint-staged on changed files. Yarn Berry Plug'n'Play is unsupported, because it has no
node_modules/.binfor the hook to resolvelint-stagedfrom.
Editor
- VS Code workspace —
settings.jsonmerged into whatever you already have, plus anextensions.jsongenerated from the units you picked. The formatter and fix-on-save action insettings.jsonmatch your lint unit: oxc's extension forcore-oxlint, ESLint's forcore-eslint. Switching lint units replaces those keys unless you changed them.
CI
- GitHub Actions — a workflow that runs install, lint, typecheck, and test on push and pull request.
Monorepo
- pnpm workspace + Turbo — workspace yaml with build-script approvals for esbuild/sharp/unrs-resolver (pnpm 10 and 11) and a turbo.json baseline.
Run unbranded list for the same catalog in your terminal, or unbranded list --json to hand it to other tooling.
Three shipped recipes bundle the common answers, and the interactive flow offers them as a starting point before the picker:
- node-lib—a typed, tested, linted Node library: strict TypeScript without the DOM libs, oxlint and oxfmt (base flavor), Vitest, pre-commit hooks, CI, and the editor/git hygiene units.
- next-app—everything node-lib has, plus the front-end stack: Tailwind v4, PostCSS, Stylelint, shadcn/ui, Playwright with axe, and a shared VS Code workspace. oxlint runs the next flavor, and TypeScript loads the DOM libs.
- cli—node-lib without the git hooks, for command-line tools. After scaffolding, point your bundler at a bin entry and add a shebang; the shipped tsconfig works as-is for a single-entry CLI.
--preset <name> behaves like --config pointed at the bundled file, with one twist: --units adds to a preset instead of replacing its list, because a preset is a starting point. Presets default to the safe run (no install, no overwrites, your existing dependency specs kept); pass --pm to install and --on-conflict overwrite to clobber. The files live in presets/ as plain recipe JSON, so they double as documentation.
The pieces that make unbranded worth keeping in a repo rather than running once and forgetting. Each is read-only unless noted, needs no TTY, and speaks --json, so it sits in CI as comfortably as at your prompt.
unbranded diffcompares your tracked files against the recorded state and labels each unchanged, user-modified, template-updated, or both. It exits non-zero on drift, so it drops straight into a CI check.--difffor the patch,--jsonfor tooling.unbranded doctoraudits any repo, whether unbranded scaffolded it or not: missing config, coexisting lockfiles, absent version pins, and more, each named with the unit or command that closes it. It writes nothing;--strictturns findings into a non-zero exit.unbranded doctor --fixhands the fixable findings to the apply pipeline, opening the picker with those units preselected (or applying them outright with--fix --yes). Findings no unit can close are printed as manual steps, never run.unbranded updatethree-way merges newer template versions into your tracked files against their recorded baseline: untouched files update silently, non-overlapping edits merge, and a real conflict asks per file.--strategy <ours|theirs|markers>answers globally for CI.unbranded outdatedgrades every manifest pin against the npm registry (patch, minor, major); a pin held to its major, like the TypeScript 6 line, is graded against the newest release in that line. It exits 0 by default so a report never fails a job;--strictgates on majors,--registrypoints at a mirror.unbranded remove <unit>backs a unit out: it deletes the unit's unmodified files, drops the package.json entries no remaining unit still claims, and refuses to strand a dependent unless you pass--cascade.--dry-runpreviews the whole thing.
Every run records what it wrote in .unbranded.json plus an .unbranded/ sidecar of byte-exact baselines (the merge base update needs), so commit both. Doctor findings are opinions, and some won't apply to your repo; accept one by adding its id to a doctor.ignore array in the state file. The full non-interactive contract, the JSON schemas under schemas/, and the agent loop end to end live in AGENTS.md and docs/agent-cookbook.md. Authoring your own units and using them alongside the built-ins is covered in docs/authoring-units.md.
unbranded interactive prompt flow (the default)
unbranded list print the unit catalog
unbranded diff report drift against the recorded state
unbranded doctor audit the current repo
unbranded update three-way merge newer templates into tracked files
unbranded outdated grade manifest pins against the npm registry
unbranded remove <unit> back a tracked unit out
unbranded validate <path> check unit definitions against the published schema
The flags you'll reach for most, with unbranded --help for the full set:
| Flag | Does |
|---|---|
--config, -c <file> |
run a JSON recipe non-interactively |
--units <a,b,c> |
pick units inline, no recipe file |
--units-dir <dir> |
load custom unit definitions from a directory |
--pm <npm|pnpm|yarn|bun|null> |
set the package manager and skip detection; null writes the files and skips the install |
--yes |
apply without the confirm prompt (needs --units/--config) |
--dry-run |
resolve and report, write nothing |
--latest |
take newest versions, not pins; held pins keep their major |
--target <dir> |
act on <dir> instead of the current directory (scaffold, diff, doctor, remove, update) |
--help covers the rest, including --force, --json, --fix, --cascade, --strategy, and --registry.
For CI and reproducible setups, drive the whole flow from a recipe:
unbranded --config recipe.json{
"units": ["core-oxlint", "core-vitest"],
"pm": "pnpm",
"onConflict": "overwrite",
"postInstall": "all",
"projectName": "my-app"
}units, pm (or null to skip install), onConflict (which settles file and dependency version collisions alike), and postInstall are required; projectName only in new-project mode (a non-interactive run there without it exits 1), and an unknown unit id fails validation immediately. Config mode skips the Apply confirmation, and inline flags like --units/--pm override the matching recipe field when both are set.
Don't want to hand-write the JSON? Finish an interactive run and it offers to save your choices as recipe.json, so you explore once and replay everywhere. The full recipe schema and the exit-code contract (0 success, 1 any error or --strict gate, 130 for Ctrl-C at a prompt) are documented in AGENTS.md.
--dry-run resolves the whole plan and prints what each file would do, then stops before writing a byte or touching the package manager. It works with or without --config.
unbranded --dry-run
unbranded --dry-run --diff # add the unified patch for every file that would changeEvery file gets one verdict: would create, would merge, would append, identical, or conflict. The closing summary mirrors a real run's Files: line, reworded as Would:.
- Target detection. A
package.jsonin the current directory means augment mode; otherwise the CLI asks for a name and works in a new directory it creates. - Package manager detection. It walks up for a lockfile, then falls back to the
packageManagerfield, thennpm_config_user_agent, then a prompt. With nopackage.jsonat all, it writes files, skips install, and prints a next-steps block. - Selection and resolution. A category-grouped multiselect feeds a resolver that closes the set under
implies, validatesrequires, and fails fast on anexcludesviolation. - Guardrails. In a git repo with a dirty working tree it warns before writing anything, since a clean tree is your undo button (
git checkout .).--forceskips the check. - Apply. Existing files prompt for overwrite or skip with a colored diff, and a dependency whose spec differs from the manifest's pin prompts the same way. The run reports every conflict either way, so a pin that replaces yours is never silent. Keeping your own spec instead can leave it out of step with the sibling versions the manifest pins as a tested set. Structured units fold into
package.json,settings.json, and ignore files rather than overwriting, and the run records what landed in.unbranded.json. - Install and hooks. The detected package manager runs under a Ctrl-C trap, then per-unit post-install steps (like
husky, gated on a real.git/) prompt with sensible defaults. - Failed installs. An install that exits non-zero undoes the run: every file it changed goes back to its pre-run contents, every file it created is deleted, and the exit code is 1. An interactive run offers to keep the files instead, so you can fix the install by hand. The failure report names any lifecycle script the run added to
package.json, because aprepareleft behind by a half-finished run fires on your next install whether or not you remember it.
- oxlint + oxfmt by default. Both ship as native binaries with no plugin tree to install. @antfu's ESLint config stays available as the
core-eslintalternative, and it beatseslint-config-nextalone with uniform style, a11y, and formatting across every kind of project. - Tabs over spaces. I prefer tabs, so the shipped oxfmt and EditorConfig settings use them.
- Strict TypeScript is non-negotiable.
noUncheckedIndexedAccesscatches the bugs the basicstrictflag misses. .vscode/is committed, and there's a unit for it. If you work in VS Code, a clone should just work.
pnpm install
pnpm build # tsdown
pnpm test # vitest, unit
pnpm test:e2e # builds first, then the end-to-end suite
pnpm test:all # both suites
pnpm typecheck
pnpm lint # CI parity: CI=true pnpm lint
pnpm lint:tracked # what the pre-push hook runs: git-tracked files onlyCI runs all of these except lint:tracked on Node 22 and 24, across Ubuntu and Windows. AGENTS.md has the repo layout and the full non-interactive contract; docs/authoring-units.md covers authoring your own units.
The repo also doubles as a template if you'd rather hand-pick files than run the CLI. Run pnpm dlx degit kendrick/unbranded-starter new-project, then take what you want from the root configs and the opt-in/ directories.
MIT.