cliseo
Version:
Instant AI-Powered SEO Optimization CLI for Developers
140 lines (135 loc) ⢠6.56 kB
JavaScript
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.");
}
}
}