Framework
Introduction

CLI Commands

Everything is driven by the ./framework binary (an alias for ./vendor/bin/framework). Every command is a #[ConsoleCommand] discovered across the framework — and your own classes can expose commands the same way.

Commands

The built-in commands cover the whole workflow — installing, building, migrating and more:

install

Scaffolds a new project: copies the framework binary, the sample config and the entry files into your app and wires everything up. Run it once, right after requiring the package.

./framework install

build

Runs discovery and regenerates the typed code under src/System — config, routes, access roles, settings and schemas. Run it after changing anything the generator reads.

./framework build

destroy

Removes the generated code that build produced, leaving your own source untouched. Handy before a clean rebuild or when switching branches.

./framework destroy

watch

Watches your app source and rebuilds automatically whenever a file changes — the convenient way to develop. It respects .gitignore and runs until you stop it.

./framework watch

migrate

Applies every pending migration: syncs the schema to your models, runs the table and column renames and executes the data migrations. Pass an env file to force a specific environment.

./framework migrate
./framework migrate .env.production   # force an environment

migration

Scaffolds a new data migration file, prompting for a title (or taking it as an argument) and opening it for editing. Files are grouped in folders by year and month.

./framework migration
./framework migration "Backfill user slugs"

ensurePaths

Creates the file-storage directories — the built-in temp, source, thumbs and avatars folders plus any you registered — so uploads have somewhere to live.

./framework ensurePaths

icons

Generates the icon stylesheet and a preview page from your registered icon sets. Point it at your icons with Icons::setSource() and Icons::register() first.

./framework icons

version

Prints the installed framework version. Also available as the -v shorthand.

./framework version
./framework -v

test

A scratch command for trying out framework code straight from the CLI — useful while developing the framework itself.

./framework test

Adding your own commands

Any public static method in your app can become a command: tag it with #[ConsoleCommand] and it is discovered and wired to the CLI on the next build:

use Framework\Discovery\Attr\ConsoleCommand;

class Reports {
    #[ConsoleCommand("report", "-r")]
    public static function generate(string $month = "", bool $email = false): void {
        print("Generating the report...\n");
        // …your logic here
    }
}
./framework report --month=2026-08 --email
./framework -r --month=2026-08

How the arguments work

The method's parameters are the arguments — there is nothing to declare twice. Each is matched by name, so order on the command line does not matter:

ParameterPassed as
string $month--month=2026-08
int $limit--limit=50 — a numeric value becomes an int.
bool $email--email on its own, or --email=true / --email=false.

Names are matched case-insensitively and the -- is optional, so all of these reach the same parameter:

./framework report --month=2026-08
./framework report month=2026-08
./framework report --MONTH=2026-08

A parameter with a default is optional. One without a default is required — if it is missing the command does not run, and the CLI prints the usage line it built from the signature:

Invalid arguments
  Usage: report (-r) --month=<value> --email

That usage line is generated, so it always matches the method. It is also why a bool shows as a bare flag while everything else shows =<value>.

Order & aliases

The second argument of the attribute is an alias, for the commands you type often — version answers to -v. Commands are listed in Priority order, which is why version and install appear at the top rather than wherever the scan happened to find them:

#[ConsoleCommand("report", "-r")]
#[Priority(Priority::High)]
public static function generate(): void {}

Framework-only commands

A command marked isPrivate is only available while working on the Framework itself — it is skipped entirely in an app that installs it. That is how docs, setVersion, incVersion and decVersion exist here without appearing in your project's CLI:

#[ConsoleCommand("docs", isPrivate: true)]
public static function docs(int $port = Package::DocsPort): void {}

Running commands on a schedule

A command is also how scheduled work runs. There is no built-in scheduler — you expose the work as a command and let the server's cron call it:

class EmailCommands {
    #[ConsoleCommand("sendEmails")]
    public static function send(): void {
        EmailQueue::sendAll();
    }
}
# crontab
* * * * *   cd /srv/app && ./framework sendEmails
0 3 * * *   cd /srv/app && ./framework cleanLogs

Anything running this way has no request behind it — no route, no signed-in user — so it has to be told the language explicitly, and reads its configuration from the ENV_FILENAME you point it at. The email and notification queues, and the log cleanup, all work this way.