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:
| Parameter | Passed 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.