Skip to content

Commit d43f2e9

Browse files
authored
Merge pull request #652 from flightphp/ai-helpers
Added AI commands and instructions for the repo.
2 parents 11806e9 + 7ddd5bb commit d43f2e9

File tree

7 files changed

+803
-4
lines changed

7 files changed

+803
-4
lines changed

.github/copilot-instructions.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# FlightPHP/Core Project Instructions
2+
3+
## Overview
4+
This is the main FlightPHP core library for building fast, simple, and extensible PHP web applications. It is dependency-free for core usage and supports PHP 7.4+.
5+
6+
## Project Guidelines
7+
- PHP 7.4 must be supported. PHP 8 or greater also supported, but avoid PHP 8+ only features.
8+
- Keep the core library dependency-free (no polyfills or interface-only repositories).
9+
- All Flight projects are meant to be kept simple and fast. Performance is a priority.
10+
- Flight is extensible and when implementing new features, consider how they can be added as plugins or extensions rather than bloating the core library.
11+
- Any new features built into the core should be well-documented and tested.
12+
- Any new features should be added with a focus on simplicity and performance, avoiding unnecessary complexity.
13+
- This is not a Laravel, Yii, Code Igniter or Symfony clone. It is a simple, fast, and extensible framework that allows you to build applications quickly without the overhead of large frameworks.
14+
15+
## Development & Testing
16+
- Run tests: `composer test` (uses phpunit/phpunit and spatie/phpunit-watcher)
17+
- Run test server: `composer test-server` or `composer test-server-v2`
18+
- Lint code: `composer lint` (uses phpstan/phpstan, level 6)
19+
- Beautify code: `composer beautify` (uses squizlabs/php_codesniffer, PSR1)
20+
- Check code style: `composer phpcs`
21+
- Test coverage: `composer test-coverage`
22+
23+
## Coding Standards
24+
- Follow PSR1 coding standards (enforced by PHPCS)
25+
- Use strict comparisons (`===`, `!==`)
26+
- PHPStan level 6 compliance
27+
- Focus on PHP 7.4 compatibility (avoid PHP 8+ only features)

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ coverage/
88
*.sublime*
99
clover.xml
1010
phpcs.xml
11-
.runway-config.json
11+
.runway-config.json
12+
.runway-creds.json
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace flight\commands;
6+
7+
use Ahc\Cli\Input\Command;
8+
9+
/**
10+
* @property-read ?string $credsFile
11+
* @property-read ?string $baseDir
12+
*/
13+
class AiGenerateInstructionsCommand extends Command
14+
{
15+
/**
16+
* Constructor for the AiGenerateInstructionsCommand class.
17+
*
18+
* Initializes a new instance of the command.
19+
*/
20+
public function __construct()
21+
{
22+
parent::__construct('ai:generate-instructions', 'Generate project-specific AI coding instructions');
23+
$this->option('--creds-file', 'Path to .runway-creds.json file', null, '');
24+
$this->option('--base-dir', 'Project base directory (for testing or custom use)', null, '');
25+
}
26+
27+
/**
28+
* Executes the command logic for generating AI instructions.
29+
*
30+
* This method is called to perform the main functionality of the
31+
* AiGenerateInstructionsCommand. It should contain the steps required
32+
* to generate and output instructions using AI, based on the command's
33+
* configuration and input.
34+
*
35+
* @return int
36+
*/
37+
public function execute()
38+
{
39+
$io = $this->app()->io();
40+
$baseDir = $this->baseDir ? rtrim($this->baseDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR : getcwd() . DIRECTORY_SEPARATOR;
41+
$runwayCredsFile = $this->credsFile ?: $baseDir . '.runway-creds.json';
42+
43+
// Check for runway creds
44+
if (!file_exists($runwayCredsFile)) {
45+
$io->error('Missing .runway-creds.json. Please run the \'ai:init\' command first.', true);
46+
return 1;
47+
}
48+
49+
$io->info('Let\'s gather some project details to generate AI coding instructions.', true);
50+
51+
// Ask questions
52+
$projectDesc = $io->prompt('Please describe what your project is for?');
53+
$database = $io->prompt('What database are you planning on using? (e.g. MySQL, SQLite, PostgreSQL, none)', 'none');
54+
$templating = $io->prompt('What HTML templating engine will you plan on using (if any)? (recommend latte)', 'latte');
55+
$security = $io->confirm('Is security an important element of this project?', 'y');
56+
$performance = $io->confirm('Is performance and speed an important part of this project?', 'y');
57+
$composerLibs = $io->prompt('What major composer libraries will you be using if you know them right now?', 'none');
58+
$envSetup = $io->prompt('How will you set up your development environment? (e.g. Docker, Vagrant, PHP dev server, other)', 'Docker');
59+
$teamSize = $io->prompt('How many developers will be working on this project?', '1');
60+
$api = $io->confirm('Will this project expose an API?', 'n');
61+
$other = $io->prompt('Any other important requirements or context? (optional)', 'no');
62+
63+
// Prepare prompt for LLM
64+
$contextFile = $baseDir . '.github/copilot-instructions.md';
65+
$context = file_exists($contextFile) ? file_get_contents($contextFile) : '';
66+
$userDetails = [
67+
'Project Description' => $projectDesc,
68+
'Database' => $database,
69+
'Templating Engine' => $templating,
70+
'Security Important' => $security ? 'yes' : 'no',
71+
'Performance Important' => $performance ? 'yes' : 'no',
72+
'Composer Libraries' => $composerLibs,
73+
'Environment Setup' => $envSetup,
74+
'Team Size' => $teamSize,
75+
'API' => $api ? 'yes' : 'no',
76+
'Other' => $other,
77+
];
78+
$detailsText = "";
79+
foreach ($userDetails as $k => $v) {
80+
$detailsText .= "$k: $v\n";
81+
}
82+
$prompt = <<<EOT
83+
You are an AI coding assistant. Update the following project instructions for this FlightPHP project based on the latest user answers. Only output the new instructions, no extra commentary.
84+
User answers:
85+
$detailsText
86+
Current instructions:
87+
$context
88+
EOT;
89+
90+
// Read LLM creds
91+
$creds = json_decode(file_get_contents($runwayCredsFile), true);
92+
$apiKey = $creds['api_key'] ?? '';
93+
$model = $creds['model'] ?? 'gpt-4o';
94+
$baseUrl = $creds['base_url'] ?? 'https://api.openai.com';
95+
96+
// Prepare curl call (OpenAI compatible)
97+
$headers = [
98+
'Content-Type: application/json',
99+
'Authorization: Bearer ' . $apiKey,
100+
];
101+
$data = [
102+
'model' => $model,
103+
'messages' => [
104+
['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.'],
105+
['role' => 'user', 'content' => $prompt],
106+
],
107+
'temperature' => 0.2,
108+
];
109+
$jsonData = json_encode($data);
110+
111+
// add info line that this may take a few minutes
112+
$io->info('Generating AI instructions, this may take a few minutes...', true);
113+
114+
$result = $this->callLlmApi($baseUrl, $headers, $jsonData, $io);
115+
if ($result === false) {
116+
return 1;
117+
}
118+
$response = json_decode($result, true);
119+
$instructions = $response['choices'][0]['message']['content'] ?? '';
120+
if (!$instructions) {
121+
$io->error('No instructions returned from LLM.', true);
122+
return 1;
123+
}
124+
125+
// Write to files
126+
$io->info('Updating .github/copilot-instructions.md, .cursor/rules/project-overview.mdc, and .windsurfrules...', true);
127+
if (!is_dir($baseDir . '.github')) {
128+
mkdir($baseDir . '.github', 0755, true);
129+
}
130+
if (!is_dir($baseDir . '.cursor/rules')) {
131+
mkdir($baseDir . '.cursor/rules', 0755, true);
132+
}
133+
file_put_contents($baseDir . '.github/copilot-instructions.md', $instructions);
134+
file_put_contents($baseDir . '.cursor/rules/project-overview.mdc', $instructions);
135+
file_put_contents($baseDir . '.windsurfrules', $instructions);
136+
$io->ok('AI instructions updated successfully.', true);
137+
return 0;
138+
}
139+
140+
/**
141+
* Make the LLM API call using curl
142+
*
143+
* @param string $baseUrl
144+
* @param array<int,string> $headers
145+
* @param string $jsonData
146+
* @param object $io
147+
*
148+
* @return string|false
149+
*
150+
* @codeCoverageIgnore
151+
*/
152+
protected function callLlmApi($baseUrl, $headers, $jsonData, $io)
153+
{
154+
$ch = curl_init($baseUrl . '/v1/chat/completions');
155+
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
156+
curl_setopt($ch, CURLOPT_POST, true);
157+
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
158+
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData);
159+
$result = curl_exec($ch);
160+
if (curl_errno($ch)) {
161+
$io->error('Failed to call LLM API: ' . curl_error($ch), true);
162+
curl_close($ch);
163+
return false;
164+
}
165+
curl_close($ch);
166+
return $result;
167+
}
168+
}

flight/commands/AiInitCommand.php

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace flight\commands;
6+
7+
use Ahc\Cli\Input\Command;
8+
9+
/**
10+
* @property-read ?string $gitignoreFile
11+
* @property-read ?string $credsFile
12+
*/
13+
class AiInitCommand extends Command
14+
{
15+
/**
16+
* Constructor for the AiInitCommand class.
17+
*
18+
* Initializes the command instance and sets up any required dependencies.
19+
*/
20+
public function __construct()
21+
{
22+
parent::__construct('ai:init', 'Initialize LLM API credentials and settings');
23+
$this
24+
->option('--gitignore-file', 'Path to .gitignore file', null, '')
25+
->option('--creds-file', 'Path to .runway-creds.json file', null, '');
26+
}
27+
28+
/**
29+
* Executes the function
30+
*
31+
* @return int
32+
*/
33+
public function execute()
34+
{
35+
$io = $this->app()->io();
36+
37+
$io->info('Welcome to AI Init!', true);
38+
39+
$baseDir = getcwd() . DIRECTORY_SEPARATOR;
40+
$runwayCredsFile = $this->credsFile ?: $baseDir . '.runway-creds.json';
41+
$gitignoreFile = $this->gitignoreFile ?: $baseDir . '.gitignore';
42+
43+
// make sure the .runway-creds.json file is not already present
44+
if (file_exists($runwayCredsFile)) {
45+
$io->error('.runway-creds.json file already exists. Please remove it before running this command.', true);
46+
// prompt to overwrite
47+
$overwrite = $io->confirm('Do you want to overwrite the existing .runway-creds.json file?', 'n');
48+
if ($overwrite === false) {
49+
$io->info('Exiting without changes.', true);
50+
return 0;
51+
}
52+
}
53+
54+
// Prompt for API provider with validation
55+
$allowedApis = [
56+
'1' => 'openai',
57+
'2' => 'grok',
58+
'3' => 'claude'
59+
];
60+
$apiChoice = strtolower(trim($io->choice('Which LLM API do you want to use?', $allowedApis, '1')));
61+
$api = $allowedApis[$apiChoice] ?? 'openai';
62+
63+
// Prompt for base URL with validation
64+
switch ($api) {
65+
case 'openai':
66+
$defaultBaseUrl = 'https://api.openai.com';
67+
break;
68+
case 'grok':
69+
$defaultBaseUrl = 'https://api.x.ai';
70+
break;
71+
case 'claude':
72+
$defaultBaseUrl = 'https://api.anthropic.com';
73+
break;
74+
}
75+
$baseUrl = trim($io->prompt('Enter the base URL for the LLM API', $defaultBaseUrl));
76+
if (empty($baseUrl) || !filter_var($baseUrl, FILTER_VALIDATE_URL)) {
77+
$io->error('Base URL cannot be empty and must be a valid URL.', true);
78+
return 1;
79+
}
80+
81+
// Validate API key input
82+
$apiKey = trim($io->prompt('Enter your API key for ' . $api));
83+
if (empty($apiKey)) {
84+
$io->error('API key cannot be empty. Please enter a valid API key.', true);
85+
return 1;
86+
}
87+
88+
// Validate model input
89+
switch ($api) {
90+
case 'openai':
91+
$defaultModel = 'gpt-4o';
92+
break;
93+
case 'grok':
94+
$defaultModel = 'grok-3-beta';
95+
break;
96+
case 'claude':
97+
$defaultModel = 'claude-3-opus';
98+
break;
99+
}
100+
$model = trim($io->prompt('Enter the model name you want to use (e.g. gpt-4, claude-3-opus, etc)', $defaultModel));
101+
102+
$creds = [
103+
'provider' => $api,
104+
'api_key' => $apiKey,
105+
'model' => $model,
106+
'base_url' => $baseUrl,
107+
];
108+
109+
$json = json_encode($creds, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
110+
$file = $runwayCredsFile;
111+
file_put_contents($file, $json);
112+
113+
// change permissions to 600
114+
chmod($file, 0600);
115+
116+
$io->ok('Credentials saved to ' . $file, true);
117+
118+
// run a check to make sure that the creds file is in the .gitignore file
119+
// use $gitignoreFile instead of hardcoded path
120+
if (!file_exists($gitignoreFile)) {
121+
// create the .gitignore file if it doesn't exist
122+
file_put_contents($gitignoreFile, basename($runwayCredsFile) . "\n");
123+
$io->info(basename($gitignoreFile) . ' file created and ' . basename($runwayCredsFile) . ' added to it.', true);
124+
} else {
125+
// check if the creds file is already in the .gitignore file
126+
$gitignoreContents = file_get_contents($gitignoreFile);
127+
if (strpos($gitignoreContents, basename($runwayCredsFile)) === false) {
128+
// add the creds file to the .gitignore file
129+
file_put_contents($gitignoreFile, "\n" . basename($runwayCredsFile) . "\n", FILE_APPEND);
130+
$io->info(basename($runwayCredsFile) . ' added to ' . basename($gitignoreFile) . ' file.', true);
131+
} else {
132+
$io->info(basename($runwayCredsFile) . ' is already in the ' . basename($gitignoreFile) . ' file.', true);
133+
}
134+
}
135+
136+
return 0;
137+
}
138+
}

0 commit comments

Comments
 (0)