UNPKG

cliseo

Version:

Instant AI-Powered SEO Optimization CLI for Developers

140 lines (135 loc) • 6.56 kB
import { glob } from 'glob'; import { readFile } from 'fs/promises'; import { join } from 'path'; import { loadConfig } from '../../config'; import OpenAI from 'openai'; const FRAMEWORK_KEY_FILES = { next: ['next.config.js', 'pages/_app.tsx', 'pages/_document.tsx', 'app/layout.tsx'], react: ['src/App.tsx', 'src/main.tsx', 'src/index.tsx', 'index.html'], angular: ['angular.json', 'src/app/app.component.ts', 'src/index.html'], vue: ['vite.config.ts', 'src/App.vue', 'src/main.ts', 'index.html'], unknown: [], }; export class ProjectAnalyzer { constructor() { this.config = loadConfig(); this.openai = new OpenAI({ apiKey: this.config.openaiApiKey }); } async analyzeProject(projectRoot) { console.log('šŸ”¬ Step 1: Detecting framework and language...'); const packageJsonFile = await this.readFile(join(projectRoot, 'package.json')); const packageJson = packageJsonFile ? JSON.parse(packageJsonFile.content) : {}; const framework = this.detectFramework(packageJson); const language = await this.detectLanguage(projectRoot); console.log(`āœ… Framework detected: ${framework}`); console.log(`āœ… Language detected: ${language}`); console.log('\nšŸ”¬ Step 2: Gathering high-priority files...'); const highPriorityFiles = await this.getHighPriorityFiles(projectRoot, framework); const filesForAnalysis = packageJsonFile ? [packageJsonFile, ...highPriorityFiles] : highPriorityFiles; console.log(`āœ… Found ${filesForAnalysis.length} high-priority files for analysis.`); console.log('\nšŸ”¬ Step 3: Performing AI-powered analysis...'); const contentForAnalysis = this.prepareContentForAnalysis(filesForAnalysis); const aiAnalysis = await this.getAIAnalysis(contentForAnalysis, framework, packageJson); console.log('āœ… AI analysis complete.'); return { projectName: packageJson.name || 'Unknown Project', description: aiAnalysis.description, keywords: aiAnalysis.keywords, author: aiAnalysis.author, framework, language, }; } async readFile(filePath) { try { const content = await readFile(filePath, 'utf-8'); return { path: filePath, content }; } catch (error) { return null; // File not found or couldn't be read } } detectFramework(packageJson) { const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies }; if (dependencies.next) return 'next'; if (dependencies.react) return 'react'; if (dependencies['@angular/core']) return 'angular'; if (dependencies.vue) return 'vue'; return 'unknown'; } async detectLanguage(projectRoot) { const tsFiles = await glob('**/*.{ts,tsx}', { cwd: projectRoot, ignore: ['node_modules/**'], absolute: true }); if (tsFiles.length > 0) return 'typescript'; const jsFiles = await glob('**/*.{js,jsx}', { cwd: projectRoot, ignore: ['node_modules/**'], absolute: true }); if (jsFiles.length > 0) return 'javascript'; return 'unknown'; } async getHighPriorityFiles(projectRoot, framework) { const defaultFiles = ['README.md', 'readme.md']; const frameworkFiles = FRAMEWORK_KEY_FILES[framework]; const filesToSearch = [...new Set([...defaultFiles, ...frameworkFiles])]; const foundFiles = []; for (const file of filesToSearch) { const foundFile = await this.readFile(join(projectRoot, file)); if (foundFile) { foundFiles.push(foundFile); } } return foundFiles; } prepareContentForAnalysis(files) { return files .map(f => `--- File: ${f.path} ---\n${f.content.slice(0, 4000)}`) // Limit content per file .join('\n\n'); } async getAIAnalysis(content, framework, packageJson) { const initialData = { name: packageJson.name || 'Not specified', description: packageJson.description || 'Not specified', author: packageJson.author || 'Not specified', keywords: packageJson.keywords || [], }; const prompt = ` You are an expert SEO analyst. I'm analyzing a '${framework}' project. From its 'package.json', I have this initial information: - Project Name: ${initialData.name} - Author: ${initialData.author} - Description: ${initialData.description} - Keywords: ${JSON.stringify(initialData.keywords)} Here is the content from other key project files (like README, App.tsx, etc.): ${content} Based on ALL of this context, please perform the following tasks: 1. **Refine the Description:** Write a compelling, SEO-friendly project description (2-3 sentences). If the existing description is good, improve it; otherwise, write a new one from scratch based on the file contents. 2. **Expand Keywords:** Generate a list of 10-15 relevant SEO keywords. Combine the existing keywords with new ones you identify from the project's content. 3. **Confirm Author:** Identify the author. Prioritize the author from 'package.json' if available, otherwise look for clues in the README or other files. Return ONLY a valid JSON object with the keys: "description", "keywords", and "author". Do not include any other text or explanations in your response. `; const response = await this.openai.chat.completions.create({ model: this.config.model, messages: [ { role: 'system', content: 'You are a helpful SEO analysis assistant that only responds with valid, unformatted JSON.' }, { role: 'user', content: prompt } ], temperature: this.config.temperature, max_tokens: this.config.maxTokens, }); try { const analysis = JSON.parse(response.choices[0].message.content || '{}'); return { description: analysis.description || initialData.description, keywords: analysis.keywords || initialData.keywords, author: analysis.author || initialData.author, }; } catch (e) { console.error("Failed to parse AI response as JSON:", response.choices[0].message.content); throw new Error("The AI returned a response that was not valid JSON."); } } }