CLI
cmdk-engine scans your route files and writes a sitemap: a file listing every route with a label, keywords and group. Use it when your routes live in files, as in Next.js, or to keep a committed list of routes in sync in CI. It needs Node.js 20 or later.
npx cmdk-engine init # create cmdk-engine.config.ts
npx cmdk-engine scan # scan routes and write the sitemap
npx cmdk-engine validate # check the config fileFrameworks
| Framework | What it scans | Default routes directory |
|---|---|---|
| react-router | Source files, for route objects and <Route> elements with a string path, with the label, keywords and group of their own handle.command. Relative child paths are joined to their parent route in the same file. Routes with a :param are skipped unless includeDynamic or their own handle.command keeps them, and catch-all routes are always skipped. | src/routes if it exists, otherwise src |
| nextjs-app | Each page file under the App Router. Route groups such as (auth) and @slot folders add no segment. Private _folders, intercepting (.) routes and api/ are skipped. [[...slug]] gives its parent's URL, and other dynamic segments need includeDynamic. | src/app if it exists, otherwise app |
| nextjs-pages | Each file under the Pages Router, except files that start with _ (such as _app) and the top-level api/ directory. Dynamic segments need includeDynamic. | src/pages if it exists, otherwise pages |
When you do not name a framework, a next.config file means Next.js (the App Router if an app or src/app directory exists, otherwise the Pages Router). Anything else is treated as React Router.
Commands
cmdk-engine init
Creates a config file from a template. It refuses to overwrite an existing one unless you pass --force.
| Option | Default | Description |
|---|---|---|
| -c, --config <path> | cmdk-engine.config.ts | Where to create the file. |
| -f, --force | off | Overwrite an existing config file. |
cmdk-engine scan
Scans the routes and writes the sitemap. It stops with an error, and leaves the existing output alone, when it finds no routes: a mistyped directory or framework is the usual cause.
| Option | Default | Description |
|---|---|---|
| -c, --config <path> | cmdk-engine.config.ts | The config file. Not finding one you named is an error; the default is optional. |
| -o, --output <path> | the config's output, else src/generated/command-routes.json | Where to write the sitemap. |
| -f, --format <format> | json | json or ts. With ts the file is a module that exports commandRoutes. |
| --framework <name> | auto-detected | react-router, nextjs-app or nextjs-pages. |
| --routes-dir <dir> | per framework | The directory to scan. |
| --include-dynamic [names...] New in 0.6 | off | Keep routes with these :param segments, for example locale(every :param when no name is given). Names can be separated by spaces or commas. The flag overrides the config value. |
| --allow-empty | off | Write the sitemap even when no routes are found. |
| --no-default-exclude | off | Keep the routes that are normally left out (below). |
cmdk-engine validate
Checks the config file. It prints Config is valid., or lists the problems and exits with status 1. It also checks that every exclude entry is a string or a RegExp, and that includeDynamic is true, false or an array of names. It takes -c, --config <path>.
Config file
The config file is optional. Without one, everything is detected. Wrap it in defineConfig to type-check it; the type is CmdkEngineConfig, which is not the provider's CommandEngineConfig.
import { defineConfig } from 'cmdk-engine'
export default defineConfig({
framework: 'react-router',
routesDir: './src/routes',
output: './src/generated/command-routes.json',
overrides: {
'/billing/overview': { keywords: ['money', 'payment'], group: 'Billing' },
},
exclude: ['/admin/*', '/internal'],
})| Field | Description |
|---|---|
| framework | react-router, nextjs-app or nextjs-pages. Detected when omitted. |
| routesDir | The directory to scan. |
| output | Where to write the sitemap. |
| overrides | Per-route changes, keyed by path. Only label, keywords (added to the route's own) and group are applied; other fields are ignored. |
| exclude | Routes to leave out, on top of the defaults. Each entry is an exact string, a glob or a RegExp. In a glob, * matches within one path segment and ** across segments. Excluding a path also excludes the paths below it, and a trailing /* also matches the base: /admin/* excludes /admin and everything under it. /admin* also matches /administration. |
| includeDynamic New in 0.6 | true, false or an array of :param names. Keeps routes with those segments, such as everything under a Next.js [locale] folder: ['locale'] gives /:locale/billing. true keeps every :param route, and catch-all segments are never kept. Default false. The --include-dynamic flag overrides it. See Dynamic routes below. |
| synonyms | Checked by validate but not used by scan. Put synonyms in the provider's config.synonyms. |
A .js, .mjs or .cjs config is imported, so it runs, and a .json config is read as data. A .ts config is read without running it, so its values must be static: strings, numbers, booleans, arrays, objects, RegExp literals and top-level consts (and spreads of them), with as const and satisfies allowed. For computed values such as process.env, use cmdk-engine.config.mjs. A config the CLI cannot read fails scan and validate with exit status 1 and names the line.
Output
The sitemap lists the routes sorted by path, and each carries the file it came from. With the JSON format, a scan that finds the same routes as the file already on disk keeps that file's generatedAt, so a committed sitemap only changes when your routes do.
{
"version": 1,
"generatedAt": "2026-01-01T00:00:00.000Z",
"framework": "react-router",
"routes": [
{
"id": "billing--overview",
"path": "/billing/overview",
"label": "Billing Dashboard",
"keywords": ["billing", "overview", "money", "payment"],
"group": "Billing",
"source": "src/routes/routes.tsx"
},
{
"id": "settings--team",
"path": "/settings/team",
"label": "Team",
"keywords": ["settings", "team", "people", "members"],
"group": "Settings",
"source": "src/routes/routes.tsx"
}
]
}Ids keep letters, digits and -, so /billing/overview gives billing--overview. When two routes would share an id (/a_b and /ab both give ab), the scan keeps it for the last of them in path order, which keeps its frecency and Recent history, gives the others ab-2, ab-3 and so on, and prints a warning for each.
Use the output
sitemapToCommands from cmdk-engine/adapters/sitemap turns the sitemap into commands. Register them once, at the app level:
import { useCommandRegister } from 'cmdk-engine/react'
import { sitemapToCommands } from 'cmdk-engine/adapters/sitemap'
import sitemap from './generated/command-routes.json'
const routeCommands = sitemapToCommands(sitemap)
function RouteCommands() {
useCommandRegister(routeCommands)
return null
}Each route becomes { id, label, keywords, group, href }, and selecting one calls your onNavigate. Commit the JSON, or run the scan in a prebuild script. The adapter is described on the Adapters page.
Dynamic routes and [locale] New in 0.6
A command needs a real URL, so the scan skips routes with a :param (/users/:id, app/[id]/page.tsx). To keep routes under a segment you can fill at runtime, such as a Next.js [locale] folder, name it in includeDynamic or pass --include-dynamic locale. app/[locale]/billing/page.tsx then becomes /:locale/billing, which params fills:
npx cmdk-engine scan --include-dynamic localeimport { useMemo } from 'react'
import { useCommandRegister } from 'cmdk-engine/react'
import { sitemapToCommands } from 'cmdk-engine/adapters/sitemap'
import sitemap from './generated/command-routes.json'
export function LocaleRouteCommands({ locale }: { locale: string }) {
const commands = useMemo(() => sitemapToCommands(sitemap, { params: { locale } }), [locale])
useCommandRegister(commands)
return null
}paramsvalues are inserted as given, and''drops the segment (a default locale served without a prefix).- Ids keep the placeholder (
locale--billing), so frecency is shared across locales. includeDynamic: truekeeps every:paramroute. A route whose param you do not fill is skipped, and catch-alls ([...slug],/docs/*) are never kept.- A React Router route with its own
handle.commandis kept, as with the runtimescanRoutes.
What the scan reads
The scan reads your files without running them.
- React Router: route objects with a string
path(as passed tocreateBrowserRouter) and<Route path="...">elements, with thelabel,keywordsandgroupof their ownhandle.command. Relative child paths are joined to their parent route in the same file (childrenarrays and nested<Route>s). It does not read paths built at runtime, routes imported from another file, index routes, or ahandlereturned bylazy(). - Next.js:
app/**/page.*(nextjs-app) andpages/**(nextjs-pages). Route groups and@slotfolders add no segment. Private_folders, intercepting(.)routes andapi/are skipped.[[...slug]]gives its parent's URL; other dynamic routes needincludeDynamic.
Routes that are left out
The scanner skips routes that are almost never useful as commands. This is the same list the runtime React Router adapter uses:
- Auth routes:
/login,/logout,/signin,/signout,/signup,/register,/forgot-password,/reset-passwordand/verify-email. - Callbacks:
/oauth/callback,/auth/callbackand/callback. - Error pages:
/404,/500,/errorand/not-found.
Pass --no-default-exclude to keep them, and use exclude in the config to choose your own.
In CI and on commit
Run the scan in CI and fail when the committed sitemap is out of date. Because a scan of unchanged routes leaves the file untouched, git diff is a reliable check:
- run: npx cmdk-engine validate
- run: npx cmdk-engine scan
- run: git diff --exit-code src/generated/command-routes.jsonTo regenerate it on every commit with husky 9, run npx husky init, then put this in .husky/pre-commit:
npx cmdk-engine scan && git add src/generated/command-routes.jsonWith lint-staged, use a function task. A plain command string would get the staged file names appended, and scan takes no file arguments:
export default {
'src/routes/**/*.{ts,tsx}': () => [
'npx cmdk-engine scan',
'git add src/generated/command-routes.json',
],
}