Skip to content

The hery CLI

The hery CLI is the only thing in this project that reads a blueprint. It is a build-time tool, not a runtime dependency of the generated application.

Command What it does
new <name> Scaffolds a fresh HeryJs project in its own directory.
create:blueprint <Name> Writes a blueprint from prompts or defaults.
generate <Name> Writes a full resource from that blueprint.
migrate --name <name> Runs prisma migrate dev, then emits and applies any missing row-level policy.
migrate:status Lists every migration as applied, pending, failed or missing.
migrate:down Prints the SQL that reverts the last migration.
migrate:fresh Drops everything and replays the history. Refuses in production.
db:seed Runs prisma/seed.ts; --fresh resets first.
db:diff Prints the SQL that would bring the database to prisma/schema/.
db:branch <action> [name] Creates, drops or lists throwaway copies of the database.
db:backup Dumps the database with pg_dump, keeping the newest N dumps.
db:backups Lists the dumps of this database.
db:restore <file> Restores a dump; refuses a database that has data without --force.
install [modules...] Installs optional modules.
uninstall <module> Removes a module and reverses what installing it did.
make:mail <Name> Writes a Mailable into src/functional/<domain>.
make:export <Name> Writes an Exportable into src/functional/<domain>.
make:job <Name> Writes a queued job, its name and its retry policy.
make:listener <Name> Writes an event listener.
make:scheduled-task <Name> Writes a cron task that records its runs.
module:list Lists the modules available to install.
module:monitoring Scaffolds Prometheus, Grafana and Loki.
search:reindex <Name> Rebuilds a resource’s search index from Postgres.
openapi Emits an OpenAPI 3.1 document from every routed blueprint.
openapi:client Emits a typed TypeScript client from the same blueprints.
secrets:key Prints a new 32-byte key for SECRETS_KEYS.
secrets:rotate Re-encrypts every encrypted column to the active key.
expose:list Prints the catalog of @ExposeActions.
expose:run <action> Runs one exposed action, --param name=value per field.
up Checks that local dependencies are ready.
doctor One command for environment, config and infra together.
env pull Writes the resolved variables into .env.
env run -- <command> Runs a command with the resolved variables injected.
lint Scores the project against conventions eslint and the architecture linter do not reach.
console Boots the app into a REPL.
hosts Adds the local hostname to your hosts file.
mcp:serve A read-only MCP server over stdio.
Command What it does
—————————– —————————————————————————————
new <name> Scaffolds a fresh HeryJs project in its own directory.
create:blueprint <Name> Writes a blueprint from prompts or defaults.
generate <Name> Writes a full resource from that blueprint.
migrate --name <name> Runs prisma migrate dev, then emits and applies any missing row-level policy.
install [modules...] Installs optional modules.
uninstall <module> Removes a module and reverses what installing it did.
make:mail <Name> Writes a Mailable into src/functional/<domain>.
make:export <Name> Writes an Exportable into src/functional/<domain>.
make:job <Name> Writes a queued job, its name and its retry policy.
make:listener <Name> Writes an event listener.
make:scheduled-task <Name> Writes a cron task that records its runs.
make:policy <Name> Writes a capability policy, admin only until you say otherwise.
make:seeder <Name> Writes a seeder exposed to expose:run and the mine, dev and test only.
make:driver <module> <Name> Writes a driver and its token module from the module’s own contract.
make:notification <Name> Writes a notification with one method per channel.
make:webhook-handler <Name> Writes a handler for one webhook source.
routes Prints every route with its capability and rate-limit bucket.
module:list Lists the modules available to install.
module:monitoring Scaffolds Prometheus, Grafana and Loki.
search:reindex <Name> Rebuilds a resource’s search index from Postgres.
openapi Emits an OpenAPI 3.1 document from every routed blueprint.
openapi:client Emits a typed TypeScript client from the same blueprints.
expose:list Prints the catalog of @ExposeActions.
expose:run <action> Runs one exposed action, --param name=value per field.
up Checks that local dependencies are ready.
doctor One command for environment, config and infra together.
env pull Writes the resolved variables into .env.
env run -- <command> Runs a command with the resolved variables injected.
lint Scores the project against conventions eslint and the architecture linter do not reach.
console Boots the app into a REPL.
hosts Adds the local hostname to your hosts file.
mcp:serve A read-only MCP server over stdio.

Scaffolds a fresh HeryJs project in ./<name>: the CLI, every module’s authoring package, the kernel (technical/), the always-on DX tools (devtools/), the modules wired in by default, and a rewritten package.json. It then runs git init and commits the result, and prints the commands to take from there:

Terminal window
pnpm dlx heryjs new my-app
cd my-app
cp .env.example .env
pnpm install
pnpm hery up --start
pnpm hery migrate --name init
pnpm start:dev

heryjs is the published package and new is the only command it carries — the rest of the CLI reads the project around it, so it travels into the project rather than being installed globally. From inside my-app every command is pnpm hery, at the version that project was created with.

hery up --start rather than docker compose up -d: the compose services publish ports Docker assigns, so several projects can run at once, and up --start writes the ones it got back into .env. Bringing compose up by hand leaves .env pointing at ports nothing listens on.

hery migrate rather than prisma migrate dev on purpose, and the scaffolded project prints it that way: it wraps Prisma’s own migration and then emits the row-level security policy for every tenant-scoped table, so the database boundary is created with the tables instead of remembered afterwards.

Examples and the doc site are deliberately not copied — this is the framework’s own demo, not part of what ships.

Terminal window
pnpm hery create:blueprint Task
# edit blueprints/task.yaml if needed
pnpm hery generate Task
pnpm hery migrate --name add_task

After this, Task is a normal NestJS module like any other. Nothing re-reads blueprints/task.yaml again — evolving the resource means editing the generated files directly, the same way you would for hand-written code.

Walks through interactive prompts (fields, permissions, whether to paginate and with which page sizes, sortable fields, filterable fields) and writes a YAML blueprint to blueprints/<name>.yaml. Pagination defaults to off — --yes writes a blueprint whose search route returns every match.

  • --yes skips the prompts and takes sensible defaults — useful in scripts, or when trying the generator out.
  • --all-options skips the prompts and writes a fully commented blueprint listing every field type, every permission preset and every other option the generator understands, mostly commented out. A quick way to see the whole menu before trimming it to what you need.

Every run also (re)writes blueprints/schema.json — a JSON Schema generated straight from the same zod schema hery generate parses a blueprint against, via zod’s own toJSONSchema — and stamps the new file with a # yaml-language-server: $schema=./schema.json modeline. An editor with the yaml-language-server extension picks that up on its own: autocompletion and inline validation for field types, permission presets and the rest, without opening a second file to check what is allowed.

Reads the blueprint and writes ten files into src/functional/<name>/. See What gets generated for what each one owns.

The argument is a name resolved under blueprints/, or a path to a YAML file if you would rather keep a blueprint next to whatever it produced:

Terminal window
pnpm hery generate Task
pnpm hery generate examples/blog-post.yaml

It also patches prisma/schema/ (the new model in app.prisma, its inverse relation on User in _hery/core.prisma), prisma.client.ts (adding the model to the tenant-scoped set) and audit-log.ts (adding it to the audited set), then prints the two manual steps it deliberately does not take for you: importing the module into src/app.module.ts, and running the migration.

  • --force overwrites an existing resource directory. Without it, generate refuses rather than clobbering code you own.
  • --graphql, --mcp, --live, --stream each add one more file, wiring the resource into the corresponding module. They require that module to be installed; the flag does not check.

Runs prisma migrate dev, then does the second half generate deliberately left undone: it reads the schema’s /// @hery.tenantScoped annotations, writes the ENABLE ROW LEVEL SECURITY migration for any tenant-scoped table that has no policy yet, and applies it. Writing that migration by hand is how the teams tables spent a month scoped by the extension with nothing behind them.

Installs optional modules à la carte or, with --all, the full package. hery module:list prints what is available. See The module system.

Terminal window
pnpm hery install storage mail
pnpm hery install --all

hery make:mail <Name> and hery make:export <Name>

Section titled “hery make:mail <Name> and hery make:export <Name>”

Where a module has a recurring per-use-case object, it gets a make: command. Both write one class into src/functional/<domain>/, next to the code that uses it, and both refuse to overwrite an existing file unless you pass --force:

Terminal window
pnpm hery make:mail WelcomeMail # src/functional/welcome/welcome-mail.ts
pnpm hery make:export TaskListExport -d task # src/functional/task/task-list-export.ts

The domain is derived from the name with a trailing mail or export stripped, and --domain overrides it. What gets generated implements a small interface — Mailable, Exportable — and never picks a driver, reads config, or names a format: that is the registry’s job, and keeping it out is what lets the same class survive a transport change. See Modules and drivers.

hery make:job, make:listener, make:scheduled-task

Section titled “hery make:job, make:listener, make:scheduled-task”

The same shape, for the three concern types the kernel already runs and nothing scaffolded:

Terminal window
pnpm hery make:job SendDigestJob # src/functional/send-digest/send-digest.ts
pnpm hery make:listener WelcomeListener # src/functional/welcome/welcome.ts
pnpm hery make:scheduled-task ExpireTokens -d auth

The kind’s suffix is stripped before anything is derived, so make:job SendDigestJob and make:job SendDigest produce the same file — SEND_DIGEST_JOB, SendDigestProcessor, and a domain of send-digest.

Each carries the part a copy-paste quietly drops:

  • a job ships a retry policy. Written by hand it inherits BullMQ’s defaults — one attempt, no backoff — and a job that fails once is simply gone.
  • a listener carries a stable name, because a queued job travels with it: renaming it while jobs are enqueued strands them.
  • a scheduled task goes through ScheduledTaskStore, so a task that stops firing is visible rather than merely silent.

Each also prints what is left to do — the payload, the event class, the cron expression, and the module to register it in. Generated code is yours from the moment it lands; these fill in the decisions, not the work.

make:policy, make:seeder, make:driver, make:notification, make:webhook-handler

Section titled “make:policy, make:seeder, make:driver, make:notification, make:webhook-handler”

Five more scaffolds on the same pattern: one file next to the code that uses it, --force to overwrite, next steps printed.

Terminal window
pnpm hery make:policy ManageInvoices # src/functional/manage-invoices/manage-invoices.policy.ts
pnpm hery make:seeder InvoiceSeeder # an @ExposeAction, so `expose:run invoice.seed` works
pnpm hery make:driver mail postmark # src/modules/mail/postmark-mail.driver.ts and .module.ts
pnpm hery make:notification OrderShipped # toMail, toSms, toPush
pnpm hery make:webhook-handler StripeWebhookHandler
  • a policy is adminAbility under a name, so a route cannot be guarded by a rule nobody wrote down.
  • a seeder is bounded (max: 500) and restricted to development and test, the same two rules agency.seed carries.
  • a driver is read from src/technical/<module>/<module>-driver.ts rather than from a table in the CLI: every method of the contract becomes a stub that rejects with “not implemented”, so an unfinished driver fails loudly instead of pretending to send. It also writes the @Global() module that provides the driverToken(<module>, <name>) the registry resolves.
  • a notification declares what it says per channel; delete the channels it has nothing to say on.
  • a webhook handler names its source and registers itself with the webhook processor, which hands it every event for that source inside the event’s tenant, before the admin notification goes out. A handler that throws fails the job, so the event is retried rather than marked processed.

Boots the application and prints what Nest’s router actually has wired:

Terminal window
METHOD PATH CAPABILITY RATE LIMIT
GET /blog-posts canViewAnyPost read
POST /webhooks/:id public (signed by its sender) exempt (sender is trusted)

--unguarded keeps only the routes with no capability or no rate-limit bucket, --json prints the rows. A route that is deliberately public or unthrottled shows its stated reason instead of a blank.

There is deliberately no module:add: hery install already does that.

migrate and migrate:down were the whole of it. The rest of what Laravel’s migrate:* and db:* commands do now exists under the same names:

Terminal window
pnpm hery migrate:status # applied / pending / failed / missing, per migration
pnpm hery migrate:fresh --seed # drop everything, replay, seed
pnpm hery db:seed --fresh # the same reset, seeding always
pnpm hery db:diff # SQL from the live database to prisma/schema/
pnpm hery db:branch create risky # a copy of the database to try a migration on
pnpm hery db:backup # a pg_dump in backups/, newest 7 kept
pnpm hery db:restore latest # refuses a database that already has data

migrate:status reads _prisma_migrations rather than guessing from the disk, which is how it can say missing for a migration that was applied and then deleted from the repository, and failed for one that stopped halfway. --check exits 1 unless everything is applied, for CI.

migrate:fresh and db:seed --fresh refuse when NODE_ENV is production, and --force does not change that. They also refuse a database that is not on this machine unless you pass --force: a command that deletes a database has to be harder to run by accident than on purpose.

db:diff is read-only. It prints what the next migrate would do; --check exits 1 when the database and the schema differ.

db:branch is Supabase branching for a local database. create <name> runs CREATE DATABASE ... TEMPLATE, so the copy has the schema and the rows, and prints the DATABASE_URL to export. Postgres cannot copy a database somebody is connected to, so a source with open connections is reported as that, not as a raw Postgres error. drop <name> can only drop a database this command made: the name is always <database>_branch_<name>, and a branch name is limited to lowercase letters, digits and underscores because it ends up in DDL, where parameters are not allowed.

Removes the module’s own npm dependencies (pnpm remove) and prints the rest of what to clean up by hand — the runtime files it copied in and the kernel files it patched. It only automates what is safe to automate outright: your own edits to those files since installing are exactly what “own your code” says the tool must never guess about and silently revert. Run hery module:list to see what’s installed. See The module system.

Rebuilds a resource’s search index from what is actually in Postgres — the source of truth stays the database, the index is a derived cache that can always be thrown away and rebuilt. Run it after installing a search engine module on a resource that already has data, or any time the index and the database might have drifted. See Full-text search.

The two CLI halves of the exposition registry: expose:list prints every @ExposeAction the app declares, expose:run runs one outside a request.

Terminal window
pnpm hery expose:run prune.run --param prune.run.model=BlogPost

There is no signed-in user at a terminal, so the capability check is skipped where the HTTP route enforces it — trust comes from having a shell on the machine, the same reasoning console --tenant relies on. The environment filter and the audit entry still apply. See Exposing an action to the mine.

Scaffolds Prometheus, Grafana and Loki as an opt-in local compose stack, wired to scrape the app’s /metrics. It writes the compose file and a Prometheus config, then prints the command to start them — it does not start anything itself.

/metrics is caller-authenticated like every other route, so the scrape carries an admin API key: the config reads it from monitoring/api-key, which the command creates with a placeholder. Mint a key with POST /api-keys, put it in that file on its own line, and keep the file out of version control. Until then every scrape gets a 401.

Despite the name it is a plain command, not a registry module: it does not appear in module:list and install --all does not cover it.

Checks that the things the app needs are actually reachable — Postgres, Valkey, and whether Prisma migrations are up to date — and exits non-zero if any of them is not, with a hint for each:

Terminal window
✔ PostgreSQL (localhost:32768)
✔ Valkey (localhost:32769)
✘ Prisma migrations — run "pnpm hery migrate --name <name>"

Reachable here means the service answered as itself, not that a socket opened: Postgres is asked for a select 1 and Valkey for a PING. Since the compose files let Docker pick the host port, recreating the containers can hand Postgres the port .env still has under REDIS_URL and the other way round — two services up, two URLs pointing at the wrong one, and a connection check would call that healthy. This one says which service failed to answer and tells you to run --start:

Terminal window
✘ PostgreSQL (localhost:32769) — something is listening there, but it does not answer as PostgreSQL — was the container recreated on a new port? run "pnpm hery up --start"

--start brings the compose services up first and then does something more interesting: it reads back the ports Docker actually assigned and writes them into .env. The compose files publish container ports without fixing a host port, so several projects can run side by side without colliding, and hery up --start is what reconciles that with your configuration. It resolves DATABASE_URL and REDIS_URL, plus ELASTICSEARCH_URL or MEILISEARCH_URL if the matching search module is installed.

hery down puts the running application in maintenance mode: every request answers 503 with a Retry-After until hery up --lift ends it. --secret lets requests carrying a secret through, --retry <seconds> sets the Retry-After, and --message <text> sets what the 503 says. The switch lives in Redis, so it reaches every instance at once.

Everything hery up checks, plus the two config files a broken value in either would otherwise crash the whole CLI on: environment variables (parsed against the same zod schema env.ts builds at startup) and hery.config.ts (loaded for real, then checked for one thing loading it does not catch on its own — that search.default actually names a key in search.engines).

Terminal window
✔ Environment variables
✔ hery.config.ts
✔ PostgreSQL (localhost:32769)
✔ Valkey (localhost:32768)
✔ Prisma migrations

Both config checks go through require() inside a try/catch rather than a normal import, on purpose: env.ts and hery-config.ts both validate at import time and throw if the result is invalid, which is exactly the failure doctor exists to turn into a diagnosis instead of a crash.

The environment schema in env.ts is split into two halves. The server half (env) never leaves the process; the public half (publicEnv) is the short list of variables the admin dashboard’s browser bundle is allowed to read, declared once so a typo in a name fails at startup instead of quietly shipping http://localhost:3000 to production. no-server-env-in-client (part of hery lint) enforces the other direction: a client-side read of anything not in that list is a critical violation, because Astro inlines whatever it reads into the bundle it sends to the browser.

Where the values themselves come from is a separate question, handled by a small contract rather than by wiring a provider into the app:

Terminal window
interface EnvSource {
name: string;
load(): Promise<Record<string, string>>;
}

The only implementation shipped is .env itself. env pull resolves the configured source and writes the result into .env; env run -- <command> resolves it and runs a command with the values injected, without ever writing them to disk:

Terminal window
pnpm hery env pull
pnpm hery env run -- pnpm start:dev

This is deliberately a CLI-only concern. env.ts reads process.env once, synchronously, at import time, and the rest of technical/ depends on that as a plain value — making it async to accommodate a remote secrets manager (Infisical, Doppler, Vault, or one a team builds itself) would contaminate every module that reads env for a need that exists only at startup. Implementing EnvSource against a real provider and pointing dotEnvSource at it instead is enough to swap one in; nothing downstream of .env changes.

Scores the project out of 100 against conventions eslint and the architecture linter do not reach — forbidden patterns (any, a controller importing Prisma directly) and the shape a generated resource is supposed to keep (every file hery generate writes, present). A fresh install is never going to start at 100 for a codebase that predates the rule, so a baseline file grandfathers what already exists:

Terminal window
pnpm hery lint --write-baseline # snapshot today's violations as known debt
pnpm hery lint --min-score 100 # fail only on anything new

The baseline (.hery/lint-baseline.json by default, override with --baseline <path>) keys each grandfathered violation by the rule and a hash of the exact file content it was recorded against — never by path. Rename the file and the debt follows it; touch even one line and the file’s violations count as new again. That is deliberate: a baseline immune to edits would let someone change a violating line for an unrelated reason and walk away having silently re-endorsed it.

This framework’s own baseline is empty, and CI runs --min-score 100 against it. There is no known debt left to grandfather here, so the next violation introduced fails the build on the commit that introduces it.

--format json prints { score, grandfathered, violations } instead of the text report, for a script or an agent to act on.

Boots the real application into a REPL with the DI container and the tenant-scoped Prisma client. --tenant <id> picks the tenant the whole session runs inside. See Developer tooling.

Adds heryjs.local (or whatever HERYJS_DOMAIN is set to) to your system hosts file, pointing at 127.0.0.1, so the app is reachable by name instead of by port. It shows the exact line it will add and asks before touching anything, then elevates — sudo on Unix, an admin prompt on Windows. If the entry is already there it says so and does nothing.

Starts a read-only MCP server over stdio, for editors and agents that want to introspect what exists in the project. Two tools:

  • list_resources — the resources actually generated under src/functional/.
  • describe_resource(name) — that resource’s routes (HTTP method, path, the capability guarding it) and its fields, read straight from the real *.controller.ts and prisma/schema/.

It never reads a blueprint file. A blueprint is a one-time input to generate, not a live source of truth — the generated code is.

This is the read-only surface, needs no running app and no credentials. For an agent that should be able to use the application — with a real session and real capability checks — install the MCP module instead.