@artilingo/artiframe-cli 1.2.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@artilingo/artiframe-cli",
3
- "version": "1.2.1",
3
+ "version": "1.4.0",
4
4
  "description": "ArtiFrame — Zero-dependency native PHP framework with a powerful multilingual CLI. Scaffold projects, generate views, APIs and classes instantly.",
5
5
  "main": "bin/artiframe.js",
6
6
  "bin": {
package/src/App.php CHANGED
@@ -170,6 +170,17 @@ class App
170
170
 
171
171
  private function routeCommand(string $commandName, array $commandArgs): void
172
172
  {
173
+ if (strpos($commandName, 'table:') === 0) {
174
+ $tableName = substr($commandName, 6);
175
+ (new \ArtiFrame\Cli\Commands\TableCommand($this->translator))->execute(array_merge([$tableName], $commandArgs));
176
+ return;
177
+ }
178
+
179
+ if ($commandName === 'table' && !empty($commandArgs) && $commandArgs[0] === 'list') {
180
+ (new \ArtiFrame\Cli\Commands\TableCommand($this->translator))->execute(['list']);
181
+ return;
182
+ }
183
+
173
184
  switch ($commandName) {
174
185
 
175
186
 
@@ -219,6 +230,18 @@ class App
219
230
  (new \ArtiFrame\Cli\Commands\ServeCommand($this->translator))->execute($commandArgs);
220
231
  break;
221
232
 
233
+ case 'auth':
234
+ (new \ArtiFrame\Cli\Commands\AuthCommand($this->translator))->execute($commandArgs);
235
+ break;
236
+
237
+ case 'suggest':
238
+ (new \ArtiFrame\Cli\Commands\SuggestCommand($this->translator))->execute($commandArgs);
239
+ break;
240
+
241
+ case 'issues':
242
+ (new \ArtiFrame\Cli\Commands\IssuesCommand($this->translator))->execute($commandArgs);
243
+ break;
244
+
222
245
  case 'add':
223
246
  (new \ArtiFrame\Cli\Commands\AddCommand($this->translator))->execute($commandArgs);
224
247
  break;
@@ -302,7 +325,13 @@ class App
302
325
  // make:class
303
326
  echo " " . $g . "make:class" . $r . " " . $y . "<path>" . $r . PHP_EOL;
304
327
  echo " " . $d . "│" . $r . " " . $t->get('HELP_MAKECLASS_DESC') . PHP_EOL;
305
- echo " " . $d . "└── " . $r . "Example: " . $lg . "make:class app/Services/UserManager.php" . $r . PHP_EOL;
328
+ echo " " . $d . "└─ " . $r . "Example: " . $lg . "make:class app/Services/UserManager.php" . $r . PHP_EOL;
329
+ echo PHP_EOL;
330
+
331
+ // table
332
+ echo " " . $g . "table:<table>" . $r . PHP_EOL;
333
+ echo " " . $d . "│" . $r . " " . $t->get('HELP_TABLE_DESC') . PHP_EOL;
334
+ echo " " . $d . "└─ " . $r . "Example: " . $lg . "table:users" . $r . PHP_EOL;
306
335
  echo PHP_EOL;
307
336
 
308
337
  // cli v / cli version — CLI self-version
@@ -366,6 +395,24 @@ class App
366
395
  echo " " . $d . "└── " . $r . $lg . "add list" . $r . $d . " " . $t->get('HELP_ADD_LIST') . $r . PHP_EOL;
367
396
  echo PHP_EOL;
368
397
 
398
+ // auth
399
+ echo " " . $g . "auth" . $r . PHP_EOL;
400
+ echo " " . $d . "│" . $r . " " . $t->get('HELP_AUTH_DESC') . PHP_EOL;
401
+ echo " " . $d . "└── " . $r . "Example: " . $lg . "auth" . $r . PHP_EOL;
402
+ echo PHP_EOL;
403
+
404
+ // suggest
405
+ echo " " . $g . "suggest" . $r . PHP_EOL;
406
+ echo " " . $d . "│" . $r . " " . $t->get('HELP_SUGGEST_DESC') . PHP_EOL;
407
+ echo " " . $d . "└── " . $r . "Example: " . $lg . "suggest" . $r . PHP_EOL;
408
+ echo PHP_EOL;
409
+
410
+ // issues
411
+ echo " " . $g . "issues" . $r . " " . $y . "[id]" . $r . PHP_EOL;
412
+ echo " " . $d . "│" . $r . " " . $t->get('HELP_ISSUES_DESC') . PHP_EOL;
413
+ echo " " . $d . "└── " . $r . "Example: " . $lg . "issues" . $r . " or " . $lg . "issues 4" . $r . PHP_EOL;
414
+ echo PHP_EOL;
415
+
369
416
  // help / exit
370
417
  echo " " . $d . "─────────────────────────────────────────────────────────────" . $r . PHP_EOL;
371
418
  echo " " . $g . "help" . $r . " " . $t->get('HELP_HELP_DESC') . PHP_EOL;
@@ -0,0 +1,219 @@
1
+ <?php
2
+ namespace ArtiFrame\Cli\Commands;
3
+
4
+ use ArtiFrame\Cli\Services\Translator;
5
+
6
+ class AuthCommand
7
+ {
8
+ private Translator $translator;
9
+
10
+ // Placeholder Client ID as requested
11
+ const CLIENT_ID = 'Ov23libWFmWDGtyaSEiT';
12
+
13
+ public function __construct(Translator $translator)
14
+ {
15
+ $this->translator = $translator;
16
+ }
17
+
18
+ public function execute(array $args): void
19
+ {
20
+ echo "\033[1;36mInitializing GitHub Authentication...\033[0m" . PHP_EOL;
21
+
22
+ // 1. Request Device Code
23
+ $deviceCodeData = $this->requestDeviceCode();
24
+ if (!$deviceCodeData) {
25
+ return;
26
+ }
27
+
28
+ $deviceCode = $deviceCodeData['device_code'];
29
+ $userCode = $deviceCodeData['user_code'];
30
+ $verificationUri = $deviceCodeData['verification_uri'];
31
+ $interval = (int)($deviceCodeData['interval'] ?? 5);
32
+
33
+ // 2. Display instructions
34
+ echo PHP_EOL;
35
+ echo "\033[1;33mAction Required:\033[0m" . PHP_EOL;
36
+ echo "1. Please open your browser to: \033[1;34m" . $verificationUri . "\033[0m" . PHP_EOL;
37
+ echo "2. Enter the following code to authorize: \033[1;32m" . $userCode . "\033[0m" . PHP_EOL;
38
+ echo PHP_EOL;
39
+ echo "Waiting for authorization..." . PHP_EOL;
40
+
41
+ // 3. Open browser automatically
42
+ $this->openBrowser($verificationUri);
43
+
44
+ // 4. Poll for Access Token
45
+ $accessToken = $this->pollForToken($deviceCode, $interval);
46
+
47
+ if ($accessToken) {
48
+ // 5. Fetch user profile
49
+ $userProfile = $this->fetchUserProfile($accessToken);
50
+ $username = $userProfile['login'] ?? 'Unknown User';
51
+
52
+ // 6. Save configuration
53
+ $this->saveConfig($accessToken, $username);
54
+
55
+ echo "\033[1;32m✔ Successfully authenticated as @{$username}!\033[0m" . PHP_EOL;
56
+ }
57
+ }
58
+
59
+ private function requestDeviceCode(): ?array
60
+ {
61
+ $ch = curl_init('https://github.com/login/device/code');
62
+ $postData = http_build_query([
63
+ 'client_id' => self::CLIENT_ID,
64
+ 'scope' => 'public_repo'
65
+ ]);
66
+
67
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
68
+ curl_setopt($ch, CURLOPT_POST, true);
69
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
70
+ curl_setopt($ch, CURLOPT_HTTPHEADER, [
71
+ 'Accept: application/json'
72
+ ]);
73
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
74
+ curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
75
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
76
+ curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
77
+ curl_setopt($ch, CURLOPT_USERAGENT, 'ArtiFrame-CLI');
78
+
79
+ $response = curl_exec($ch);
80
+ $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
81
+
82
+ if ($httpCode !== 200 || !$response) {
83
+ echo "\033[1;31m[-] Failed to request device code from GitHub.\033[0m" . PHP_EOL;
84
+ curl_close($ch);
85
+ return null;
86
+ }
87
+ curl_close($ch);
88
+
89
+ return json_decode($response, true);
90
+ }
91
+
92
+ private function pollForToken(string $deviceCode, int $interval): ?string
93
+ {
94
+ $url = 'https://github.com/login/oauth/access_token';
95
+
96
+ while (true) {
97
+ sleep($interval);
98
+
99
+ $ch = curl_init($url);
100
+ $postData = http_build_query([
101
+ 'client_id' => self::CLIENT_ID,
102
+ 'device_code' => $deviceCode,
103
+ 'grant_type' => 'urn:ietf:params:oauth:grant-type:device_code'
104
+ ]);
105
+
106
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
107
+ curl_setopt($ch, CURLOPT_POST, true);
108
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
109
+ curl_setopt($ch, CURLOPT_HTTPHEADER, [
110
+ 'Accept: application/json'
111
+ ]);
112
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
113
+ curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
114
+ curl_setopt($ch, CURLOPT_USERAGENT, 'ArtiFrame-CLI');
115
+
116
+ $response = curl_exec($ch);
117
+ curl_close($ch);
118
+
119
+ if ($response) {
120
+ $data = json_decode($response, true);
121
+
122
+ if (isset($data['access_token'])) {
123
+ return $data['access_token'];
124
+ }
125
+
126
+ if (isset($data['error'])) {
127
+ if ($data['error'] === 'authorization_pending') {
128
+ // Keep waiting
129
+ continue;
130
+ } elseif ($data['error'] === 'slow_down') {
131
+ $interval += 5; // Slow down polling
132
+ continue;
133
+ } elseif ($data['error'] === 'expired_token') {
134
+ echo "\033[1;31m[-] The device code expired. Please run the auth command again.\033[0m" . PHP_EOL;
135
+ return null;
136
+ } elseif ($data['error'] === 'access_denied') {
137
+ echo "\033[1;31m[-] Authorization was denied.\033[0m" . PHP_EOL;
138
+ return null;
139
+ } else {
140
+ echo "\033[1;31m[-] An error occurred: " . $data['error'] . "\033[0m" . PHP_EOL;
141
+ return null;
142
+ }
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ private function fetchUserProfile(string $token): ?array
149
+ {
150
+ $ch = curl_init('https://api.github.com/user');
151
+
152
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
153
+ curl_setopt($ch, CURLOPT_HTTPHEADER, [
154
+ 'Accept: application/vnd.github.v3+json',
155
+ 'Authorization: Bearer ' . $token,
156
+ 'User-Agent: ArtiFrame-CLI'
157
+ ]);
158
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
159
+ curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
160
+
161
+ $response = curl_exec($ch);
162
+ curl_close($ch);
163
+
164
+ if ($response) {
165
+ return json_decode($response, true);
166
+ }
167
+
168
+ return null;
169
+ }
170
+
171
+ private function saveConfig(string $token, string $username): void
172
+ {
173
+ $home = getenv('HOME') ?: getenv('USERPROFILE');
174
+ $configDir = $home . \DIRECTORY_SEPARATOR . '.artiframe';
175
+
176
+ if (!is_dir($configDir)) {
177
+ mkdir($configDir, 0755, true);
178
+ }
179
+
180
+ $configFile = $configDir . \DIRECTORY_SEPARATOR . 'config.json';
181
+
182
+ $config = [];
183
+ if (file_exists($configFile)) {
184
+ $existing = json_decode(file_get_contents($configFile), true);
185
+ if (is_array($existing)) {
186
+ $config = $existing;
187
+ }
188
+ }
189
+
190
+ $config['github_token'] = $token;
191
+ $config['github_user'] = $username;
192
+ $config['logged_in_at'] = date('c');
193
+
194
+ file_put_contents($configFile, json_encode($config, JSON_PRETTY_PRINT));
195
+ }
196
+
197
+ private function openBrowser(string $url): void
198
+ {
199
+ $os = php_uname('s');
200
+ $command = '';
201
+
202
+ if (stripos($os, 'win') === 0) {
203
+ $command = 'start "" "' . $url . '"';
204
+ } elseif (stripos($os, 'darwin') === 0) {
205
+ $command = 'open "' . $url . '"';
206
+ } else {
207
+ $command = 'xdg-open "' . $url . '"';
208
+ }
209
+
210
+ if ($command) {
211
+ // execute command silently in background
212
+ if (stripos($os, 'win') === 0) {
213
+ pclose(popen($command, "r"));
214
+ } else {
215
+ exec($command . ' > /dev/null 2>&1 &');
216
+ }
217
+ }
218
+ }
219
+ }
@@ -0,0 +1,174 @@
1
+ <?php
2
+ namespace ArtiFrame\Cli\Commands;
3
+
4
+ use ArtiFrame\Cli\Services\Translator;
5
+
6
+ class IssuesCommand
7
+ {
8
+ private Translator $translator;
9
+ const REPO = 'utkuthecoder/artiframe-cli';
10
+
11
+ public function __construct(Translator $translator)
12
+ {
13
+ $this->translator = $translator;
14
+ }
15
+
16
+ public function execute(array $args = []): void
17
+ {
18
+ $home = getenv('HOME') ?: getenv('USERPROFILE');
19
+ $configPath = $home . \DIRECTORY_SEPARATOR . '.artiframe' . \DIRECTORY_SEPARATOR . 'config.json';
20
+
21
+ if (!file_exists($configPath)) {
22
+ echo "\033[1;31m[-] " . $this->translator->get('SUGGEST_NOT_AUTH') . "\033[0m" . PHP_EOL;
23
+ echo "Run: \033[1;32martiframe auth\033[0m" . PHP_EOL;
24
+ return;
25
+ }
26
+
27
+ $config = json_decode(file_get_contents($configPath), true);
28
+ $token = $config['github_token'] ?? null;
29
+ $username = $config['github_user'] ?? null;
30
+
31
+ if (!$token || !$username) {
32
+ echo "\033[1;31m[-] " . $this->translator->get('SUGGEST_NOT_AUTH') . "\033[0m" . PHP_EOL;
33
+ echo "Run: \033[1;32martiframe auth\033[0m" . PHP_EOL;
34
+ return;
35
+ }
36
+
37
+ $issueId = $args[0] ?? null;
38
+
39
+ if ($issueId) {
40
+ $this->showIssueDetails($token, $username, $issueId);
41
+ } else {
42
+ $this->listIssues($token, $username);
43
+ }
44
+ }
45
+
46
+ private function listIssues(string $token, string $username): void
47
+ {
48
+ echo PHP_EOL . "\033[1;36mFetching issues for @{$username}...\033[0m" . PHP_EOL;
49
+
50
+ $url = 'https://api.github.com/repos/' . self::REPO . '/issues?creator=' . urlencode($username) . '&state=all';
51
+ $response = $this->githubApiRequest($url, $token);
52
+
53
+ if (!$response) {
54
+ echo "\033[1;31m[-] Failed to fetch issues from GitHub.\033[0m" . PHP_EOL;
55
+ return;
56
+ }
57
+
58
+ $issues = json_decode($response, true);
59
+
60
+ if (isset($issues['message'])) {
61
+ echo "\033[1;31m[-] GitHub Error: " . $issues['message'] . "\033[0m" . PHP_EOL;
62
+ return;
63
+ }
64
+
65
+ if (empty($issues)) {
66
+ echo "\033[1;33mYou haven't opened any issues yet.\033[0m" . PHP_EOL;
67
+ return;
68
+ }
69
+
70
+ echo "\033[1;37m" . str_pad("ID", 6) . str_pad("STATUS", 10) . str_pad("COMMENTS", 10) . "TITLE\033[0m" . PHP_EOL;
71
+ echo str_repeat("-", 80) . PHP_EOL;
72
+
73
+ foreach ($issues as $issue) {
74
+ if (isset($issue['pull_request'])) {
75
+ continue; // Skip PRs if they show up in issue search
76
+ }
77
+
78
+ $id = "#" . $issue['number'];
79
+ $state = $issue['state']; // open or closed
80
+ $comments = $issue['comments'];
81
+ $title = strlen($issue['title']) > 50 ? substr($issue['title'], 0, 47) . '...' : $issue['title'];
82
+
83
+ $stateColor = $state === 'open' ? "\033[1;32m" : "\033[1;31m"; // Green for open, red for closed
84
+ $displayState = ucfirst($state);
85
+
86
+ echo str_pad($id, 6) .
87
+ $stateColor . str_pad($displayState, 10) . "\033[0m" .
88
+ str_pad((string)$comments, 10) .
89
+ $title . PHP_EOL;
90
+ }
91
+
92
+ echo PHP_EOL . "\033[38;5;240mTip: To view comments for a specific issue, run `artiframe issues <ID>`\033[0m" . PHP_EOL;
93
+ }
94
+
95
+ private function showIssueDetails(string $token, string $username, string $issueId): void
96
+ {
97
+ $issueId = ltrim($issueId, '#');
98
+ echo PHP_EOL . "\033[1;36mFetching details for Issue #{$issueId}...\033[0m" . PHP_EOL;
99
+
100
+ // Fetch Issue Details
101
+ $url = 'https://api.github.com/repos/' . self::REPO . '/issues/' . $issueId;
102
+ $response = $this->githubApiRequest($url, $token);
103
+
104
+ if (!$response) {
105
+ echo "\033[1;31m[-] Failed to fetch issue details.\033[0m" . PHP_EOL;
106
+ return;
107
+ }
108
+
109
+ $issue = json_decode($response, true);
110
+
111
+ if (isset($issue['message'])) {
112
+ echo "\033[1;31m[-] GitHub Error: " . $issue['message'] . "\033[0m" . PHP_EOL;
113
+ return;
114
+ }
115
+
116
+ $stateColor = $issue['state'] === 'open' ? "\033[1;32m" : "\033[1;31m";
117
+
118
+ echo PHP_EOL;
119
+ echo "\033[1;37m[" . $issue['state'] . "] " . $issue['title'] . "\033[0m" . PHP_EOL;
120
+ echo "\033[38;5;240mOpened by @" . $issue['user']['login'] . " at " . date('Y-m-d H:i', strtotime($issue['created_at'])) . "\033[0m" . PHP_EOL;
121
+ echo str_repeat("=", 80) . PHP_EOL;
122
+ echo $this->wordWrapWithIndentation($issue['body']) . PHP_EOL;
123
+ echo str_repeat("=", 80) . PHP_EOL;
124
+
125
+ if ($issue['comments'] > 0) {
126
+ echo PHP_EOL . "\033[1;36mComments ({$issue['comments']}):\033[0m" . PHP_EOL;
127
+
128
+ $commentsUrl = $issue['comments_url'];
129
+ $commentsResponse = $this->githubApiRequest($commentsUrl, $token);
130
+ $comments = json_decode($commentsResponse, true);
131
+
132
+ if (is_array($comments)) {
133
+ foreach ($comments as $comment) {
134
+ echo PHP_EOL;
135
+ echo "\033[1;34m@" . $comment['user']['login'] . "\033[0m \033[38;5;240m(" . date('Y-m-d H:i', strtotime($comment['created_at'])) . "):\033[0m" . PHP_EOL;
136
+ echo str_repeat("-", 80) . PHP_EOL;
137
+ echo $this->wordWrapWithIndentation($comment['body']) . PHP_EOL;
138
+ echo str_repeat("-", 80) . PHP_EOL;
139
+ }
140
+ }
141
+ } else {
142
+ echo PHP_EOL . "\033[38;5;240mNo comments on this issue yet.\033[0m" . PHP_EOL;
143
+ }
144
+ echo PHP_EOL;
145
+ }
146
+
147
+ private function githubApiRequest(string $url, string $token): ?string
148
+ {
149
+ $ch = curl_init($url);
150
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
151
+ curl_setopt($ch, CURLOPT_HTTPHEADER, [
152
+ 'Accept: application/vnd.github.v3+json',
153
+ 'Authorization: Bearer ' . $token,
154
+ 'User-Agent: ArtiFrame-CLI'
155
+ ]);
156
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
157
+ curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
158
+
159
+ $response = curl_exec($ch);
160
+ curl_close($ch);
161
+
162
+ return $response ?: null;
163
+ }
164
+
165
+ private function wordWrapWithIndentation(string $text, int $width = 80): string
166
+ {
167
+ $lines = explode("\n", $text);
168
+ $wrapped = [];
169
+ foreach ($lines as $line) {
170
+ $wrapped[] = wordwrap(trim($line), $width, "\n");
171
+ }
172
+ return implode("\n", $wrapped);
173
+ }
174
+ }
@@ -0,0 +1,143 @@
1
+ <?php
2
+ namespace ArtiFrame\Cli\Commands;
3
+
4
+ use ArtiFrame\Cli\Services\Translator;
5
+
6
+ class SuggestCommand
7
+ {
8
+ private Translator $translator;
9
+ const API_ENDPOINT = 'https://api.artilingo.com/artiframe/v1/suggest.php';
10
+
11
+ public function __construct(Translator $translator)
12
+ {
13
+ $this->translator = $translator;
14
+ }
15
+
16
+ public function execute(array $args): void
17
+ {
18
+ $home = getenv('HOME') ?: getenv('USERPROFILE');
19
+ $configFile = $home . \DIRECTORY_SEPARATOR . '.artiframe' . \DIRECTORY_SEPARATOR . 'config.json';
20
+
21
+ if (!file_exists($configFile)) {
22
+ echo "\033[1;31m[-] Not authenticated. Please run 'artiframe auth' first.\033[0m" . PHP_EOL;
23
+ return;
24
+ }
25
+
26
+ $config = json_decode(file_get_contents($configFile), true);
27
+ if (!$config || !isset($config['github_token'])) {
28
+ echo "\033[1;31m[-] Not authenticated. Please run 'artiframe auth' first.\033[0m" . PHP_EOL;
29
+ return;
30
+ }
31
+
32
+ $token = $config['github_token'];
33
+ $username = $config['github_user'] ?? 'User';
34
+
35
+ echo "\033[1;36mHello @{$username}, welcome to ArtiFrame Issue Submitter!\033[0m" . PHP_EOL;
36
+ echo PHP_EOL;
37
+
38
+ // 1. Ask for Category
39
+ $categories = [
40
+ '1' => 'Feature Request',
41
+ '2' => 'Bug Report',
42
+ '3' => 'Helper Suggestion'
43
+ ];
44
+
45
+ echo "Please select a Category:" . PHP_EOL;
46
+ echo " [1] Feature Request" . PHP_EOL;
47
+ echo " [2] Bug Report" . PHP_EOL;
48
+ echo " [3] Helper Suggestion" . PHP_EOL;
49
+
50
+ $categoryId = null;
51
+ while (true) {
52
+ echo "Select [1-3]: ";
53
+ $input = trim(fgets(STDIN));
54
+ if (isset($categories[$input])) {
55
+ $categoryId = $input;
56
+ break;
57
+ }
58
+ echo "\033[1;31mInvalid selection.\033[0m" . PHP_EOL;
59
+ }
60
+
61
+ $category = $categories[$categoryId];
62
+
63
+ // 2. Ask for Title
64
+ echo PHP_EOL;
65
+ $title = '';
66
+ while (true) {
67
+ echo "Title (Short summary, max 120 chars): ";
68
+ $title = trim(fgets(STDIN));
69
+ if (strlen($title) === 0) {
70
+ echo "\033[1;31mTitle cannot be empty.\033[0m" . PHP_EOL;
71
+ } elseif (strlen($title) > 120) {
72
+ echo "\033[1;31mTitle is too long. Max 120 chars.\033[0m" . PHP_EOL;
73
+ } else {
74
+ break;
75
+ }
76
+ }
77
+
78
+ // 3. Ask for Description
79
+ echo PHP_EOL;
80
+ echo "Description (Detailed explanation or code sample):" . PHP_EOL;
81
+ echo "\033[1;33m(Type 'END' on a new line and press Enter to finish)\033[0m" . PHP_EOL;
82
+
83
+ $bodyLines = [];
84
+ while (true) {
85
+ $line = fgets(STDIN);
86
+ if ($line === false || trim(strtoupper($line)) === 'END') {
87
+ break;
88
+ }
89
+ $bodyLines[] = rtrim($line, "\r\n");
90
+ }
91
+ $body = implode(PHP_EOL, $bodyLines);
92
+
93
+ if (empty(trim($body))) {
94
+ echo "\033[1;31m[-] Description cannot be empty. Aborting.\033[0m" . PHP_EOL;
95
+ return;
96
+ }
97
+
98
+ // 4. Send Payload
99
+ echo PHP_EOL . "\033[1;36mSubmitting to ArtiFrame...\033[0m" . PHP_EOL;
100
+
101
+ $payload = json_encode([
102
+ 'token' => $token,
103
+ 'category' => $category,
104
+ 'title' => $title,
105
+ 'body' => $body
106
+ ]);
107
+
108
+ $ch = curl_init(self::API_ENDPOINT);
109
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
110
+ curl_setopt($ch, CURLOPT_POST, true);
111
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
112
+ curl_setopt($ch, CURLOPT_HTTPHEADER, [
113
+ 'Content-Type: application/json',
114
+ 'Accept: application/json'
115
+ ]);
116
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
117
+ curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
118
+ curl_setopt($ch, CURLOPT_USERAGENT, 'ArtiFrame-CLI');
119
+
120
+ $response = curl_exec($ch);
121
+ $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
122
+ curl_close($ch);
123
+
124
+ if ($response) {
125
+ $data = json_decode($response, true);
126
+ if ($httpCode === 200 && isset($data['status']) && $data['status'] === 'success') {
127
+ echo "\033[1;32m🎉 Issue successfully created!\033[0m" . PHP_EOL;
128
+ echo "URL: " . ($data['issue_url'] ?? 'N/A') . PHP_EOL;
129
+ } else {
130
+ if ($data && isset($data['message'])) {
131
+ echo "\033[1;31m[-] Server Error: " . $data['message'] . "\033[0m" . PHP_EOL;
132
+ } else {
133
+ echo "\033[1;31m[-] Server Error: Unknown response format.\033[0m" . PHP_EOL;
134
+ echo "HTTP Code: " . $httpCode . PHP_EOL;
135
+ echo "Raw Response: " . substr($response, 0, 300) . PHP_EOL;
136
+ }
137
+ }
138
+ } else {
139
+ echo "\033[1;31m[-] Failed to communicate with the server.\033[0m" . PHP_EOL;
140
+ echo "cURL Error: " . curl_error($ch) . PHP_EOL;
141
+ }
142
+ }
143
+ }