Skip to content

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.

Terminal window
npx quarry <command> [arguments] [options]

By default, Quarry uses ./src/index.ts as the application entry point. To use a different entry file, pass it as the first argument:

Terminal window
npx quarry ./src/other-entry.ts <command> [arguments] [options]

Extend Command, define a static command signature and description, then implement handle():

import { Command } from 'stratal/quarry'
import { inject } from 'tsyringe'
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 use a Laravel-style syntax to define the command name, arguments, and options in a single string.

StyleSignatureInvocation
Flat'greet'npx quarry greet
Namespaced'task:add'npx quarry task:add
Subcommand'task add'npx quarry task add
SyntaxDescription
{name}Required argument
{name?}Optional argument
{name=default}Argument with default value
{name*}Array/variadic argument
{name : description}Argument with description
SyntaxDescription
{--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

Use these methods inside handle() to retrieve parsed input values:

MethodReturn typeDescription
this.string(name)stringGet a string input (empty string if missing)
this.boolean(name)booleanGet a boolean input (false if missing)
this.number(name)numberGet a numeric input (coerces strings, 0 if missing)
this.array(name)string[]Get an array input (empty array if missing)
this.input<T>(name)TGet an input with a generic type

Use these methods inside handle() to write output:

MethodDescription
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)
import { Command } from 'stratal/quarry'
import { inject } from 'tsyringe'
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(', ') || '-',
]),
)
}
}

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 {}

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')
}
}

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> {
// ...
}
}

Stratal ships with built-in Quarry commands. Each command is documented in detail on its respective page.

CommandDescriptionDocs
helpShow help or list all commands
route:listList all registered routesControllers and Routing
event:listList all registered event listenersEvents
schedule:listList all registered cron jobsCron Jobs
queue:listList all registered queue consumersQueues
CommandDescriptionDocs
apiCall an API route directlyControllers and Routing
mcp:serveStart MCP stdio server exposing routes as toolsAI Integration
mcp:toolsList API routes exposed as MCP toolsAI Integration
CommandDescriptionDocs
db:seedRun database seedersSeeders
db:seed:listList available database seedersSeeders
CommandDescriptionDocs
db:generateGenerate ZenStack ORM clientDatabase
db:pullIntrospect database and generate schemaDatabase
db:pushPush schema changes to databaseDatabase
migrate:devCreate and apply a migrationDatabase
migrate:deployDeploy pending migrationsDatabase
migrate:resetReset databaseDatabase
migrate:statusCheck migration statusDatabase

When you run npx quarry, the CLI:

  1. Uses Wrangler’s getPlatformProxy() to obtain your Cloudflare bindings (D1, KV, R2, etc.) locally
  2. Imports your application entry file to bootstrap the full module tree
  3. Discovers all Command subclasses registered as providers
  4. 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.

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()
})
})

The TestCommandResult object returned by .run() provides these assertion methods:

MethodDescription
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.