08.10.2026 7 min read

Festi CLI 7.2.0: Agent-Ready Tools for Plugins and Development

More and more of the work on a Festi project is done by coding agents, and an agent can only use what it can run from a command line. Festi CLI 7.2.0 extends that toolset. An agent, or a developer, can now trace a URL to the code behind it, install and update a plugin from its own git repository, generate the data access code of a plugin, turn working SQL into a migration, and ask any command to show exactly what it ran.

Release Notes

Features

  • festi-route. Prints the plugin method a URL is routed to as Class::method:line, without sending a request. --http-base cuts the project's base path off the URL.
  • Plugins from their own repository. festi-plugin --mode install adds the repository as a git submodule and applies the plugin's install SQL as a new migration of the project. --mode update takes the plugin's update SQL when it changed since the project last took it. A marker line at the top of each generated migration records what was taken.
  • Generated data access code. festi-plugin --mode dao writes search, get, add, change and remove methods for the given tables into the plugin's DataAccessObject and, on request, a values object for the main table.
  • Migrations from a working file. festi-make-migration --develop turns dumps/develop.sql into the next updates<timestamp>.sql and records it as applied to the developer's database. --not-applied skips the record.
  • Saving generated SQL. --schema on the tools that generate SQL saves the SQL of the run as a new migration, or appends it to the file --schema=<file> names.
  • Config values in SQL migrations. A migration can take values from config.php in the dumps directory. festi-migrate fills each %%name%% in, and a parameter without a value stops the run before the migration is applied.
  • Verbose mode everywhere. --verbose and -v on every festi-* command show executed commands, the SQL that changes the database, resolved paths and stack traces on failure. Migration SQL is no longer printed by default.
  • Reinstall in one step. festi-install --force y removes the previous installation and drops the tables of the configured database before installing. It asks for confirmation when interactive and lists the tables it drops.
  • Clearer async installs. After an async install the installer shows how to start the service and warns when PHP has no swoole.

Security

  • Force mode only destroys what it recognises. It refuses /, the home directory, a folder whose composer.json requires no Festi package, and a database without schema_migration. A mistyped --install_path no longer resolves to /.
  • No credentials in stack traces. The verbose stack trace is rendered without frame arguments, so a failed database connection cannot print the credentials.
  • Bound table names. The PostgreSQL and SQL Server foreign key lookups bind the table name instead of concatenating it into the SQL.

Bug Fixes

  • Complete translation messages. festi-locale no longer cuts a message at a quote before a closing parenthesis, and messages followed by arguments are collected too.
  • Help wherever you ask. --help and -h are answered wherever they appear on the command line, and a prompt with input closed is an error instead of repeating the question forever.
  • MySQL async containers start. The mysql-async Docker health check passed the host with a leading space, so the database never turned healthy and the service never started.
  • Saved URL rules replay. The area of a URL rule is linked by the rule's plugin, pattern and method instead of its generated id, so a saved migration replays correctly on another database.

Improvements

  • The database adapters behind the DGS tools share one storage layer, with one adapter per database. Table names are quoted where the adapters build SQL.
  • The checks that tell column types apart moved out of the storage adapters into their own component. The protected isTypeText() to isTypePrice() methods of the adapters are gone: a subclass that overrides one of them has to override createColumnTypes() instead.
  • A tool can take an argument without an option name. For such a tool an unknown option or an extra argument is an error.
  • The code festi-plugin --mode dao generates passes the Festi coding standard 1.3.0.
  • The local test stack has its own container names, so it no longer shares containers or host ports with other Festi projects.

Test Results

  • 315 of 315 tests passed on MySQL with PHP 8.4, with 1586 assertions.
  • The MySQL, PostgreSQL and SQL Server test jobs passed in continuous integration, and the quality gate is green.

What Changed and Why

Festi CLI 7.1.0 made the commands explain themselves, which is what lets an agent discover them. 7.2.0 gives agents and developers more to do with them: operating plugins, finding the code behind a page, and running the development process from the command line, without steps that only a person at a keyboard could complete.

Problem

To find the method behind a URL you had to read the URL rules table and match patterns by eye. A plugin that lives in its own repository was set up by hand: add the submodule, find its install SQL, copy it into a migration, and remember to repeat that when the plugin changed. The data access layer of a new plugin started as the same boilerplate every time.

Migrations had their own friction. SQL written against a development database had to be moved into a numbered file by hand and marked as applied. A value that differed between installations, or a secret, had nowhere to live except the migration itself. And when a command failed there was no way to ask it what it had run. Each of these is a nuisance for a developer and a dead end for an agent, which cannot read a routing table by eye or answer a prompt that waits forever.

Solution

7.2.0 adds a tenth command, festi-route, that answers the URL question directly. festi-plugin gains three modes: install and update for plugins from their own repository, and dao for generated data access code. festi-make-migration --develop promotes a working SQL file to a migration, migrations can read values from a config file, and every command accepts --verbose. The installer can replace a previous installation in one step, with guard rails on what it is allowed to delete. And a command that needs an answer it cannot get now fails with an error instead of asking again forever, so an unattended run ends instead of hanging.

Feature Highlights

festi-route: from a URL to the code

festi-route tells you which plugin method a URL of the project is routed to, so you can open the code behind a page without reading the URL rules table. It only reads the rules and the code. No request is sent to the method.

$ festi-route "/test13/"
TestPlugin::onDisplayTest:125

The answer is the class or trait that declares the method, the method and the line it starts on. The URL can be pasted from the browser as it is: the host and the query string are ignored.

$ festi-route "https://app.example.com/report/42/?page=2" --area backend

When several rules match, each is printed with its area and pattern, and the one this installation routes by is marked as used. Like every Festi command it documents itself. Here is the top of its help page, run from a directory that is not a Festi project:

$ festi-route --help | head -19
festi-route - Find the plugin method a URL is routed to and print it as Class::method:line

Usage:
  festi-route <url> [options]

Arguments:
  <url>  URL or its path, e.g. /test13/ (asked for when missing)

Options:
  --url <url>               The URL, when it is not given as the argument
  --area <area>             Look only in this area, e.g. backend (default: every area of the project)
  --system-plugin <plugin>  Plugin registered as the system plugin (default: Jimbo)
  --http-base <path>        Path the project is served under, cut off the URL as the framework does (default: the http_base of the project, or /)
  --path <path>             Project root, where config.php is (default: current directory)
  --config <file>           The project's config.php, when it is not in the project root (default: <path>/config.php)
  --verbose, -v             Print debug output: commands, SQL, paths, stack traces
  --help, -h                Show this help and exit

Examples:

Plugins from their own repository

A plugin that lives in a repository of its own is set up in an existing project with one command:

$ festi-plugin --mode install --path ./src/dashboard/ --dump ./dumps/ \
    --plugin Contents --repository [email protected]:FestiPlugins/PHP_Festi_Plugin_Contents.git

It adds the repository as a git submodule, copies the plugin's install SQL into the dumps directory as a new migration, and applies the migrations the database does not have yet. The SQL goes through a migration of the project on purpose: that is how every other database of the project gets the plugin. Commit the submodule and the new migration together. When anything fails, the plugin folder and the migration are removed again.

After you take a newer version of the plugin, --mode update applies what it brought, and only when the plugin's update SQL changed since the project last took it. Each generated migration starts with a line that records what was taken:

-- festi-plugin: <plugin> <file> sha256=<hash>

Generated data access code

festi-plugin --mode dao writes search, get, add, change and remove methods for the tables you name into the plugin's DataAccessObject. The first table is the main one, and on request it also creates a values object for it.

$ festi-plugin --mode dao --plugin Shop --tables orders,orders_items --values_object n
$ festi-plugin --mode dao --plugin Shop --tables orders --values_object y --entity Order

Migrations from a working file

While developing, keep your SQL in dumps/develop.sql and run it against your own database. When the change is ready, one command turns that file into the next updates<timestamp>.sql:

$ festi-make-migration --develop
$ festi-make-migration --develop=./sql/feature.sql --dump ./dumps/
$ festi-make-migration --develop --not-applied

The file is renamed, its content is not changed, and the migration is recorded as applied to your database, because its SQL already ran there. festi-migrate then runs it only on the other databases. Use --not-applied for a file you wrote by hand and never ran.

Config values in SQL migrations

A migration can take a value that differs between installations, or that must not be committed, from config.php in the dumps directory. The migration names the value as %%name%%:

INSERT INTO ticketing_systems (caption, redirect_uri, client_id, client_secret)
VALUES ('Discord', 'https://%%host%%/callback/discord/', '%%discord_client_id%%', '%%discord_client_secret%%');
<?php
// dumps/config.php

return [
    'host'                  => 'app.example.com',
    'discord_client_id'     => getenv('DISCORD_CLIENT_ID'),
    'discord_client_secret' => getenv('DISCORD_CLIENT_SECRET'),
];

A parameter that the config does not define stops festi-migrate before the migration is applied, and nothing is recorded as applied. Keep config.php out of git when it holds secrets and commit a config.dist.php that lists the keys.

Saving the SQL a tool runs

The tools that generate SQL accept --schema. The SQL of the run is saved as a new migration, or appended to the file you name with --schema=<file>. Nothing is saved when the run fails or changes nothing, and the saved SQL can be replayed on another database.

Verbose mode on every command

Every festi-* command accepts --verbose or -v and then prints the commands it executes, the SQL that changes the database, the paths it resolved, and a stack trace on failure. The quiet default got quieter too: migration SQL is no longer printed unless you ask for it.

Reinstall with guard rails

festi-install --force y removes the previous installation and drops the tables of the configured database before installing. It asks for confirmation when interactive and lists the tables it drops. It refuses the root directory, the home directory, a folder that is not a Festi project, and a database that has never been migrated.

Benefits & Impact

A toolset agents can operate

Every step in this release is a single command with documented options, a clear result and an error when it cannot proceed. That is what a coding agent needs to work on a Festi project on its own: look up the code, add a plugin, generate the data layer, produce the migration, and read back what happened.

Faster debugging

A bug report usually starts with a URL. One command now takes you from that URL to the class, method and line, without a request and without reading routing tables.

Plugins that stay in sync

Installing a plugin through a migration means every database of the project gets the same SQL, and the recorded hash means an update is applied once, when something actually changed.

Less boilerplate

The first hour of a new plugin no longer goes into writing the same five data access methods per table. They are generated to the coding standard and ready to extend.

Migrations that match how you work

Write SQL while you develop, promote it when it is ready, and keep installation specific values and secrets out of the repository. A missing value stops the run before anything is applied.

Answers when something fails

Verbose mode shows what a command ran, and its stack traces leave out frame arguments, so database credentials do not end up in a terminal or a CI log.

Destructive options you can trust

Force mode deletes only what it recognises as a previous Festi installation. A mistyped path cannot turn a reinstall into a wiped disk.

Conclusion

Festi CLI 7.2.0 extends what can be done on a Festi project from the command line, which is the same as extending what an agent can do on it: find the code behind a URL, add or update a plugin, generate its data access code, and produce the migration. Each of those is now one command.

If you already run Festi, upgrade and point your agent at --help. Then try festi-route on the first URL you are asked about. One thing to check before you do: if you subclass a DGS storage adapter and override one of its column type checks, override createColumnTypes() instead. Want to talk through a Festi project? Get in touch with the team.

Featured Articles

Festi CLI 7.2.0: Agent-Ready Tools for Plugins and Development

October 08, 2026

Festi CLI 7.2.0 gives coding agents and developers commands to operate Festi plugins and the development process: trace a URL to its code, install and update plugins from git, generate data access code, and build migrations.

iTerm2 for Agentic Development: A Workspace That Survives a Restart

October 07, 2026

How to set up iTerm2 for Claude Code and Codex: one profile per agent, a status on every tab, and a saved window arrangement that brings back every tab, group and split pane after a restart, with each agent resuming its last conversation.

How Festi Built a Mobile App and CMS for a Big Four Regional Office

October 06, 2026

Festi built a cross-platform mobile application and a custom CMS for a regional office of a Big Four accounting firm, deployed inside its existing Microsoft Azure environment. Internal communication efficiency improved by 40%, manual content update workload fell by over 50%, and every intended user adopted the platform after a four-month build.

Get in Touch

Building on Festi, or weighing it up? Tell us what you are working on and we will point you at the right starting place.