Quarry CLI
Quarry is Stratal’s built-in CLI framework, inspired by Laravel Artisan. It lets you define custom commands that run against your full application, with access to dependency injection, services, and Cloudflare bindings. Commands extend the Command base class from stratal/quarry.
The command entry point
Section titled “The command entry point”Quarry loads your application through a dedicated entry file at src/quarry.ts, kept separate from your Worker entry (src/index.ts). Create it once and export a QuarryRunner:
import { QuarryRunner } from 'stratal/quarry/runner'import { AppModule } from './app.module'
export default QuarryRunner.run({ imports: [AppModule],})Running commands
Section titled “Running commands”npx quarry <command> [arguments] [options]By default, Quarry uses ./src/quarry.ts as the application entry point. To use a different entry file, pass it as the first argument:
npx quarry ./src/other-entry.ts <command> [arguments] [options]Targeting a Wrangler environment
Section titled “Targeting a Wrangler environment”Use --env <name> (or -e <name>) to target a wrangler.jsonc environment such as env.staging or env.production. Quarry loads that environment’s bindings and vars before running the command. The flag is position-tolerant, so it works before or after the entry path:
npx quarry --env staging route:listnpx quarry -e production db:seednpx quarry ./custom/entry.ts --env staging route:listOmit the flag to use the top-level (default) Wrangler config.
Creating a command
Section titled “Creating a command”Extend Command, define a static command signature and description, then implement handle():
import { Command } from 'stratal/quarry'import { inject } from 'stratal/di'import { TaskService } from '../services/task.service'
export class AddTaskCommand extends Command { static command = 'task:add {title : The task title} {--p|priority= : Task priority (low, normal, high)}' static description = 'Add a new task'
constructor( @inject(TaskService) private readonly tasks: TaskService, ) { super() }
async handle(): Promise<number | undefined> { const title = this.string('title') const priority = this.string('priority') || 'normal'
const task = await this.tasks.add(title, priority)
this.success(`Task #${task.id} created: "${task.title}" [${task.priority}]`) }}The handle() method contains your command’s logic. Return a number to set the exit code, or undefined (or nothing) for exit code 0.
Command signatures
Section titled “Command signatures”Command signatures use a Laravel-style syntax to define the command name, arguments, and options in a single string.
Command naming
Section titled “Command naming”| Style | Signature | Invocation |
|---|---|---|
| Flat | 'greet' | npx quarry greet |
| Namespaced | 'task:add' | npx quarry task:add |
| Subcommand | 'task add' | npx quarry task add |
Arguments
Section titled “Arguments”| Syntax | Description |
|---|---|
{name} | Required argument |
{name?} | Optional argument |
{name=default} | Argument with default value |
{name*} | Array/variadic argument |
{name : description} | Argument with description |
Options
Section titled “Options”| Syntax | Description |
|---|---|
{--flag} | Boolean flag |
{--name=} | Option that accepts a value |
{--name=default} | Option with default value |
{--name=*} | Array option (multiple values) |
{--A|name} | Option with single-char alias |
{--name= : description} | Option with description |
Input accessors
Section titled “Input accessors”Use these methods inside handle() to retrieve parsed input values:
| Method | Return type | Description |
|---|---|---|
this.string(name) | string | Get a string input (empty string if missing) |
this.boolean(name) | boolean | Get a boolean input (false if missing) |
this.number(name) | number | Get a numeric input (coerces strings, 0 if missing) |
this.array(name) | string[] | Get an array input (empty array if missing) |
this.input<T>(name) | T | Get an input with a generic type |
Output helpers
Section titled “Output helpers”Use these methods inside handle() to write output:
| Method | Description |
|---|---|
this.info(message) | Write an informational message |
this.success(message) | Write a success message |
this.warn(message) | Write a warning message (prefixed with Warning:) |
this.error(message) | Write an error message to the error stream |
this.line(message?) | Write a plain line (or empty line if no argument) |
this.table(headers, rows) | Write a formatted table |
this.fail(message, exitCode?) | Write an error and set exit code (default 1) |
Table example
Section titled “Table example”import { Command } from 'stratal/quarry'import { inject } from 'stratal/di'import { TaskService } from '../services/task.service'
export class ListTasksCommand extends Command { static command = 'task:list {--s|status= : Filter by status (pending, done)}' static description = 'List all tasks'
constructor( @inject(TaskService) private readonly tasks: TaskService, ) { super() }
async handle(): Promise<number | undefined> { const status = this.string('status') let tasks = await this.tasks.list()
if (status) { tasks = tasks.filter((t) => t.status === status) }
if (tasks.length === 0) { this.info('No tasks found.') return }
this.table( ['ID', 'Title', 'Priority', 'Status', 'Tags'], tasks.map((t) => [ String(t.id), t.title, t.priority, t.status, t.tags.join(', ') || '-', ]), ) }}Registering commands
Section titled “Registering commands”Add command classes to a module’s providers array. Quarry automatically discovers all Command subclasses in the module tree:
import { Module } from 'stratal/module'import { AddTaskCommand } from './commands/add-task.command'import { ListTasksCommand } from './commands/list-tasks.command'import { TaskService } from './services/task.service'
@Module({ providers: [ TaskService, AddTaskCommand, ListTasksCommand, ],})export class AppModule {}Command chaining
Section titled “Command chaining”Call another command from within a command using this.call():
async handle(): Promise<number | undefined> { // Call another command with arguments and options const result = await this.call('task:list', { status: 'pending' })
if (result.exitCode !== 0) { this.error('Failed to list tasks') }}Aliases
Section titled “Aliases”Define alternative names for a command using the static aliases property:
export class ListTasksCommand extends Command { static command = 'task:list' static description = 'List all tasks' static aliases = ['tasks', 'task:ls']
async handle(): Promise<number | undefined> { // ... }}Built-in commands
Section titled “Built-in commands”Stratal ships with built-in Quarry commands. Each command is documented in detail on its respective page.
General
Section titled “General”| Command | Description | Docs |
|---|---|---|
help | Show help or list all commands | - |
route:list | List all registered routes | Controllers and Routing |
route:types | Generate TypeScript types for named routes | Controllers and Routing |
event:list | List all registered event listeners | Events |
schedule:list | List all registered cron jobs | Cron Jobs |
Queues
Section titled “Queues”| Command | Description | Docs |
|---|---|---|
queue:list | List all registered queue consumers | Queues |
queue:failed | List failed queue jobs | Queues |
queue:retry | Retry failed queue jobs | Queues |
queue:purge | Delete failed queue jobs without retrying | Queues |
Internationalization
Section titled “Internationalization”| Command | Description | Docs |
|---|---|---|
i18n:check | Audit locales for missing or extra keys | I18n |
i18n:stats | Show translation coverage per locale | I18n |
i18n:list | List message keys with per-locale coverage | I18n |
i18n:search | Search message keys or values | I18n |
i18n:namespaces | List namespaces with key counts | I18n |
i18n:duplicates | Find keys sharing identical values | I18n |
API and MCP
Section titled “API and MCP”| Command | Description | Docs |
|---|---|---|
api | Call an API route directly | Controllers and Routing |
mcp:serve | Start MCP stdio server exposing routes as tools | AI Integration |
mcp:tools | List API routes exposed as MCP tools | AI Integration |
Seeders
Section titled “Seeders”| Command | Description | Docs |
|---|---|---|
db:seed | Run database seeders | Seeders |
db:seed:list | List available database seeders | Seeders |
Database (@stratal/framework)
Section titled “Database (@stratal/framework)”| Command | Description | Docs |
|---|---|---|
db:generate | Generate ZenStack ORM client | Database |
db:pull | Introspect database and generate schema | Database |
db:push | Push schema changes to database | Database |
migrate:dev | Create and apply a migration | Database |
migrate:deploy | Deploy pending migrations | Database |
migrate:reset | Reset database | Database |
migrate:status | Check migration status | Database |
Managing queues
Section titled “Managing queues”Quarry includes commands to inspect queue consumers and work with the failed-job store.
queue:list prints every registered consumer alongside the message types it handles:
npx quarry queue:listqueue:failed lists jobs that exhausted their retries and landed in the failed-job store. It accepts two options:
| Option | Description |
|---|---|
--queue=<name> | Filter results to a single queue |
--limit=<n> | Maximum number of jobs to return (default 50) |
npx quarry queue:failednpx quarry queue:failed --queue=NOTIFICATIONS_QUEUEnpx quarry queue:failed --limit=100The output table includes each job’s ID, queue, message type, consumer, attempt count, and the time it failed. The job ID is what you pass to queue:retry and queue:purge.
queue:retry re-sends failed jobs back onto their original queue. Pass a single message ID, or use --all to retry every failed job. You must provide one or the other:
| Argument or option | Description |
|---|---|
id | Optional message ID to retry |
--all | Retry all failed jobs |
--queue=<name> | When used with --all, only retry jobs from this queue |
npx quarry queue:retry 0c1d2e3f-... # retry one job by IDnpx quarry queue:retry --all # retry every failed jobnpx quarry queue:retry --all --queue=EMAIL_QUEUEqueue:purge deletes failed jobs from the store without re-sending them. It mirrors the arguments of queue:retry:
| Argument or option | Description |
|---|---|
id | Optional message ID to purge |
--all | Purge all failed jobs |
--queue=<name> | When used with --all, only purge jobs from this queue |
npx quarry queue:purge 0c1d2e3f-... # delete one job by IDnpx quarry queue:purge --all # delete every failed jobnpx quarry queue:purge --all --queue=EMAIL_QUEUEInspecting schedules
Section titled “Inspecting schedules”schedule:list prints every registered cron job grouped by its schedule expression, so you can confirm your jobs match the triggers declared in wrangler.jsonc:
npx quarry schedule:listThe output is a table of schedule expression and job class. If no cron jobs are registered, it reports No cron jobs found.
Serving an MCP server
Section titled “Serving an MCP server”mcp:serve starts a Model Context Protocol server over stdio that exposes your API routes as tools for AI agents. Each OpenAPI route becomes a tool, and the full OpenAPI spec is registered as an MCP resource.
npx quarry mcp:serveBy default, tool calls are dispatched in-process through your app. The following options tune what is exposed and where requests go:
| Option | Description |
|---|---|
--url=<base-url> | Dispatch requests to an external URL instead of the in-process app |
--header=<Key:Value> | Add a header to dispatched requests (repeatable) |
--tag=<tag> | Only expose routes carrying these OpenAPI tags (repeatable) |
--path=<prefix> | Only expose routes matching this path prefix |
# Expose only routes tagged "Notes"npx quarry mcp:serve --tag=Notes
# Expose routes under /api/v1 and dispatch to a running servernpx quarry mcp:serve --path=/api/v1 --url=http://localhost:8787To preview which routes would be exposed without starting the server, use mcp:tools, which accepts the same --tag and --path filters:
npx quarry mcp:toolsnpx quarry mcp:tools --tag=Notes --path=/api/v1See AI Integration for the full MCP workflow.
Auditing translations
Section titled “Auditing translations”Quarry ships a set of i18n:* commands for inspecting translation coverage. The base locale is always en.
# Audit non-en locales for missing or extra keys (exit code 1 on issues, CI-friendly)npx quarry i18n:checknpx quarry i18n:check --locale=fr --prefix=common
# Coverage dashboard per localenpx quarry i18n:stats
# List keys with per-locale coverage; --values shows translated stringsnpx quarry i18n:list --locale=fr --values
# Search keys or values by substring; --keys-only skips value matchingnpx quarry i18n:search email --keys-only
# Show namespaces with key counts; --depth drills into sub-namespacesnpx quarry i18n:namespaces --depth=2
# Find keys that share identical translation valuesnpx quarry i18n:duplicatesSee I18n for the translation system these commands operate on.
How it works
Section titled “How it works”When you run npx quarry, the CLI:
- Uses Wrangler’s
getPlatformProxy()to obtain your Cloudflare bindings (D1, KV, R2, etc.) locally - Imports your application entry file to bootstrap the full module tree
- Discovers all
Commandsubclasses registered as providers - Parses command signatures and delegates to the matched command’s
handle()method
This means your commands have full access to dependency injection, services, and Cloudflare bindings, just like your HTTP handlers.
Testing commands
Section titled “Testing commands”Use TestingModule.quarry() to test commands with a fluent API:
import { describe, it } from 'vitest'import { Test } from '@stratal/testing'import { AppModule } from '../src/app.module'
describe('AddTaskCommand', () => { it('creates a task', async () => { const module = await Test.createTestingModule({ imports: [AppModule], }).compile()
const result = await module .quarry('task:add') .withInput({ title: 'Buy milk', priority: 'high' }) .run()
result.assertSuccessful() result.assertOutputContains('Task #') })
it('fails without a title', async () => { const module = await Test.createTestingModule({ imports: [AppModule], }).compile()
const result = await module .quarry('task:add') .withInput({}) .run()
result.assertFailed() })})Assertion methods
Section titled “Assertion methods”The TestCommandResult object returned by .run() provides these assertion methods:
| Method | Description |
|---|---|
assertSuccessful() | Assert exit code is 0 and no errors |
assertFailed(exitCode?) | Assert non-zero exit code (optionally a specific code) |
assertExitCode(code) | Assert a specific exit code |
assertOutputContains(text) | Assert output contains the given text |
assertOutputMissing(text) | Assert output does not contain the given text |
assertErrorContains(text) | Assert error output contains the given text |
assertErrorMissing(text) | Assert error output does not contain the given text |
You can also access result.exitCode, result.output, and result.errors directly for custom assertions.
Next steps
Section titled “Next steps”- Seeders for populating your database via CLI commands.
- AI Integration for exposing your API as MCP tools.
- Dependency Injection for understanding how providers work.
- Testing Module for the full
TestingModuleAPI.