Skip to content

Commit ee03274

Browse files
authored
Merge pull request #714 from flightphp/feat/align-runway-commands-with-skeleton
Align Runway generators with skeleton App\ layout
2 parents f2df523 + 98c9c89 commit ee03274

17 files changed

Lines changed: 325 additions & 215 deletions

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@
9999
]
100100
},
101101
"suggest": {
102-
"latte/latte": "Latte template engine",
102+
"twig/twig": "Twig template engine (recommended for apps, e.g. flightphp/skeleton)",
103+
"latte/latte": "Latte template engine (optional alternative to Twig)",
103104
"tracy/tracy": "Tracy debugger",
104105
"phpstan/phpstan": "PHP Static Analyzer"
105106
},

flight/Engine.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -463,8 +463,7 @@ protected function processMiddleware(Route $route, string $eventName): bool
463463
}
464464

465465
throw new Exception(
466-
"Middleware class '$middleware' not found. "
467-
. "Is it being correctly autoloaded with Flight::path()?"
466+
"Middleware class '$middleware' not found. Is it being correctly autoloaded with Flight::path()?"
468467
);
469468
}
470469

flight/commands/AiGenerateInstructionsCommand.php

Lines changed: 125 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -42,20 +42,7 @@ public function __construct(array $config)
4242
public function execute(): int
4343
{
4444
$io = $this->app()->io();
45-
46-
if (empty($this->config['runway'])) {
47-
$configFile = $this->configFile;
48-
$io = $this->app()->io();
49-
50-
$io->warn(
51-
'The --config-file option is deprecated. '
52-
. 'Move your config values to the \'runway\' key in the config.php file for configuration.',
53-
true
54-
);
55-
$runwayConfig = json_decode(file_get_contents($configFile), true) ?? [];
56-
} else {
57-
$runwayConfig = $this->config['runway'];
58-
}
45+
$runwayConfig = $this->resolveRunwayConfig($io);
5946

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

6653
$io->info('Let\'s gather some project details to generate AI coding instructions.', true);
54+
$userDetails = $this->gatherProjectDetails($io);
55+
$prompt = $this->buildPrompt($userDetails, $this->loadExistingInstructions());
56+
57+
// Read LLM creds
58+
$creds = $runwayConfig['ai'];
59+
$headers = [
60+
'Content-Type: application/json',
61+
'Authorization: Bearer ' . $creds['api_key'],
62+
];
63+
$data = [
64+
'model' => $creds['model'],
65+
'messages' => [
66+
[
67+
'role' => 'system',
68+
// phpcs:ignore Generic.Files.LineLength
69+
'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.',
70+
],
71+
['role' => 'user', 'content' => $prompt],
72+
],
73+
'temperature' => 0.2,
74+
];
75+
$jsonData = json_encode($data);
76+
77+
// add info line that this may take a few minutes
78+
$io->info('Generating AI instructions, this may take a few minutes...', true);
6779

68-
// Ask questions
80+
$result = $this->callLlmApi($creds['base_url'], $headers, $jsonData, $io);
81+
if ($result === false) {
82+
return 1;
83+
}
84+
$response = json_decode($result, true);
85+
$instructions = $response['choices'][0]['message']['content'] ?? '';
86+
if (!$instructions) {
87+
$io->error('No instructions returned from LLM.', true);
88+
return 1;
89+
}
90+
91+
$agentsPath = $this->projectRoot . 'AGENTS.md';
92+
$io->info('Updating AGENTS.md...', true);
93+
file_put_contents($agentsPath, $instructions);
94+
$io->ok('AI instructions updated successfully in AGENTS.md.', true);
95+
return 0;
96+
}
97+
98+
/**
99+
* Resolve runway config from config.php or deprecated --config-file.
100+
*
101+
* @param object $io
102+
*
103+
* @return array<string,mixed>
104+
*/
105+
protected function resolveRunwayConfig($io): array
106+
{
107+
if (empty($this->config['runway'])) {
108+
$io->warn(
109+
'The --config-file option is deprecated. Move your config values to the \'runway\' key in the config.php file for configuration.', // phpcs:ignore
110+
true
111+
);
112+
return json_decode(file_get_contents($this->configFile), true) ?? [];
113+
}
114+
115+
return $this->config['runway'];
116+
}
117+
118+
/**
119+
* Prompt the user for project details used to generate instructions.
120+
*
121+
* @param object $io
122+
*
123+
* @return array<string,string>
124+
*/
125+
protected function gatherProjectDetails($io): array
126+
{
69127
$projectDesc = $io->prompt('Please describe what your project is for?');
70128

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

76134
$templating = $io->prompt(
77-
'What HTML templating engine will you plan on using (if any)? (recommend latte)',
78-
'latte'
135+
'What HTML templating engine will you plan on using (if any)? (recommend twig)',
136+
'twig'
79137
);
80138

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

98-
// Prepare prompt for LLM
99-
$contextFile = $this->projectRoot . '.github/copilot-instructions.md';
100-
$context = file_exists($contextFile) === true ? file_get_contents($contextFile) : '';
101-
$userDetails = [
156+
return [
102157
'Project Description' => $projectDesc,
103158
'Database' => $database,
104159
'Templating Engine' => $templating,
@@ -110,83 +165,66 @@ public function execute(): int
110165
'API' => $api ? 'yes' : 'no',
111166
'Other' => $other,
112167
];
113-
$detailsText = "";
168+
}
169+
170+
/**
171+
* Build the LLM user prompt from answers and existing instructions.
172+
*
173+
* @param array<string,string> $userDetails
174+
* @param string $context
175+
*
176+
* @return string
177+
*/
178+
protected function buildPrompt(array $userDetails, string $context): string
179+
{
180+
$detailsText = '';
114181
foreach ($userDetails as $k => $v) {
115182
$detailsText .= "$k: $v\n";
116183
}
184+
185+
// phpcs:disable Generic.Files.LineLength
117186
$prompt = <<<EOT
118-
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.
119-
User answers:
120-
$detailsText
121-
Current instructions:
122-
$context
123-
EOT; // phpcs:ignore
187+
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.
124188
125-
// Read LLM creds
126-
$creds = $runwayConfig['ai'];
127-
$apiKey = $creds['api_key'];
128-
$model = $creds['model'];
129-
$baseUrl = $creds['base_url'];
189+
Conventions to encode in the instructions (unless the user answers clearly contradict them):
190+
- Use App\\ namespaces: App\\Controller, App\\Middleware, App\\Model, App\\Utils, App\\Command
191+
- 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.
192+
- Prefer flight\\database\\SimplePdo for database access (PdoWrapper is deprecated). Use ActiveRecord for models when an ORM is needed.
193+
- Prefer Twig for HTML views when a templating engine is used.
194+
- 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.
195+
- Keep Flight simple and fast; avoid unnecessary abstractions.
130196
131-
// Prepare curl call (OpenAI compatible)
132-
$headers = [
133-
'Content-Type: application/json',
134-
'Authorization: Bearer ' . $apiKey,
135-
];
136-
$data = [
137-
'model' => $model,
138-
'messages' => [
139-
[
140-
'role' => 'system',
141-
'content' => 'You are a helpful AI coding assistant focused on the Flight Framework for PHP. '
142-
. 'You are up to date with all your knowledge from https://docs.flightphp.com. '
143-
. 'As an expert into the programming language PHP, '
144-
. 'you are top notch at architecting out proper instructions for FlightPHP projects.'
145-
],
146-
['role' => 'user', 'content' => $prompt],
147-
],
148-
'temperature' => 0.2,
149-
];
150-
$jsonData = json_encode($data);
197+
User answers:
198+
$detailsText
199+
Current instructions:
200+
$context
201+
EOT;
202+
// phpcs:enable Generic.Files.LineLength
151203

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

155-
$result = $this->callLlmApi($baseUrl, $headers, $jsonData, $io);
156-
if ($result === false) {
157-
return 1;
158-
}
159-
$response = json_decode($result, true);
160-
$instructions = $response['choices'][0]['message']['content'] ?? '';
161-
if (!$instructions) {
162-
$io->error('No instructions returned from LLM.', true);
163-
return 1;
207+
/**
208+
* Load existing project instructions for context.
209+
* Prefers AGENTS.md; falls back to legacy .github/copilot-instructions.md.
210+
*
211+
* @return string
212+
*/
213+
protected function loadExistingInstructions(): string
214+
{
215+
$agentsFile = $this->projectRoot . 'AGENTS.md';
216+
if (file_exists($agentsFile) === true) {
217+
$content = file_get_contents($agentsFile);
218+
return $content !== false ? $content : '';
164219
}
165220

166-
// Write to files
167-
$io->info(
168-
'Updating .github/copilot-instructions.md, '
169-
. '.cursor/rules/project-overview.mdc, '
170-
. '.gemini/GEMINI.md, .windsurfrules and AGENTS.md...',
171-
true
172-
);
173-
174-
if (!is_dir($this->projectRoot . '.github')) {
175-
mkdir($this->projectRoot . '.github', 0755, true);
176-
}
177-
if (!is_dir($this->projectRoot . '.cursor/rules')) {
178-
mkdir($this->projectRoot . '.cursor/rules', 0755, true);
221+
$legacyFile = $this->projectRoot . '.github/copilot-instructions.md';
222+
if (file_exists($legacyFile) === true) {
223+
$content = file_get_contents($legacyFile);
224+
return $content !== false ? $content : '';
179225
}
180-
if (!is_dir($this->projectRoot . '.gemini')) {
181-
mkdir($this->projectRoot . '.gemini', 0755, true);
182-
}
183-
file_put_contents($this->projectRoot . '.github/copilot-instructions.md', $instructions);
184-
file_put_contents($this->projectRoot . '.cursor/rules/project-overview.mdc', $instructions);
185-
file_put_contents($this->projectRoot . '.gemini/GEMINI.md', $instructions);
186-
file_put_contents($this->projectRoot . '.windsurfrules', $instructions);
187-
file_put_contents($this->projectRoot . 'AGENTS.md', $instructions);
188-
$io->ok('AI instructions updated successfully.', true);
189-
return 0;
226+
227+
return '';
190228
}
191229

192230
/**

flight/commands/AiInitCommand.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public function execute(): int
7979
$defaultModel = 'claude-sonnet-4-5';
8080
break;
8181
}
82-
82+
8383
$model = trim($io->prompt(
8484
'Enter the model name you want to use (e.g. gpt-5, claude-sonnet-4-5, etc)',
8585
$defaultModel

flight/commands/ControllerCommand.php

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@
1010

1111
class ControllerCommand extends AbstractBaseCommand
1212
{
13+
/**
14+
* Relative directory under app_root for controllers (skeleton: App\Controller).
15+
*/
16+
private const CONTROLLER_DIR = 'Controller';
17+
18+
/**
19+
* PSR-4 namespace for generated controllers.
20+
*/
21+
private const CONTROLLER_NAMESPACE = 'App\\Controller';
22+
1323
/**
1424
* Construct
1525
*
@@ -32,8 +42,7 @@ public function execute(string $controller): void
3242

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

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

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

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

8191
$class = new ClassType($controller);
8292
$class->addProperty('app')
83-
->setVisibility('protected')
93+
->setVisibility('private')
8494
->setType('flight\\Engine')
8595
->addComment('@var Engine');
8696
$method = $class->addMethod('__construct')
@@ -93,7 +103,7 @@ public function execute(string $controller): void
93103
$namespace->add($class);
94104
$file->addNamespace($namespace);
95105

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

98108
$io->ok('Controller successfully created at ' . $controllerPath, true);
99109
}
@@ -111,7 +121,7 @@ protected function persistClass(string $controllerName, PhpFile $file, string $a
111121
{
112122
$printer = new \Nette\PhpGenerator\PsrPrinter();
113123
file_put_contents(
114-
$this->projectRoot . '/' . $appRoot . 'controllers/' . $controllerName . '.php',
124+
$this->projectRoot . '/' . $appRoot . self::CONTROLLER_DIR . '/' . $controllerName . '.php',
115125
$printer->printFile($file)
116126
);
117127
}

flight/commands/RouteCommand.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,7 @@ public function execute(): void
4343

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

flight/core/Dispatcher.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -415,8 +415,7 @@ protected function verifyValidClassCallable($class, $method, $resolvedClass): vo
415415
// Final check to make sure it's actually a class and a method, or throw an error
416416
if (is_object($class) === false && class_exists($class) === false) {
417417
$exception = new Exception(
418-
"Class '$class' not found. "
419-
. "Is it being correctly autoloaded with Flight::path()?"
418+
"Class '$class' not found. Is it being correctly autoloaded with Flight::path()?"
420419
);
421420

422421
// If this tried to resolve a class in a container and failed somehow, throw the exception

flight/core/Loader.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,11 @@ class Loader
4747
/**
4848
* Registers a class.
4949
*
50-
* @param string $name Registry name
51-
* @param class-string<T>|(Closure(): T) $class Class name or function to instantiate class
52-
* @param array<int, mixed> $params Class initialization parameters
53-
* @param null|(Closure(T $instance): void) $callback $callback Function to call after object instantiation
50+
* @param string $name Registry name
51+
* @param class-string<T>|(Closure(): T) $class Class name or function to instantiate class
52+
* @param array<int, mixed> $params Class initialization parameters
53+
* @param null|(Closure(T $instance): void) $callback Function to call after object instantiation
54+
*
5455
* @template T of object
5556
*/
5657
public function register(string $name, $class, array $params = [], ?callable $callback = null): void

0 commit comments

Comments
 (0)