Initial commit
This commit is contained in:
300
src/analyzers/detector.js
Normal file
300
src/analyzers/detector.js
Normal file
@@ -0,0 +1,300 @@
|
||||
/**
|
||||
* Platform Detection
|
||||
* Analyzes a directory to determine if it's a Claude skill, Gemini extension, or universal
|
||||
*/
|
||||
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
export const PLATFORM_TYPES = {
|
||||
CLAUDE: 'claude',
|
||||
GEMINI: 'gemini',
|
||||
UNIVERSAL: 'universal',
|
||||
UNKNOWN: 'unknown'
|
||||
};
|
||||
|
||||
export class PlatformDetector {
|
||||
/**
|
||||
* Detect the platform type of a skill/extension directory
|
||||
* @param {string} dirPath - Path to the directory to analyze
|
||||
* @returns {Promise<{platform: string, files: object, confidence: string}>}
|
||||
*/
|
||||
async detect(dirPath) {
|
||||
const detection = {
|
||||
platform: PLATFORM_TYPES.UNKNOWN,
|
||||
files: {
|
||||
claude: [],
|
||||
gemini: [],
|
||||
shared: []
|
||||
},
|
||||
confidence: 'low',
|
||||
metadata: {}
|
||||
};
|
||||
|
||||
try {
|
||||
const exists = await this._checkDirectoryExists(dirPath);
|
||||
if (!exists) {
|
||||
throw new Error(`Directory not found: ${dirPath}`);
|
||||
}
|
||||
|
||||
// Check for Claude-specific files
|
||||
const claudeFiles = await this._detectClaudeFiles(dirPath);
|
||||
detection.files.claude = claudeFiles;
|
||||
|
||||
// Check for Gemini-specific files
|
||||
const geminiFiles = await this._detectGeminiFiles(dirPath);
|
||||
detection.files.gemini = geminiFiles;
|
||||
|
||||
// Check for shared files
|
||||
const sharedFiles = await this._detectSharedFiles(dirPath);
|
||||
detection.files.shared = sharedFiles;
|
||||
|
||||
// Determine platform type
|
||||
const hasClaude = claudeFiles.length > 0;
|
||||
const hasGemini = geminiFiles.length > 0;
|
||||
|
||||
if (hasClaude && hasGemini) {
|
||||
detection.platform = PLATFORM_TYPES.UNIVERSAL;
|
||||
detection.confidence = 'high';
|
||||
} else if (hasClaude) {
|
||||
detection.platform = PLATFORM_TYPES.CLAUDE;
|
||||
detection.confidence = 'high';
|
||||
} else if (hasGemini) {
|
||||
detection.platform = PLATFORM_TYPES.GEMINI;
|
||||
detection.confidence = 'high';
|
||||
} else {
|
||||
detection.platform = PLATFORM_TYPES.UNKNOWN;
|
||||
detection.confidence = 'low';
|
||||
}
|
||||
|
||||
// Extract metadata
|
||||
detection.metadata = await this._extractMetadata(dirPath, detection.platform);
|
||||
|
||||
return detection;
|
||||
} catch (error) {
|
||||
throw new Error(`Detection failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if directory exists
|
||||
*/
|
||||
async _checkDirectoryExists(dirPath) {
|
||||
try {
|
||||
const stats = await fs.stat(dirPath);
|
||||
return stats.isDirectory();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect Claude-specific files
|
||||
*/
|
||||
async _detectClaudeFiles(dirPath) {
|
||||
const claudeFiles = [];
|
||||
|
||||
// Check for SKILL.md
|
||||
const skillPath = path.join(dirPath, 'SKILL.md');
|
||||
if (await this._fileExists(skillPath)) {
|
||||
const hasValidFrontmatter = await this._hasYAMLFrontmatter(skillPath);
|
||||
if (hasValidFrontmatter) {
|
||||
claudeFiles.push({ file: 'SKILL.md', type: 'entry', valid: true });
|
||||
} else {
|
||||
claudeFiles.push({ file: 'SKILL.md', type: 'entry', valid: false, issue: 'Missing or invalid YAML frontmatter' });
|
||||
}
|
||||
}
|
||||
|
||||
// Check for .claude-plugin/marketplace.json
|
||||
const marketplacePath = path.join(dirPath, '.claude-plugin', 'marketplace.json');
|
||||
if (await this._fileExists(marketplacePath)) {
|
||||
const isValidJSON = await this._isValidJSON(marketplacePath);
|
||||
if (isValidJSON) {
|
||||
claudeFiles.push({ file: '.claude-plugin/marketplace.json', type: 'manifest', valid: true });
|
||||
} else {
|
||||
claudeFiles.push({ file: '.claude-plugin/marketplace.json', type: 'manifest', valid: false, issue: 'Invalid JSON' });
|
||||
}
|
||||
}
|
||||
|
||||
return claudeFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect Gemini-specific files
|
||||
*/
|
||||
async _detectGeminiFiles(dirPath) {
|
||||
const geminiFiles = [];
|
||||
|
||||
// Check for gemini-extension.json
|
||||
const manifestPath = path.join(dirPath, 'gemini-extension.json');
|
||||
if (await this._fileExists(manifestPath)) {
|
||||
const isValidJSON = await this._isValidJSON(manifestPath);
|
||||
if (isValidJSON) {
|
||||
geminiFiles.push({ file: 'gemini-extension.json', type: 'manifest', valid: true });
|
||||
} else {
|
||||
geminiFiles.push({ file: 'gemini-extension.json', type: 'manifest', valid: false, issue: 'Invalid JSON' });
|
||||
}
|
||||
}
|
||||
|
||||
// Check for GEMINI.md
|
||||
const geminiMdPath = path.join(dirPath, 'GEMINI.md');
|
||||
if (await this._fileExists(geminiMdPath)) {
|
||||
geminiFiles.push({ file: 'GEMINI.md', type: 'context', valid: true });
|
||||
}
|
||||
|
||||
return geminiFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect shared files (common to both platforms)
|
||||
*/
|
||||
async _detectSharedFiles(dirPath) {
|
||||
const sharedFiles = [];
|
||||
|
||||
// Check for package.json
|
||||
const packagePath = path.join(dirPath, 'package.json');
|
||||
if (await this._fileExists(packagePath)) {
|
||||
sharedFiles.push({ file: 'package.json', type: 'dependency' });
|
||||
}
|
||||
|
||||
// Check for shared directory
|
||||
const sharedDirPath = path.join(dirPath, 'shared');
|
||||
if (await this._checkDirectoryExists(sharedDirPath)) {
|
||||
sharedFiles.push({ file: 'shared/', type: 'directory' });
|
||||
}
|
||||
|
||||
// Check for MCP server directory
|
||||
const mcpServerPath = path.join(dirPath, 'mcp-server');
|
||||
if (await this._checkDirectoryExists(mcpServerPath)) {
|
||||
sharedFiles.push({ file: 'mcp-server/', type: 'directory' });
|
||||
}
|
||||
|
||||
return sharedFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract metadata from files
|
||||
*/
|
||||
async _extractMetadata(dirPath, platform) {
|
||||
const metadata = {};
|
||||
|
||||
if (platform === PLATFORM_TYPES.CLAUDE || platform === PLATFORM_TYPES.UNIVERSAL) {
|
||||
// Try to extract from SKILL.md
|
||||
const skillPath = path.join(dirPath, 'SKILL.md');
|
||||
if (await this._fileExists(skillPath)) {
|
||||
const frontmatter = await this._extractYAMLFrontmatter(skillPath);
|
||||
if (frontmatter) {
|
||||
metadata.claude = frontmatter;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to extract from marketplace.json
|
||||
const marketplacePath = path.join(dirPath, '.claude-plugin', 'marketplace.json');
|
||||
if (await this._fileExists(marketplacePath)) {
|
||||
const content = await fs.readFile(marketplacePath, 'utf8');
|
||||
try {
|
||||
const json = JSON.parse(content);
|
||||
metadata.claudeMarketplace = json;
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
if (platform === PLATFORM_TYPES.GEMINI || platform === PLATFORM_TYPES.UNIVERSAL) {
|
||||
// Try to extract from gemini-extension.json
|
||||
const manifestPath = path.join(dirPath, 'gemini-extension.json');
|
||||
if (await this._fileExists(manifestPath)) {
|
||||
const content = await fs.readFile(manifestPath, 'utf8');
|
||||
try {
|
||||
const json = JSON.parse(content);
|
||||
metadata.gemini = json;
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if file exists
|
||||
*/
|
||||
async _fileExists(filePath) {
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if file is valid JSON
|
||||
*/
|
||||
async _isValidJSON(filePath) {
|
||||
try {
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
JSON.parse(content);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if file has YAML frontmatter
|
||||
*/
|
||||
async _hasYAMLFrontmatter(filePath) {
|
||||
try {
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
return /^---\n[\s\S]+?\n---/.test(content);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract YAML frontmatter from file
|
||||
*/
|
||||
async _extractYAMLFrontmatter(filePath) {
|
||||
try {
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
const match = content.match(/^---\n([\s\S]+?)\n---/);
|
||||
if (match) {
|
||||
// Simple YAML parser for basic key-value pairs
|
||||
const yaml = match[1];
|
||||
const parsed = {};
|
||||
|
||||
const lines = yaml.split('\n');
|
||||
let currentKey = null;
|
||||
let currentValue = null;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.trim().startsWith('-')) {
|
||||
// Array item
|
||||
if (currentKey && Array.isArray(parsed[currentKey])) {
|
||||
parsed[currentKey].push(line.trim().substring(1).trim());
|
||||
}
|
||||
} else if (line.includes(':')) {
|
||||
// Key-value pair
|
||||
const [key, ...valueParts] = line.split(':');
|
||||
const value = valueParts.join(':').trim();
|
||||
currentKey = key.trim();
|
||||
|
||||
if (value === '') {
|
||||
// Array or multi-line value
|
||||
parsed[currentKey] = [];
|
||||
} else {
|
||||
parsed[currentKey] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default PlatformDetector;
|
||||
Reference in New Issue
Block a user