Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@
]
},
"suggest": {
"latte/latte": "Latte template engine",
"twig/twig": "Twig template engine (recommended for apps, e.g. flightphp/skeleton)",
"latte/latte": "Latte template engine (optional alternative to Twig)",
"tracy/tracy": "Tracy debugger",
"phpstan/phpstan": "PHP Static Analyzer"
},
Expand Down
3 changes: 1 addition & 2 deletions flight/Engine.php
Original file line number Diff line number Diff line change
Expand Up @@ -463,8 +463,7 @@ protected function processMiddleware(Route $route, string $eventName): bool
}

throw new Exception(
"Middleware class '$middleware' not found. "
. "Is it being correctly autoloaded with Flight::path()?"
"Middleware class '$middleware' not found. Is it being correctly autoloaded with Flight::path()?"
);
}

Expand Down
212 changes: 125 additions & 87 deletions flight/commands/AiGenerateInstructionsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,7 @@ public function __construct(array $config)
public function execute(): int
{
$io = $this->app()->io();

if (empty($this->config['runway'])) {
$configFile = $this->configFile;
$io = $this->app()->io();

$io->warn(
'The --config-file option is deprecated. '
. 'Move your config values to the \'runway\' key in the config.php file for configuration.',
true
);
$runwayConfig = json_decode(file_get_contents($configFile), true) ?? [];
} else {
$runwayConfig = $this->config['runway'];
}
$runwayConfig = $this->resolveRunwayConfig($io);

// Check for runway creds ai
if (empty($runwayConfig['ai'])) {
Expand All @@ -64,8 +51,79 @@ public function execute(): int
}

$io->info('Let\'s gather some project details to generate AI coding instructions.', true);
$userDetails = $this->gatherProjectDetails($io);
$prompt = $this->buildPrompt($userDetails, $this->loadExistingInstructions());

// Read LLM creds
$creds = $runwayConfig['ai'];
$headers = [
'Content-Type: application/json',
'Authorization: Bearer ' . $creds['api_key'],
];
$data = [
'model' => $creds['model'],
'messages' => [
[
'role' => 'system',
// phpcs:ignore Generic.Files.LineLength
'content' => 'You are a helpful AI coding assistant focused on the Flight Framework for PHP. You are up to date with all your knowledge from https://docs.flightphp.com. As an expert into the programming language PHP, you are top notch at architecting out proper instructions for FlightPHP projects. Output a single AGENTS.md document only.',
],
['role' => 'user', 'content' => $prompt],
],
'temperature' => 0.2,
];
$jsonData = json_encode($data);

// add info line that this may take a few minutes
$io->info('Generating AI instructions, this may take a few minutes...', true);

// Ask questions
$result = $this->callLlmApi($creds['base_url'], $headers, $jsonData, $io);
if ($result === false) {
return 1;
}
$response = json_decode($result, true);
$instructions = $response['choices'][0]['message']['content'] ?? '';
if (!$instructions) {
$io->error('No instructions returned from LLM.', true);
return 1;
}

$agentsPath = $this->projectRoot . 'AGENTS.md';
$io->info('Updating AGENTS.md...', true);
file_put_contents($agentsPath, $instructions);
$io->ok('AI instructions updated successfully in AGENTS.md.', true);
return 0;
}

/**
* Resolve runway config from config.php or deprecated --config-file.
*
* @param object $io
*
* @return array<string,mixed>
*/
protected function resolveRunwayConfig($io): array
{
if (empty($this->config['runway'])) {
$io->warn(
'The --config-file option is deprecated. Move your config values to the \'runway\' key in the config.php file for configuration.', // phpcs:ignore
true
);
return json_decode(file_get_contents($this->configFile), true) ?? [];
}

return $this->config['runway'];
}

/**
* Prompt the user for project details used to generate instructions.
*
* @param object $io
*
* @return array<string,string>
*/
protected function gatherProjectDetails($io): array
{
$projectDesc = $io->prompt('Please describe what your project is for?');

$database = $io->prompt(
Expand All @@ -74,8 +132,8 @@ public function execute(): int
);

$templating = $io->prompt(
'What HTML templating engine will you plan on using (if any)? (recommend latte)',
'latte'
'What HTML templating engine will you plan on using (if any)? (recommend twig)',
'twig'
);

$security = $io->confirm('Is security an important element of this project?', 'y');
Expand All @@ -95,10 +153,7 @@ public function execute(): int
$api = $io->confirm('Will this project expose an API?', 'n');
$other = $io->prompt('Any other important requirements or context? (optional)', 'no');

// Prepare prompt for LLM
$contextFile = $this->projectRoot . '.github/copilot-instructions.md';
$context = file_exists($contextFile) === true ? file_get_contents($contextFile) : '';
$userDetails = [
return [
'Project Description' => $projectDesc,
'Database' => $database,
'Templating Engine' => $templating,
Expand All @@ -110,83 +165,66 @@ public function execute(): int
'API' => $api ? 'yes' : 'no',
'Other' => $other,
];
$detailsText = "";
}

/**
* Build the LLM user prompt from answers and existing instructions.
*
* @param array<string,string> $userDetails
* @param string $context
*
* @return string
*/
protected function buildPrompt(array $userDetails, string $context): string
{
$detailsText = '';
foreach ($userDetails as $k => $v) {
$detailsText .= "$k: $v\n";
}

// phpcs:disable Generic.Files.LineLength
$prompt = <<<EOT
You are an AI coding assistant. Update the following project instructions for this Flight PHP project based on the latest user answers. Only output the new instructions, no extra commentary.
User answers:
$detailsText
Current instructions:
$context
EOT; // phpcs:ignore
You are an AI coding assistant. Write or update project instructions for this Flight PHP project based on the latest user answers. Only output the new instructions (markdown suitable for AGENTS.md), no extra commentary.

// Read LLM creds
$creds = $runwayConfig['ai'];
$apiKey = $creds['api_key'];
$model = $creds['model'];
$baseUrl = $creds['base_url'];
Conventions to encode in the instructions (unless the user answers clearly contradict them):
- Use App\\ namespaces: App\\Controller, App\\Middleware, App\\Model, App\\Utils, App\\Command
- Controllers live in app/Controller/; inject flight\\Engine and other services via the DI container (Dice). Do not use the Flight:: facade in the app layer.
- Prefer flight\\database\\SimplePdo for database access (PdoWrapper is deprecated). Use ActiveRecord for models when an ORM is needed.
- Prefer Twig for HTML views when a templating engine is used.
- AGENTS.md is the sole AI instruction surface (no separate Copilot/Cursor/Gemini/Windsurf rule files). Scoped AGENTS.md files under app/ directories are fine when useful.
- Keep Flight simple and fast; avoid unnecessary abstractions.

// Prepare curl call (OpenAI compatible)
$headers = [
'Content-Type: application/json',
'Authorization: Bearer ' . $apiKey,
];
$data = [
'model' => $model,
'messages' => [
[
'role' => 'system',
'content' => 'You are a helpful AI coding assistant focused on the Flight Framework for PHP. '
. 'You are up to date with all your knowledge from https://docs.flightphp.com. '
. 'As an expert into the programming language PHP, '
. 'you are top notch at architecting out proper instructions for FlightPHP projects.'
],
['role' => 'user', 'content' => $prompt],
],
'temperature' => 0.2,
];
$jsonData = json_encode($data);
User answers:
$detailsText
Current instructions:
$context
EOT;
// phpcs:enable Generic.Files.LineLength

// add info line that this may take a few minutes
$io->info('Generating AI instructions, this may take a few minutes...', true);
return $prompt;
}

$result = $this->callLlmApi($baseUrl, $headers, $jsonData, $io);
if ($result === false) {
return 1;
}
$response = json_decode($result, true);
$instructions = $response['choices'][0]['message']['content'] ?? '';
if (!$instructions) {
$io->error('No instructions returned from LLM.', true);
return 1;
/**
* Load existing project instructions for context.
* Prefers AGENTS.md; falls back to legacy .github/copilot-instructions.md.
*
* @return string
*/
protected function loadExistingInstructions(): string
{
$agentsFile = $this->projectRoot . 'AGENTS.md';
if (file_exists($agentsFile) === true) {
$content = file_get_contents($agentsFile);
return $content !== false ? $content : '';
}

// Write to files
$io->info(
'Updating .github/copilot-instructions.md, '
. '.cursor/rules/project-overview.mdc, '
. '.gemini/GEMINI.md, .windsurfrules and AGENTS.md...',
true
);

if (!is_dir($this->projectRoot . '.github')) {
mkdir($this->projectRoot . '.github', 0755, true);
}
if (!is_dir($this->projectRoot . '.cursor/rules')) {
mkdir($this->projectRoot . '.cursor/rules', 0755, true);
$legacyFile = $this->projectRoot . '.github/copilot-instructions.md';
if (file_exists($legacyFile) === true) {
$content = file_get_contents($legacyFile);
return $content !== false ? $content : '';
}
if (!is_dir($this->projectRoot . '.gemini')) {
mkdir($this->projectRoot . '.gemini', 0755, true);
}
file_put_contents($this->projectRoot . '.github/copilot-instructions.md', $instructions);
file_put_contents($this->projectRoot . '.cursor/rules/project-overview.mdc', $instructions);
file_put_contents($this->projectRoot . '.gemini/GEMINI.md', $instructions);
file_put_contents($this->projectRoot . '.windsurfrules', $instructions);
file_put_contents($this->projectRoot . 'AGENTS.md', $instructions);
$io->ok('AI instructions updated successfully.', true);
return 0;

return '';
}

/**
Expand Down
2 changes: 1 addition & 1 deletion flight/commands/AiInitCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public function execute(): int
$defaultModel = 'claude-sonnet-4-5';
break;
}

$model = trim($io->prompt(
'Enter the model name you want to use (e.g. gpt-5, claude-sonnet-4-5, etc)',
$defaultModel
Expand Down
24 changes: 17 additions & 7 deletions flight/commands/ControllerCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@

class ControllerCommand extends AbstractBaseCommand
{
/**
* Relative directory under app_root for controllers (skeleton: App\Controller).
*/
private const CONTROLLER_DIR = 'Controller';

/**
* PSR-4 namespace for generated controllers.
*/
private const CONTROLLER_NAMESPACE = 'App\\Controller';

/**
* Construct
*
Expand All @@ -32,8 +42,7 @@ public function execute(string $controller): void

if (empty($this->config['runway'])) {
$io->warn(
'Using a .runway-config.json file is deprecated. '
. 'Move your config values to app/config/config.php with `php runway config:migrate`.',
'Using a .runway-config.json file is deprecated. Move your config values to app/config/config.php with `php runway config:migrate`.', // phpcs:ignore
true
); // @codeCoverageIgnore

Expand Down Expand Up @@ -61,7 +70,8 @@ public function execute(string $controller): void
$controller .= 'Controller';
}

$controllerPath = $this->projectRoot . '/' . $runwayConfig['app_root'] . 'controllers/' . $controller . '.php';
$appRoot = rtrim(str_replace('\\', '/', $runwayConfig['app_root']), '/') . '/';
$controllerPath = $this->projectRoot . '/' . $appRoot . self::CONTROLLER_DIR . '/' . $controller . '.php';
if (file_exists($controllerPath) === true) {
$io->error($controller . ' already exists.', true);
return;
Expand All @@ -75,12 +85,12 @@ public function execute(string $controller): void
$file = new PhpFile();
$file->setStrictTypes();

$namespace = new PhpNamespace('app\\controllers');
$namespace = new PhpNamespace(self::CONTROLLER_NAMESPACE);
$namespace->addUse('flight\\Engine');

$class = new ClassType($controller);
$class->addProperty('app')
->setVisibility('protected')
->setVisibility('private')
->setType('flight\\Engine')
->addComment('@var Engine');
$method = $class->addMethod('__construct')
Expand All @@ -93,7 +103,7 @@ public function execute(string $controller): void
$namespace->add($class);
$file->addNamespace($namespace);

$this->persistClass($controller, $file, $runwayConfig['app_root']);
$this->persistClass($controller, $file, $appRoot);

$io->ok('Controller successfully created at ' . $controllerPath, true);
}
Expand All @@ -111,7 +121,7 @@ protected function persistClass(string $controllerName, PhpFile $file, string $a
{
$printer = new \Nette\PhpGenerator\PsrPrinter();
file_put_contents(
$this->projectRoot . '/' . $appRoot . 'controllers/' . $controllerName . '.php',
$this->projectRoot . '/' . $appRoot . self::CONTROLLER_DIR . '/' . $controllerName . '.php',
$printer->printFile($file)
);
}
Expand Down
3 changes: 1 addition & 2 deletions flight/commands/RouteCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ public function execute(): void

if (empty($this->config['runway'])) {
$io->warn(
'Using a .runway-config.json file is deprecated. '
. 'Move your config values to app/config/config.php with `php runway config:migrate`.',
'Using a .runway-config.json file is deprecated. Move your config values to app/config/config.php with `php runway config:migrate`.', // phpcs:ignore
true
); // @codeCoverageIgnore

Expand Down
3 changes: 1 addition & 2 deletions flight/core/Dispatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -415,8 +415,7 @@ protected function verifyValidClassCallable($class, $method, $resolvedClass): vo
// Final check to make sure it's actually a class and a method, or throw an error
if (is_object($class) === false && class_exists($class) === false) {
$exception = new Exception(
"Class '$class' not found. "
. "Is it being correctly autoloaded with Flight::path()?"
"Class '$class' not found. Is it being correctly autoloaded with Flight::path()?"
);

// If this tried to resolve a class in a container and failed somehow, throw the exception
Expand Down
9 changes: 5 additions & 4 deletions flight/core/Loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,11 @@ class Loader
/**
* Registers a class.
*
* @param string $name Registry name
* @param class-string<T>|(Closure(): T) $class Class name or function to instantiate class
* @param array<int, mixed> $params Class initialization parameters
* @param null|(Closure(T $instance): void) $callback $callback Function to call after object instantiation
* @param string $name Registry name
* @param class-string<T>|(Closure(): T) $class Class name or function to instantiate class
* @param array<int, mixed> $params Class initialization parameters
* @param null|(Closure(T $instance): void) $callback Function to call after object instantiation
*
* @template T of object
*/
public function register(string $name, $class, array $params = [], ?callable $callback = null): void
Expand Down
Loading