Skip to main content
Glama
detector.ts6.83 kB
/** * Spec Kit project detector * * Detects and validates Spec Kit project structures */ import * as fs from 'fs/promises'; import * as path from 'path'; import { exec } from 'child_process'; import { promisify } from 'util'; const execAsync = promisify(exec); export type ProjectType = 'speckit' | 'dincoder' | 'hybrid' | 'none'; export interface SpecKitDetection { type: ProjectType; hasSpecKit: boolean; hasDincoder: boolean; specKitDirs: string[]; dincoderDirs: string[]; pythonAvailable: boolean; uvAvailable: boolean; } /** * Check if Python and uv are available for Spec Kit CLI */ export async function checkSpecKitPrerequisites(): Promise<{ python: boolean; uv: boolean }> { const results = { python: false, uv: false }; try { await execAsync('python3 --version'); results.python = true; } catch { try { await execAsync('python --version'); results.python = true; } catch { // Python not available } } try { await execAsync('uv --version'); results.uv = true; } catch { // uv not available } return results; } /** * Check if a directory exists */ async function directoryExists(dirPath: string): Promise<boolean> { try { const stats = await fs.stat(dirPath); return stats.isDirectory(); } catch { return false; } } /** * Detect Spec Kit project structure */ export async function detectSpecKitStructure(workspacePath: string): Promise<string[]> { const specKitDirs = ['memory', 'scripts', 'templates', 'specs']; const foundDirs: string[] = []; for (const dir of specKitDirs) { const dirPath = path.join(workspacePath, dir); if (await directoryExists(dirPath)) { foundDirs.push(dir); } } return foundDirs; } /** * Detect DinCoder project structure */ export async function detectDincoderStructure(workspacePath: string): Promise<string[]> { const dincoderDir = path.join(workspacePath, '.dincoder'); if (await directoryExists(dincoderDir)) { try { const contents = await fs.readdir(dincoderDir); return contents.filter(item => item !== '.DS_Store'); } catch { return []; } } return []; } /** * Detect project type and available tools */ export async function detectProjectType(workspacePath: string): Promise<SpecKitDetection> { const [specKitDirs, dincoderDirs, prerequisites] = await Promise.all([ detectSpecKitStructure(workspacePath), detectDincoderStructure(workspacePath), checkSpecKitPrerequisites() ]); const hasSpecKit = specKitDirs.length >= 2; // At least 2 Spec Kit dirs const hasDincoder = dincoderDirs.length > 0; let type: ProjectType = 'none'; if (hasSpecKit && hasDincoder) { type = 'hybrid'; } else if (hasSpecKit) { type = 'speckit'; } else if (hasDincoder) { type = 'dincoder'; } return { type, hasSpecKit, hasDincoder, specKitDirs, dincoderDirs, pythonAvailable: prerequisites.python, uvAvailable: prerequisites.uv }; } /** * Find the appropriate specs directory for the project */ export async function findSpecsDirectory(workspacePath: string): Promise<string> { const detection = await detectProjectType(workspacePath); // Prefer Spec Kit structure if available if (detection.hasSpecKit) { const specsDir = path.join(workspacePath, 'specs'); if (await directoryExists(specsDir)) { return specsDir; } } // Fall back to DinCoder structure if (detection.hasDincoder) { const dincoderSpecsDir = path.join(workspacePath, '.dincoder', 'specs'); await fs.mkdir(dincoderSpecsDir, { recursive: true }); return dincoderSpecsDir; } // Create new Spec Kit structure by default const specsDir = path.join(workspacePath, 'specs'); await fs.mkdir(specsDir, { recursive: true }); return specsDir; } /** * Get next feature number for specs directory */ export async function getNextFeatureNumber(specsDir: string): Promise<string> { try { const entries = await fs.readdir(specsDir); // Find directories matching pattern ###-feature-name const featureNumbers = entries .filter(entry => /^\d{3}-/.test(entry)) .map(entry => parseInt(entry.substring(0, 3), 10)) .filter(num => !isNaN(num)); if (featureNumbers.length === 0) { return '001'; } const maxNumber = Math.max(...featureNumbers); return String(maxNumber + 1).padStart(3, '0'); } catch { return '001'; } } /** * Create feature directory with Spec Kit structure */ export async function createFeatureDirectory( specsDir: string, featureName: string ): Promise<string> { const featureNumber = await getNextFeatureNumber(specsDir); const sanitizedName = featureName .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, ''); const featureDirName = `${featureNumber}-${sanitizedName}`; const featurePath = path.join(specsDir, featureDirName); // Create feature directory and subdirectories await fs.mkdir(featurePath, { recursive: true }); await fs.mkdir(path.join(featurePath, 'contracts'), { recursive: true }); return featurePath; } /** * Migrate from DinCoder to Spec Kit structure */ export async function migrateDincoderToSpecKit(workspacePath: string): Promise<void> { const dincoderDir = path.join(workspacePath, '.dincoder'); if (!await directoryExists(dincoderDir)) { throw new Error('No .dincoder directory found to migrate'); } // Create Spec Kit directories const specKitDirs = ['memory', 'scripts', 'templates', 'specs']; for (const dir of specKitDirs) { await fs.mkdir(path.join(workspacePath, dir), { recursive: true }); } // Copy any existing specs from .dincoder to specs/ const dincoderSpecsDir = path.join(dincoderDir, 'specs'); if (await directoryExists(dincoderSpecsDir)) { const specsDir = path.join(workspacePath, 'specs'); try { const entries = await fs.readdir(dincoderSpecsDir); for (const entry of entries) { const sourcePath = path.join(dincoderSpecsDir, entry); const targetPath = path.join(specsDir, entry); // Copy recursively await fs.cp(sourcePath, targetPath, { recursive: true }); } } catch (error) { console.error('Error migrating specs:', error); } } // Create constitution.md in memory/ const constitutionContent = `# Project Constitution ## Principles - Simplicity first - Test-driven development - Clear documentation - Minimal dependencies ## Rules - Every feature starts with a spec - Tests before implementation - No code without purpose - Document decisions in research.md `; await fs.writeFile( path.join(workspacePath, 'memory', 'constitution.md'), constitutionContent ); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/flight505/MCP_DinCoder'

If you have feedback or need assistance with the MCP directory API, please join our Discord server