Skip to content

Commit 6742770

Browse files
committed
Added unit tests and adjusted some syntax
1 parent dcb7ff9 commit 6742770

File tree

5 files changed

+637
-125
lines changed

5 files changed

+637
-125
lines changed

flight/commands/AiGenerateInstructionsCommand.php

Lines changed: 70 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,44 @@
11
<?php
2-
namespace app\commands;
2+
3+
declare(strict_types=1);
4+
5+
namespace flight\commands;
36

47
use Ahc\Cli\Input\Command;
58

9+
/**
10+
* @property-read ?string $credsFile
11+
* @property-read ?string $baseDir
12+
*/
613
class AiGenerateInstructionsCommand extends Command
714
{
15+
/**
16+
* Constructor for the AiGenerateInstructionsCommand class.
17+
*
18+
* Initializes a new instance of the command.
19+
*/
820
public function __construct()
921
{
1022
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, '');
1125
}
1226

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+
*/
1337
public function execute()
1438
{
1539
$io = $this->app()->io();
16-
$baseDir = getcwd() . DIRECTORY_SEPARATOR;
17-
$runwayCredsFile = $baseDir . '.runway-creds.json';
40+
$baseDir = $this->baseDir ? rtrim($this->baseDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR : getcwd() . DIRECTORY_SEPARATOR;
41+
$runwayCredsFile = $this->credsFile ?: $baseDir . '.runway-creds.json';
1842

1943
// Check for runway creds
2044
if (!file_exists($runwayCredsFile)) {
@@ -55,11 +79,13 @@ public function execute()
5579
foreach ($userDetails as $k => $v) {
5680
$detailsText .= "$k: $v\n";
5781
}
58-
$prompt = "" .
59-
"You are an AI coding assistant. Update the following project instructions for this FlightPHP project based on the latest user answers. " .
60-
"Only output the new instructions, no extra commentary.\n" .
61-
"User answers:\n$detailsText\n" .
62-
"Current instructions:\n$context\n";
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;
6389

6490
// Read LLM creds
6591
$creds = json_decode(file_get_contents($runwayCredsFile), true);
@@ -82,20 +108,13 @@ public function execute()
82108
];
83109
$jsonData = json_encode($data);
84110

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

88-
$ch = curl_init($baseUrl . '/v1/chat/completions');
89-
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
90-
curl_setopt($ch, CURLOPT_POST, true);
91-
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
92-
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData);
93-
$result = curl_exec($ch);
94-
if (curl_errno($ch)) {
95-
$io->error('Failed to call LLM API: ' . curl_error($ch), true);
114+
$result = $this->callLlmApi($baseUrl, $headers, $jsonData, $io);
115+
if ($result === false) {
96116
return 1;
97117
}
98-
curl_close($ch);
99118
$response = json_decode($result, true);
100119
$instructions = $response['choices'][0]['message']['content'] ?? '';
101120
if (!$instructions) {
@@ -108,13 +127,42 @@ public function execute()
108127
if (!is_dir($baseDir . '.github')) {
109128
mkdir($baseDir . '.github', 0755, true);
110129
}
111-
if (!is_dir($baseDir . '.cursor/rules')) {
112-
mkdir($baseDir . '.cursor/rules', 0755, true);
113-
}
130+
if (!is_dir($baseDir . '.cursor/rules')) {
131+
mkdir($baseDir . '.cursor/rules', 0755, true);
132+
}
114133
file_put_contents($baseDir . '.github/copilot-instructions.md', $instructions);
115134
file_put_contents($baseDir . '.cursor/rules/project-overview.mdc', $instructions);
116135
file_put_contents($baseDir . '.windsurfrules', $instructions);
117136
$io->ok('AI instructions updated successfully.', true);
118137
return 0;
119138
}
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+
}
120168
}

flight/commands/AiInitCommand.php

Lines changed: 99 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,138 +1,137 @@
11
<?php
2-
namespace app\commands;
32

4-
use Ahc\Cli\Input\Option;
5-
use Ahc\Cli\Output\Writer;
6-
use Ahc\Cli\IO\Interactor;
3+
declare(strict_types=1);
4+
5+
namespace flight\commands;
6+
77
use Ahc\Cli\Input\Command;
88

9+
/**
10+
* @property-read ?string $gitignoreFile
11+
* @property-read ?string $credsFile
12+
*/
913
class AiInitCommand extends Command
1014
{
15+
/**
16+
* Constructor for the AiInitCommand class.
17+
*
18+
* Initializes the command instance and sets up any required dependencies.
19+
*/
1120
public function __construct()
1221
{
1322
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, '');
1426
}
1527

1628
/**
1729
* Executes the function
1830
*
19-
* @return void
31+
* @return int
2032
*/
2133
public function execute()
2234
{
23-
$io = $this->app()->io();
35+
$io = $this->app()->io();
2436

2537
$io->info('Welcome to AI Init!', true);
2638

27-
// if runway creds already exist, prompt to overwrite
28-
$baseDir = getcwd() . DIRECTORY_SEPARATOR;
29-
$runwayCredsFile = $baseDir . '.runway-creds.json';
30-
31-
// make sure the .runway-creds.json file is not already present
32-
if (file_exists($runwayCredsFile)) {
33-
$io->error('.runway-creds.json file already exists. Please remove it before running this command.', true);
34-
// prompt to overwrite
35-
$overwrite = $io->confirm('Do you want to overwrite the existing .runway-creds.json file?', 'n');
36-
if ($overwrite === false) {
37-
$io->info('Exiting without changes.', true);
38-
return 0;
39-
}
40-
}
41-
42-
// Prompt for API provider with validation
43-
do {
44-
$api = $io->prompt('Which LLM API do you want to use? (openai, grok, claude) [openai]', 'openai');
45-
$api = strtolower(trim($api));
46-
if (!in_array($api, ['openai', 'grok', 'claude'], true)) {
47-
$io->error('Invalid API provider. Please enter one of: openai, grok, claude.', true);
48-
$api = '';
49-
}
50-
} while (empty($api));
51-
52-
// Prompt for base URL with validation
53-
do {
54-
switch($api) {
55-
case 'openai':
56-
$defaultBaseUrl = 'https://api.openai.com';
57-
break;
58-
case 'grok':
59-
$defaultBaseUrl = 'https://api.x.ai';
60-
break;
61-
case 'claude':
62-
$defaultBaseUrl = 'https://api.anthropic.com';
63-
break;
64-
default:
65-
$defaultBaseUrl = '';
66-
}
67-
$baseUrl = $io->prompt('Enter the base URL for the LLM API', $defaultBaseUrl);
68-
$baseUrl = trim($baseUrl);
69-
if (empty($baseUrl) || !filter_var($baseUrl, FILTER_VALIDATE_URL)) {
70-
$io->error('Base URL cannot be empty and must be a valid URL.', true);
71-
$baseUrl = '';
72-
}
73-
} while (empty($baseUrl));
39+
$baseDir = getcwd() . DIRECTORY_SEPARATOR;
40+
$runwayCredsFile = $this->credsFile ?: $baseDir . '.runway-creds.json';
41+
$gitignoreFile = $this->gitignoreFile ?: $baseDir . '.gitignore';
7442

75-
// Validate API key input
76-
do {
77-
$apiKey = $io->prompt('Enter your API key for ' . $api);
78-
if (empty(trim($apiKey))) {
79-
$io->error('API key cannot be empty. Please enter a valid API key.', true);
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;
8051
}
81-
} while (empty(trim($apiKey)));
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+
}
8287

8388
// Validate model input
84-
do {
85-
switch($api) {
86-
case 'openai':
87-
$defaultModel = 'gpt-4o';
88-
break;
89-
case 'grok':
90-
$defaultModel = 'grok-3-beta';
91-
break;
92-
case 'claude':
93-
$defaultModel = 'claude-3-opus';
94-
break;
95-
default:
96-
$defaultModel = '';
97-
}
98-
$model = $io->prompt('Enter the model name you want to use (e.g. gpt-4, claude-3-opus, etc)', $defaultModel);
99-
if (empty(trim($model))) {
100-
$io->error('Model name cannot be empty. Please enter a valid model name.', true);
101-
}
102-
} while (empty(trim($model)));
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));
103101

104102
$creds = [
105103
'provider' => $api,
106104
'api_key' => $apiKey,
107105
'model' => $model,
108-
'base_url' => $baseUrl,
106+
'base_url' => $baseUrl,
109107
];
110108

111109
$json = json_encode($creds, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
112110
$file = $runwayCredsFile;
113-
if (file_put_contents($file, $json) === false) {
114-
$io->error('Failed to write credentials to ' . $file, true);
115-
return 1;
116-
}
111+
file_put_contents($file, $json);
112+
113+
// change permissions to 600
114+
chmod($file, 0600);
115+
117116
$io->ok('Credentials saved to ' . $file, true);
118117

119-
// run a check to make sure that the creds file is in the .gitignore file
120-
$gitignoreFile = $baseDir . '.gitignore';
121-
if (!file_exists($gitignoreFile)) {
122-
// create the .gitignore file if it doesn't exist
123-
file_put_contents($gitignoreFile, ".runway-creds.json\n");
124-
$io->info('.gitignore file created and .runway-creds.json added to it.', true);
125-
} else {
126-
// check if the .runway-creds.json file is already in the .gitignore file
127-
$gitignoreContents = file_get_contents($gitignoreFile);
128-
if (strpos($gitignoreContents, '.runway-creds.json') === false) {
129-
// add the .runway-creds.json file to the .gitignore file
130-
file_put_contents($gitignoreFile, "\n.runway-creds.json\n", FILE_APPEND);
131-
$io->info('.runway-creds.json added to .gitignore file.', true);
132-
} else {
133-
$io->info('.runway-creds.json is already in the .gitignore file.', true);
134-
}
135-
}
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+
}
136135

137136
return 0;
138137
}

0 commit comments

Comments
 (0)