Merge pull request #652 from flightphp/ai-helpers

Added AI commands and instructions for the repo.
master v3.16.0
n0nag0n 2 days ago committed by GitHub
commit d43f2e9db5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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. PHP 8 or greater also supported, but avoid PHP 8+ 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)

3
.gitignore vendored

@ -8,4 +8,5 @@ coverage/
*.sublime*
clover.xml
phpcs.xml
.runway-config.json
.runway-config.json
.runway-creds.json

@ -0,0 +1,168 @@
<?php
declare(strict_types=1);
namespace flight\commands;
use Ahc\Cli\Input\Command;
/**
* @property-read ?string $credsFile
* @property-read ?string $baseDir
*/
class AiGenerateInstructionsCommand extends Command
{
/**
* Constructor for the AiGenerateInstructionsCommand class.
*
* Initializes a new instance of the command.
*/
public function __construct()
{
parent::__construct('ai:generate-instructions', 'Generate project-specific AI coding instructions');
$this->option('--creds-file', 'Path to .runway-creds.json file', null, '');
$this->option('--base-dir', 'Project base directory (for testing or custom use)', null, '');
}
/**
* Executes the command logic for generating AI instructions.
*
* This method is called to perform the main functionality of the
* AiGenerateInstructionsCommand. It should contain the steps required
* to generate and output instructions using AI, based on the command's
* configuration and input.
*
* @return int
*/
public function execute()
{
$io = $this->app()->io();
$baseDir = $this->baseDir ? rtrim($this->baseDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR : getcwd() . DIRECTORY_SEPARATOR;
$runwayCredsFile = $this->credsFile ?: $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 = <<<EOT
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.
User answers:
$detailsText
Current instructions:
$context
EOT;
// 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);
$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;
}
// 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;
}
/**
* Make the LLM API call using curl
*
* @param string $baseUrl
* @param array<int,string> $headers
* @param string $jsonData
* @param object $io
*
* @return string|false
*
* @codeCoverageIgnore
*/
protected function callLlmApi($baseUrl, $headers, $jsonData, $io)
{
$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);
curl_close($ch);
return false;
}
curl_close($ch);
return $result;
}
}

@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
namespace flight\commands;
use Ahc\Cli\Input\Command;
/**
* @property-read ?string $gitignoreFile
* @property-read ?string $credsFile
*/
class AiInitCommand extends Command
{
/**
* Constructor for the AiInitCommand class.
*
* Initializes the command instance and sets up any required dependencies.
*/
public function __construct()
{
parent::__construct('ai:init', 'Initialize LLM API credentials and settings');
$this
->option('--gitignore-file', 'Path to .gitignore file', null, '')
->option('--creds-file', 'Path to .runway-creds.json file', null, '');
}
/**
* Executes the function
*
* @return int
*/
public function execute()
{
$io = $this->app()->io();
$io->info('Welcome to AI Init!', true);
$baseDir = getcwd() . DIRECTORY_SEPARATOR;
$runwayCredsFile = $this->credsFile ?: $baseDir . '.runway-creds.json';
$gitignoreFile = $this->gitignoreFile ?: $baseDir . '.gitignore';
// 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
$allowedApis = [
'1' => 'openai',
'2' => 'grok',
'3' => 'claude'
];
$apiChoice = strtolower(trim($io->choice('Which LLM API do you want to use?', $allowedApis, '1')));
$api = $allowedApis[$apiChoice] ?? 'openai';
// Prompt for base URL with validation
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;
}
$baseUrl = trim($io->prompt('Enter the base URL for the LLM API', $defaultBaseUrl));
if (empty($baseUrl) || !filter_var($baseUrl, FILTER_VALIDATE_URL)) {
$io->error('Base URL cannot be empty and must be a valid URL.', true);
return 1;
}
// Validate API key input
$apiKey = trim($io->prompt('Enter your API key for ' . $api));
if (empty($apiKey)) {
$io->error('API key cannot be empty. Please enter a valid API key.', true);
return 1;
}
// Validate model input
switch ($api) {
case 'openai':
$defaultModel = 'gpt-4o';
break;
case 'grok':
$defaultModel = 'grok-3-beta';
break;
case 'claude':
$defaultModel = 'claude-3-opus';
break;
}
$model = trim($io->prompt('Enter the model name you want to use (e.g. gpt-4, claude-3-opus, etc)', $defaultModel));
$creds = [
'provider' => $api,
'api_key' => $apiKey,
'model' => $model,
'base_url' => $baseUrl,
];
$json = json_encode($creds, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
$file = $runwayCredsFile;
file_put_contents($file, $json);
// change permissions to 600
chmod($file, 0600);
$io->ok('Credentials saved to ' . $file, true);
// run a check to make sure that the creds file is in the .gitignore file
// use $gitignoreFile instead of hardcoded path
if (!file_exists($gitignoreFile)) {
// create the .gitignore file if it doesn't exist
file_put_contents($gitignoreFile, basename($runwayCredsFile) . "\n");
$io->info(basename($gitignoreFile) . ' file created and ' . basename($runwayCredsFile) . ' added to it.', true);
} else {
// check if the creds file is already in the .gitignore file
$gitignoreContents = file_get_contents($gitignoreFile);
if (strpos($gitignoreContents, basename($runwayCredsFile)) === false) {
// add the creds file to the .gitignore file
file_put_contents($gitignoreFile, "\n" . basename($runwayCredsFile) . "\n", FILE_APPEND);
$io->info(basename($runwayCredsFile) . ' added to ' . basename($gitignoreFile) . ' file.', true);
} else {
$io->info(basename($runwayCredsFile) . ' is already in the ' . basename($gitignoreFile) . ' file.', true);
}
}
return 0;
}
}

@ -0,0 +1,199 @@
<?php
declare(strict_types=1);
namespace tests\commands;
use Ahc\Cli\Application;
use Ahc\Cli\IO\Interactor;
use flight\commands\AiGenerateInstructionsCommand;
use PHPUnit\Framework\TestCase;
class AiGenerateInstructionsCommandTest extends TestCase
{
protected static $in;
protected static $ou;
protected $baseDir;
protected $runwayCredsFile;
public function setUp(): void
{
self::$in = __DIR__ . DIRECTORY_SEPARATOR . 'input.test' . uniqid('', true) . '.txt';
self::$ou = __DIR__ . DIRECTORY_SEPARATOR . 'output.test' . uniqid('', true) . '.txt';
file_put_contents(self::$in, '');
file_put_contents(self::$ou, '');
$this->baseDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'flightphp-test-basedir-' . uniqid('', true) . DIRECTORY_SEPARATOR;
if (!is_dir($this->baseDir)) {
mkdir($this->baseDir, 0777, true);
}
$this->runwayCredsFile = $this->baseDir . 'dummy-creds.json';
if (file_exists($this->runwayCredsFile)) {
unlink($this->runwayCredsFile);
}
@unlink($this->baseDir . '.github/copilot-instructions.md');
@unlink($this->baseDir . '.cursor/rules/project-overview.mdc');
@unlink($this->baseDir . '.windsurfrules');
@rmdir($this->baseDir . '.github');
@rmdir($this->baseDir . '.cursor/rules');
@rmdir($this->baseDir . '.cursor');
}
public function tearDown(): void
{
if (file_exists(self::$in)) {
unlink(self::$in);
}
if (file_exists(self::$ou)) {
unlink(self::$ou);
}
if (file_exists($this->runwayCredsFile)) {
unlink($this->runwayCredsFile);
}
@unlink($this->baseDir . '.github/copilot-instructions.md');
@unlink($this->baseDir . '.cursor/rules/project-overview.mdc');
@unlink($this->baseDir . '.windsurfrules');
@rmdir($this->baseDir . '.github');
@rmdir($this->baseDir . '.cursor/rules');
@rmdir($this->baseDir . '.cursor');
if (is_dir($this->baseDir . '.cursor/rules')) {
@rmdir($this->baseDir . '.cursor/rules');
}
if (is_dir($this->baseDir . '.cursor')) {
@rmdir($this->baseDir . '.cursor');
}
if (is_dir($this->baseDir . '.github')) {
@rmdir($this->baseDir . '.github');
}
if (is_dir($this->baseDir)) {
@rmdir($this->baseDir);
}
}
protected function newApp($command): Application
{
$app = new Application('test', '0.0.1', function ($exitCode) {
return $exitCode;
});
$app->io(new Interactor(self::$in, self::$ou));
$app->add($command);
return $app;
}
protected function setInput(array $lines): void
{
file_put_contents(self::$in, implode("\n", $lines) . "\n");
}
public function testFailsIfCredsFileMissing()
{
$this->setInput([
'desc', 'none', 'latte', 'y', 'y', 'none', 'Docker', '1', 'n', 'no'
]);
$cmd = $this->getMockBuilder(AiGenerateInstructionsCommand::class)
->onlyMethods(['callLlmApi'])
->getMock();
$app = $this->newApp($cmd);
$result = $app->handle([
'runway', 'ai:generate-instructions',
'--creds-file=' . $this->runwayCredsFile,
'--base-dir=' . $this->baseDir
]);
$this->assertSame(1, $result);
$this->assertFileDoesNotExist($this->baseDir . '.github/copilot-instructions.md');
}
public function testWritesInstructionsToFiles()
{
$creds = [
'api_key' => 'key',
'model' => 'gpt-4o',
'base_url' => 'https://api.openai.com',
];
file_put_contents($this->runwayCredsFile, json_encode($creds));
$this->setInput([
'desc', 'mysql', 'latte', 'y', 'y', 'flight/lib', 'Docker', '2', 'y', 'context info'
]);
$mockInstructions = "# Project Instructions\n\nUse MySQL, Latte, Docker.";
$cmd = $this->getMockBuilder(AiGenerateInstructionsCommand::class)
->onlyMethods(['callLlmApi'])
->getMock();
$cmd->expects($this->once())
->method('callLlmApi')
->willReturn(json_encode([
'choices' => [
['message' => ['content' => $mockInstructions]]
]
]));
$app = $this->newApp($cmd);
$result = $app->handle([
'runway', 'ai:generate-instructions',
'--creds-file=' . $this->runwayCredsFile,
'--base-dir=' . $this->baseDir
]);
$this->assertSame(0, $result);
$this->assertFileExists($this->baseDir . '.github/copilot-instructions.md');
$this->assertFileExists($this->baseDir . '.cursor/rules/project-overview.mdc');
$this->assertFileExists($this->baseDir . '.windsurfrules');
$this->assertStringContainsString('MySQL', file_get_contents($this->baseDir . '.github/copilot-instructions.md'));
$this->assertStringContainsString('MySQL', file_get_contents($this->baseDir . '.cursor/rules/project-overview.mdc'));
$this->assertStringContainsString('MySQL', file_get_contents($this->baseDir . '.windsurfrules'));
}
public function testNoInstructionsReturnedFromLlm()
{
$creds = [
'api_key' => 'key',
'model' => 'gpt-4o',
'base_url' => 'https://api.openai.com',
];
file_put_contents($this->runwayCredsFile, json_encode($creds));
$this->setInput([
'desc', 'mysql', 'latte', 'y', 'y', 'flight/lib', 'Docker', '2', 'y', 'context info'
]);
$cmd = $this->getMockBuilder(AiGenerateInstructionsCommand::class)
->onlyMethods(['callLlmApi'])
->getMock();
$cmd->expects($this->once())
->method('callLlmApi')
->willReturn(json_encode([
'choices' => [
['message' => ['content' => '']]
]
]));
$app = $this->newApp($cmd);
$result = $app->handle([
'runway', 'ai:generate-instructions',
'--creds-file=' . $this->runwayCredsFile,
'--base-dir=' . $this->baseDir
]);
$this->assertSame(1, $result);
$this->assertFileDoesNotExist($this->baseDir . '.github/copilot-instructions.md');
}
public function testLlmApiCallFails()
{
$creds = [
'api_key' => 'key',
'model' => 'gpt-4o',
'base_url' => 'https://api.openai.com',
];
file_put_contents($this->runwayCredsFile, json_encode($creds));
$this->setInput([
'desc', 'mysql', 'latte', 'y', 'y', 'flight/lib', 'Docker', '2', 'y', 'context info'
]);
$cmd = $this->getMockBuilder(AiGenerateInstructionsCommand::class)
->onlyMethods(['callLlmApi'])
->getMock();
$cmd->expects($this->once())
->method('callLlmApi')
->willReturn(false);
$app = $this->newApp($cmd);
$result = $app->handle([
'runway', 'ai:generate-instructions',
'--creds-file=' . $this->runwayCredsFile,
'--base-dir=' . $this->baseDir
]);
$this->assertSame(1, $result);
$this->assertFileDoesNotExist($this->baseDir . '.github/copilot-instructions.md');
}
}

@ -0,0 +1,266 @@
<?php
declare(strict_types=1);
namespace tests\commands;
use Ahc\Cli\Application;
use Ahc\Cli\IO\Interactor;
use flight\commands\AiInitCommand;
use PHPUnit\Framework\TestCase;
class AiInitCommandTest extends TestCase
{
protected static $in;
protected static $ou;
protected $baseDir;
protected $runwayCredsFile;
protected $gitignoreFile;
public function setUp(): void
{
self::$in = __DIR__ . DIRECTORY_SEPARATOR . 'input.test' . uniqid('', true) . '.txt';
self::$ou = __DIR__ . DIRECTORY_SEPARATOR . 'output.test' . uniqid('', true) . '.txt';
file_put_contents(self::$in, '');
file_put_contents(self::$ou, '');
$this->baseDir = getcwd() . DIRECTORY_SEPARATOR;
$this->runwayCredsFile = __DIR__ . DIRECTORY_SEPARATOR . 'dummy-creds-' . uniqid('', true) . '.json';
$this->gitignoreFile = __DIR__ . DIRECTORY_SEPARATOR . 'dummy-gitignore-' . uniqid('', true);
if (file_exists($this->runwayCredsFile)) {
unlink($this->runwayCredsFile);
}
if (file_exists($this->gitignoreFile)) {
unlink($this->gitignoreFile);
}
}
public function tearDown(): void
{
if (file_exists(self::$in)) {
unlink(self::$in);
}
if (file_exists(self::$ou)) {
unlink(self::$ou);
}
if (file_exists($this->runwayCredsFile)) {
if (is_dir($this->runwayCredsFile)) {
rmdir($this->runwayCredsFile);
} else {
unlink($this->runwayCredsFile);
}
}
if (file_exists($this->gitignoreFile)) {
unlink($this->gitignoreFile);
}
}
protected function newApp(): Application
{
$app = new Application('test', '0.0.1', function ($exitCode) {
return $exitCode;
});
$app->io(new Interactor(self::$in, self::$ou));
return $app;
}
protected function setInput(array $lines): void
{
file_put_contents(self::$in, implode("\n", $lines) . "\n");
}
public function testInitCreatesCredsAndGitignore()
{
$this->setInput([
'1', // provider
'', // accept default base url
'test-key', // api key
'', // accept default model
]);
$app = $this->newApp();
$app->add(new AiInitCommand());
$result = $app->handle([
'runway', 'ai:init',
'--creds-file=' . $this->runwayCredsFile,
'--gitignore-file=' . $this->gitignoreFile
]);
$this->assertSame(0, $result);
$this->assertFileExists($this->runwayCredsFile);
$creds = json_decode(file_get_contents($this->runwayCredsFile), true);
$this->assertSame('openai', $creds['provider']);
$this->assertSame('test-key', $creds['api_key']);
$this->assertSame('gpt-4o', $creds['model']);
$this->assertSame('https://api.openai.com', $creds['base_url']);
$this->assertFileExists($this->gitignoreFile);
$this->assertStringContainsString(basename($this->runwayCredsFile), file_get_contents($this->gitignoreFile));
}
public function testInitWithExistingCredsNoOverwrite()
{
file_put_contents($this->runwayCredsFile, '{}');
$this->setInput([
'n', // do not overwrite
]);
$app = $this->newApp();
$app->add(new AiInitCommand());
$result = $app->handle([
'runway', 'ai:init',
'--creds-file=' . $this->runwayCredsFile,
'--gitignore-file=' . $this->gitignoreFile
]);
$this->assertSame(0, $result);
$this->assertSame('{}', file_get_contents($this->runwayCredsFile));
}
public function testInitWithExistingCredsOverwrite()
{
file_put_contents($this->runwayCredsFile, '{}');
$this->setInput([
'y', // overwrite
'2', // provider
'', // accept default base url
'grok-key', // api key
'', // accept default model
]);
$app = $this->newApp();
$app->add(new AiInitCommand());
$result = $app->handle([
'runway', 'ai:init',
'--creds-file=' . $this->runwayCredsFile,
'--gitignore-file=' . $this->gitignoreFile
]);
$this->assertSame(0, $result);
$creds = json_decode(file_get_contents($this->runwayCredsFile), true);
$this->assertSame('grok', $creds['provider']);
$this->assertSame('grok-key', $creds['api_key']);
$this->assertSame('grok-3-beta', $creds['model']);
$this->assertSame('https://api.x.ai', $creds['base_url']);
}
public function testEmptyApiKeyPromptsAgain()
{
$this->setInput([
'1',
'', // accept default base url
'', // empty api key, should error and exit
]);
$app = $this->newApp();
$app->add(new AiInitCommand());
$result = $app->handle([
'runway', 'ai:init',
'--creds-file=' . $this->runwayCredsFile,
'--gitignore-file=' . $this->gitignoreFile
]);
$this->assertSame(1, $result);
$this->assertFileDoesNotExist($this->runwayCredsFile);
}
public function testEmptyModelPrompts()
{
$this->setInput([
'1',
'',
'key',
'', // accept default model (should use default)
]);
$app = $this->newApp();
$app->add(new AiInitCommand());
$result = $app->handle([
'runway', 'ai:init',
'--creds-file=' . $this->runwayCredsFile,
'--gitignore-file=' . $this->gitignoreFile
]);
$this->assertSame(0, $result);
$creds = json_decode(file_get_contents($this->runwayCredsFile), true);
$this->assertSame('gpt-4o', $creds['model']);
}
public function testGitignoreAlreadyHasCreds()
{
file_put_contents($this->gitignoreFile, basename($this->runwayCredsFile) . "\n");
$this->setInput([
'1',
'',
'key',
'',
]);
$app = $this->newApp();
$app->add(new AiInitCommand());
$result = $app->handle([
'runway', 'ai:init',
'--creds-file=' . $this->runwayCredsFile,
'--gitignore-file=' . $this->gitignoreFile
]);
$this->assertSame(0, $result);
$this->assertFileExists($this->gitignoreFile);
$lines = file($this->gitignoreFile, FILE_IGNORE_NEW_LINES);
$this->assertContains(basename($this->runwayCredsFile), $lines);
$this->assertCount(1, array_filter($lines, function ($l) {
return trim($l) === basename($this->runwayCredsFile);
}));
}
public function testInitWithClaudeProvider()
{
$this->setInput([
'3', // provider (claude)
'', // accept default base url
'claude-key', // api key
'', // accept default model
]);
$app = $this->newApp();
$app->add(new AiInitCommand());
$result = $app->handle([
'runway', 'ai:init',
'--creds-file=' . $this->runwayCredsFile,
'--gitignore-file=' . $this->gitignoreFile
]);
$this->assertSame(0, $result);
$creds = json_decode(file_get_contents($this->runwayCredsFile), true);
$this->assertSame('claude', $creds['provider']);
$this->assertSame('claude-key', $creds['api_key']);
$this->assertSame('claude-3-opus', $creds['model']);
$this->assertSame('https://api.anthropic.com', $creds['base_url']);
}
public function testAddsCredsFileToExistingGitignoreIfMissing()
{
// .gitignore exists but does not contain creds file
file_put_contents($this->gitignoreFile, "vendor\nnode_modules\n.DS_Store\n");
$this->setInput([
'1', // provider
'', // accept default base url
'test-key', // api key
'', // accept default model
]);
$app = $this->newApp();
$app->add(new AiInitCommand());
$result = $app->handle([
'runway', 'ai:init',
'--creds-file=' . $this->runwayCredsFile,
'--gitignore-file=' . $this->gitignoreFile
]);
$this->assertSame(0, $result);
$lines = file($this->gitignoreFile, FILE_IGNORE_NEW_LINES);
$this->assertContains(basename($this->runwayCredsFile), $lines);
$this->assertCount(1, array_filter($lines, function ($l) {
return trim($l) === basename($this->runwayCredsFile);
}));
}
public function testInvalidBaseUrlFails()
{
$this->setInput([
'1', // provider
'not-a-valid-url', // invalid base url
]);
$app = $this->newApp();
$app->add(new AiInitCommand());
$result = $app->handle([
'runway', 'ai:init',
'--creds-file=' . $this->runwayCredsFile,
'--gitignore-file=' . $this->gitignoreFile
]);
$this->assertSame(1, $result);
$this->assertFileDoesNotExist($this->runwayCredsFile);
}
}

@ -98,9 +98,9 @@ Flight::group('', function () {
Flight::render('template.phtml', ['name' => $name]);
});
Flight::route('/template-data/@data', function ($data) {
Flight::render('template.phtml', ['data' => $data]);
});
Flight::route('/template-data/@data', function ($data) {
Flight::render('template.phtml', ['data' => $data]);
});
// Test 8: Throw an error
Flight::route('/error', function () {

Loading…
Cancel
Save