Files
gh-jduncan-rva-skill-porter/src/analyzers/detector.js
2025-11-29 18:50:16 +08:00

301 lines
8.4 KiB
JavaScript

/**
* 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;