diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..50774b9 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,27 @@ +# FlightPHP/Core Project Instructions + +## Overview +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+. + +## Project Guidelines +- PHP 7.4 must be supported. PHP8 or greater also supported, but avoid PHP8+ only features. +- Keep the core library dependency-free (no polyfills or interface-only repositories). +- All Flight projects are meant to be kept simple and fast. Performance is a priority. +- Flight is extensible and when implementing new features, consider how they can be added as plugins or extensions rather than bloating the core library. +- Any new features built into the core should be well-documented and tested. +- Any new features should be added with a focus on simplicity and performance, avoiding unnecessary complexity. +- 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. + +## Development & Testing +- Run tests: `composer test` (uses phpunit/phpunit and spatie/phpunit-watcher) +- Run test server: `composer test-server` or `composer test-server-v2` +- Lint code: `composer lint` (uses phpstan/phpstan, level 6) +- Beautify code: `composer beautify` (uses squizlabs/php_codesniffer, PSR1) +- Check code style: `composer phpcs` +- Test coverage: `composer test-coverage` + +## Coding Standards +- Follow PSR1 coding standards (enforced by PHPCS) +- Use strict comparisons (`===`, `!==`) +- PHPStan level 6 compliance +- Focus on PHP 7.4 compatibility (avoid PHP 8+ only features) diff --git a/.gitignore b/.gitignore index 7f0b216..90cd18f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ coverage/ *.sublime* clover.xml phpcs.xml -.runway-config.json \ No newline at end of file +.runway-config.json +.runway-creds.json diff --git a/flight/commands/AiGenerateInstructionsCommand.php b/flight/commands/AiGenerateInstructionsCommand.php new file mode 100644 index 0000000..bdb2e8b --- /dev/null +++ b/flight/commands/AiGenerateInstructionsCommand.php @@ -0,0 +1,120 @@ +app()->io(); + $baseDir = getcwd() . DIRECTORY_SEPARATOR; + $runwayCredsFile = $baseDir . '.runway-creds.json'; + + // Check for runway creds + if (!file_exists($runwayCredsFile)) { + $io->error('Missing .runway-creds.json. Please run the \'ai:init\' command first.', true); + return 1; + } + + $io->info('Let\'s gather some project details to generate AI coding instructions.', true); + + // Ask questions + $projectDesc = $io->prompt('Please describe what your project is for?'); + $database = $io->prompt('What database are you planning on using? (e.g. MySQL, SQLite, PostgreSQL, none)', 'none'); + $templating = $io->prompt('What HTML templating engine will you plan on using (if any)? (recommend latte)', 'latte'); + $security = $io->confirm('Is security an important element of this project?', 'y'); + $performance = $io->confirm('Is performance and speed an important part of this project?', 'y'); + $composerLibs = $io->prompt('What major composer libraries will you be using if you know them right now?', 'none'); + $envSetup = $io->prompt('How will you set up your development environment? (e.g. Docker, Vagrant, PHP dev server, other)', 'Docker'); + $teamSize = $io->prompt('How many developers will be working on this project?', '1'); + $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 = $baseDir . '.github/copilot-instructions.md'; + $context = file_exists($contextFile) ? file_get_contents($contextFile) : ''; + $userDetails = [ + 'Project Description' => $projectDesc, + 'Database' => $database, + 'Templating Engine' => $templating, + 'Security Important' => $security ? 'yes' : 'no', + 'Performance Important' => $performance ? 'yes' : 'no', + 'Composer Libraries' => $composerLibs, + 'Environment Setup' => $envSetup, + 'Team Size' => $teamSize, + 'API' => $api ? 'yes' : 'no', + 'Other' => $other, + ]; + $detailsText = ""; + foreach ($userDetails as $k => $v) { + $detailsText .= "$k: $v\n"; + } + $prompt = "" . + "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.\n" . + "User answers:\n$detailsText\n" . + "Current instructions:\n$context\n"; + + // Read LLM creds + $creds = json_decode(file_get_contents($runwayCredsFile), true); + $apiKey = $creds['api_key'] ?? ''; + $model = $creds['model'] ?? 'gpt-4o'; + $baseUrl = $creds['base_url'] ?? 'https://api.openai.com'; + + // 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); + + // add info line that this may take a few minutes + $io->info('Generating AI instructions, this may take a few minutes...', true); + + $ch = curl_init($baseUrl . '/v1/chat/completions'); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData); + $result = curl_exec($ch); + if (curl_errno($ch)) { + $io->error('Failed to call LLM API: ' . curl_error($ch), true); + return 1; + } + curl_close($ch); + $response = json_decode($result, true); + $instructions = $response['choices'][0]['message']['content'] ?? ''; + if (!$instructions) { + $io->error('No instructions returned from LLM.', true); + return 1; + } + + // Write to files + $io->info('Updating .github/copilot-instructions.md, .cursor/rules/project-overview.mdc, and .windsurfrules...', true); + if (!is_dir($baseDir . '.github')) { + mkdir($baseDir . '.github', 0755, true); + } + if (!is_dir($baseDir . '.cursor/rules')) { + mkdir($baseDir . '.cursor/rules', 0755, true); + } + file_put_contents($baseDir . '.github/copilot-instructions.md', $instructions); + file_put_contents($baseDir . '.cursor/rules/project-overview.mdc', $instructions); + file_put_contents($baseDir . '.windsurfrules', $instructions); + $io->ok('AI instructions updated successfully.', true); + return 0; + } +} diff --git a/flight/commands/AiInitCommand.php b/flight/commands/AiInitCommand.php new file mode 100644 index 0000000..83fba49 --- /dev/null +++ b/flight/commands/AiInitCommand.php @@ -0,0 +1,139 @@ +app()->io(); + + $io->info('Welcome to AI Init!', true); + + // if runway creds already exist, prompt to overwrite + $baseDir = getcwd() . DIRECTORY_SEPARATOR; + $runwayCredsFile = $baseDir . '.runway-creds.json'; + + // make sure the .runway-creds.json file is not already present + if (file_exists($runwayCredsFile)) { + $io->error('.runway-creds.json file already exists. Please remove it before running this command.', true); + // prompt to overwrite + $overwrite = $io->confirm('Do you want to overwrite the existing .runway-creds.json file?', 'n'); + if ($overwrite === false) { + $io->info('Exiting without changes.', true); + return 0; + } + } + + // Prompt for API provider with validation + do { + $api = $io->prompt('Which LLM API do you want to use? (openai, grok, claude) [openai]', 'openai'); + $api = strtolower(trim($api)); + if (!in_array($api, ['openai', 'grok', 'claude'], true)) { + $io->error('Invalid API provider. Please enter one of: openai, grok, claude.', true); + $api = ''; + } + } while (empty($api)); + + // Prompt for base URL with validation + do { + switch($api) { + case 'openai': + $defaultBaseUrl = 'https://api.openai.com'; + break; + case 'grok': + $defaultBaseUrl = 'https://api.x.ai'; + break; + case 'claude': + $defaultBaseUrl = 'https://api.anthropic.com'; + break; + default: + $defaultBaseUrl = ''; + } + $baseUrl = $io->prompt('Enter the base URL for the LLM API', $defaultBaseUrl); + $baseUrl = trim($baseUrl); + if (empty($baseUrl) || !filter_var($baseUrl, FILTER_VALIDATE_URL)) { + $io->error('Base URL cannot be empty and must be a valid URL.', true); + $baseUrl = ''; + } + } while (empty($baseUrl)); + + // Validate API key input + do { + $apiKey = $io->prompt('Enter your API key for ' . $api); + if (empty(trim($apiKey))) { + $io->error('API key cannot be empty. Please enter a valid API key.', true); + } + } while (empty(trim($apiKey))); + + // Validate model input + do { + switch($api) { + case 'openai': + $defaultModel = 'gpt-4o'; + break; + case 'grok': + $defaultModel = 'grok-3-beta'; + break; + case 'claude': + $defaultModel = 'claude-3-opus'; + break; + default: + $defaultModel = ''; + } + $model = $io->prompt('Enter the model name you want to use (e.g. gpt-4, claude-3-opus, etc)', $defaultModel); + if (empty(trim($model))) { + $io->error('Model name cannot be empty. Please enter a valid model name.', true); + } + } while (empty(trim($model))); + + $creds = [ + 'provider' => $api, + 'api_key' => $apiKey, + 'model' => $model, + 'base_url' => $baseUrl, + ]; + + $json = json_encode($creds, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + $file = $runwayCredsFile; + if (file_put_contents($file, $json) === false) { + $io->error('Failed to write credentials to ' . $file, true); + return 1; + } + $io->ok('Credentials saved to ' . $file, true); + + // run a check to make sure that the creds file is in the .gitignore file + $gitignoreFile = $baseDir . '.gitignore'; + if (!file_exists($gitignoreFile)) { + // create the .gitignore file if it doesn't exist + file_put_contents($gitignoreFile, ".runway-creds.json\n"); + $io->info('.gitignore file created and .runway-creds.json added to it.', true); + } else { + // check if the .runway-creds.json file is already in the .gitignore file + $gitignoreContents = file_get_contents($gitignoreFile); + if (strpos($gitignoreContents, '.runway-creds.json') === false) { + // add the .runway-creds.json file to the .gitignore file + file_put_contents($gitignoreFile, "\n.runway-creds.json\n", FILE_APPEND); + $io->info('.runway-creds.json added to .gitignore file.', true); + } else { + $io->info('.runway-creds.json is already in the .gitignore file.', true); + } + } + + return 0; + } +}