info('Scanning admin-ajax endpoints...'); $endpoints = $this->scanEndpoints(); $this->info("Found " . count($endpoints) . " endpoints"); if ($this->option('append')) { $this->appendToOpenApiSpec($endpoints); } else { $this->generateStandaloneSpec($endpoints); } $this->info('✅ Endpoints documentation generated successfully!'); return Command::SUCCESS; } /** * Scan admin-ajax directory for blade files */ protected function scanEndpoints(): array { $endpoints = []; $path = resource_path('views/admin-ajax'); if (!File::isDirectory($path)) { $this->error('admin-ajax directory not found'); return $endpoints; } // Scan files recursively $this->scanDirectory($path, '', $endpoints); // Sort alphabetically usort($endpoints, fn($a, $b) => strcmp($a['name'], $b['name'])); return $endpoints; } /** * Recursively scan directory for blade files * SECURITY: Only includes endpoints marked with @api-readonly annotation */ protected function scanDirectory(string $basePath, string $prefix, array &$endpoints): void { $items = File::files($basePath); foreach ($items as $file) { $filename = $file->getFilename(); // Only process .blade.php files if (!Str::endsWith($filename, '.blade.php')) { continue; } // Extract endpoint name $name = str_replace('.blade.php', '', $filename); $fullName = $prefix ? "{$prefix}/{$name}" : $name; // Analyze file for response type hints $content = File::get($file->getPathname()); // SECURITY: Only include endpoints marked as @api-readonly if (!Str::contains($content, '@api-readonly')) { continue; } $responseType = $this->detectResponseType($content); $description = $this->extractDescription($content, $name); $category = $this->categorizeEndpoint($fullName); $endpoints[] = [ 'name' => $fullName, 'path' => str_replace(base_path(), '', $file->getPathname()), 'response_type' => $responseType, 'description' => $description, 'category' => $category, ]; } // Scan subdirectories $directories = File::directories($basePath); foreach ($directories as $dir) { $dirName = basename($dir); $newPrefix = $prefix ? "{$prefix}/{$dirName}" : $dirName; $this->scanDirectory($dir, $newPrefix, $endpoints); } } /** * Detect response type from blade content */ protected function detectResponseType(string $content): string { // Check for JSON indicators if ( Str::contains($content, 'json_encode') || Str::contains($content, 'json_encode_tr') || Str::contains($content, 'response()->json') || Str::contains($content, "'status' =>") ) { return 'json'; } // Check for HTML indicators if ( Str::contains($content, '/s', $content, $matches)) { return trim($matches[1]); } // Generate description from name return 'Execute ' . Str::title(str_replace(['-', '_'], ' ', $name)) . ' endpoint'; } /** * Categorize endpoint based on name */ protected function categorizeEndpoint(string $name): string { $prefixes = [ 'ndt-' => 'NDT Operations', 'welder-' => 'Welder Management', 'weld' => 'Welding', 'register-' => 'Register Creator', 'test-package' => 'Test Packages', 'tp-' => 'Test Packages', 'document-' => 'Document Management', 'report-' => 'Reports', 'summary' => 'Summaries', 'excel-' => 'Excel Operations', 'pdf' => 'PDF Operations', 'user' => 'User Management', 'permission' => 'Permissions', 'content-' => 'Content Management', 'spool-' => 'Spool Operations', 'material-' => 'Materials', 'paint-' => 'Paint Operations', 'repair-' => 'Repair Operations', 'rfi-' => 'RFI Operations', 'punch-' => 'Punch Lists', 'mto' => 'MTO Operations', 'ai-' => 'AI Features', 'cron-' => 'Cron Jobs', 'si-' => 'System Info', ]; $baseName = Str::contains($name, '/') ? Str::afterLast($name, '/') : $name; foreach ($prefixes as $prefix => $category) { if (Str::startsWith($baseName, $prefix)) { return $category; } } return 'General'; } /** * Append endpoints to existing OpenAPI spec */ protected function appendToOpenApiSpec(array $endpoints): void { $specPath = storage_path('app/scribe/openapi.yaml'); if (!File::exists($specPath)) { $this->warn('OpenAPI spec not found at ' . $specPath); $this->info('Run "php artisan scribe:generate" first, then run this command with --append'); return; } // Load existing spec $spec = yaml_parse_file($specPath); if (!$spec) { $this->error('Failed to parse OpenAPI spec'); return; } // Add endpoint paths foreach ($endpoints as $endpoint) { $pathKey = '/api/endpoints/' . $endpoint['name']; $spec['paths'][$pathKey] = $this->generatePathSpec($endpoint); } // Add Endpoints tag if not exists $hasEndpointsTag = false; foreach ($spec['tags'] ?? [] as $tag) { if ($tag['name'] === 'Endpoints') { $hasEndpointsTag = true; break; } } if (!$hasEndpointsTag) { $spec['tags'][] = [ 'name' => 'Endpoints', 'description' => 'Admin-ajax endpoints served via API' ]; } // Save updated spec $yaml = yaml_emit($spec, YAML_UTF8_ENCODING); File::put($specPath, $yaml); $this->info('Updated OpenAPI spec at ' . $specPath); } /** * Generate standalone endpoints spec file */ protected function generateStandaloneSpec(array $endpoints): void { $outputPath = $this->option('output') ?: storage_path('app/scribe/endpoints.json'); // Group endpoints by category $grouped = []; foreach ($endpoints as $endpoint) { $category = $endpoint['category']; if (!isset($grouped[$category])) { $grouped[$category] = []; } $grouped[$category][] = $endpoint; } $output = [ 'generated_at' => now()->toIso8601String(), 'total_endpoints' => count($endpoints), 'categories' => array_keys($grouped), 'endpoints' => $endpoints, 'grouped' => $grouped, ]; File::ensureDirectoryExists(dirname($outputPath)); File::put($outputPath, json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); $this->info('Generated endpoints spec at ' . $outputPath); // Generate Scribe response file for dynamic documentation $this->generateScribeResponseFile($endpoints); // Also generate markdown documentation $this->generateMarkdownDocs($grouped); } /** * Generate response file for Scribe documentation */ protected function generateScribeResponseFile(array $endpoints): void { $responsePath = storage_path('app/scribe/endpoints-response.json'); // Format endpoints with url and methods for API response $formattedEndpoints = array_map(function ($endpoint) { return [ 'name' => $endpoint['name'], 'url' => '/api/endpoints/' . $endpoint['name'], 'methods' => ['GET', 'POST'], 'response_type' => $endpoint['response_type'], 'description' => $endpoint['description'], 'category' => $endpoint['category'], ]; }, $endpoints); $response = [ 'status' => 'success', 'data' => [ 'endpoints' => $formattedEndpoints, 'total' => count($formattedEndpoints) ] ]; File::ensureDirectoryExists(dirname($responsePath)); File::put($responsePath, json_encode($response, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); $this->info('Generated Scribe response file at ' . $responsePath); } /** * Generate OpenAPI path specification for an endpoint */ protected function generatePathSpec(array $endpoint): array { $responseSchema = $endpoint['response_type'] === 'json' ? ['type' => 'object', 'properties' => ['status' => ['type' => 'string'], 'data' => ['type' => 'object']]] : ['type' => 'object', 'properties' => ['status' => ['type' => 'string'], 'html' => ['type' => 'string']]]; return [ 'get' => [ 'tags' => ['Endpoints'], 'summary' => $endpoint['description'], 'description' => "Endpoint: {$endpoint['name']}\nResponse Type: {$endpoint['response_type']}\nCategory: {$endpoint['category']}", 'operationId' => 'endpoint_' . Str::slug($endpoint['name'], '_'), 'security' => [['bearerAuth' => []]], 'responses' => [ '200' => [ 'description' => 'Successful response', 'content' => [ 'application/json' => [ 'schema' => $responseSchema ] ] ], '401' => [ 'description' => 'Unauthorized' ], '404' => [ 'description' => 'Endpoint not found' ] ] ], 'post' => [ 'tags' => ['Endpoints'], 'summary' => $endpoint['description'], 'description' => "Endpoint: {$endpoint['name']}\nResponse Type: {$endpoint['response_type']}\nCategory: {$endpoint['category']}", 'operationId' => 'endpoint_' . Str::slug($endpoint['name'], '_') . '_post', 'security' => [['bearerAuth' => []]], 'requestBody' => [ 'content' => [ 'application/json' => [ 'schema' => [ 'type' => 'object', 'additionalProperties' => true ] ] ] ], 'responses' => [ '200' => [ 'description' => 'Successful response', 'content' => [ 'application/json' => [ 'schema' => $responseSchema ] ] ], '401' => [ 'description' => 'Unauthorized' ], '404' => [ 'description' => 'Endpoint not found' ] ] ] ]; } /** * Generate markdown documentation */ protected function generateMarkdownDocs(array $grouped): void { $markdown = "# Available Endpoints\n\n"; $markdown .= "This document lists all available admin-ajax endpoints accessible via the API.\n\n"; $markdown .= "**Base URL:** `/api/endpoints/{endpoint}`\n\n"; $markdown .= "**Authentication:** Bearer Token required\n\n"; $markdown .= "---\n\n"; foreach ($grouped as $category => $endpoints) { $markdown .= "## {$category}\n\n"; $markdown .= "| Endpoint | Description | Response Type |\n"; $markdown .= "|----------|-------------|---------------|\n"; foreach ($endpoints as $endpoint) { $markdown .= "| `{$endpoint['name']}` | {$endpoint['description']} | {$endpoint['response_type']} |\n"; } $markdown .= "\n"; } $markdown .= "---\n\n"; $markdown .= "## Usage Examples\n\n"; $markdown .= "### cURL\n\n"; $markdown .= "```bash\n"; $markdown .= "curl -X GET \\\n"; $markdown .= " '{base_url}/api/endpoints/users' \\\n"; $markdown .= " -H 'Authorization: Bearer {your_token}' \\\n"; $markdown .= " -H 'Accept: application/json'\n"; $markdown .= "```\n\n"; $markdown .= "### JavaScript\n\n"; $markdown .= "```javascript\n"; $markdown .= "const response = await fetch('/api/endpoints/users', {\n"; $markdown .= " headers: {\n"; $markdown .= " 'Authorization': 'Bearer ' + token,\n"; $markdown .= " 'Accept': 'application/json'\n"; $markdown .= " }\n"; $markdown .= "});\n"; $markdown .= "const data = await response.json();\n"; $markdown .= "```\n"; $docsPath = resource_path('views/guide/api-endpoints.md'); File::ensureDirectoryExists(dirname($docsPath)); File::put($docsPath, $markdown); $this->info('Generated markdown docs at ' . $docsPath); } }