UNPKG

spec-flow

Version:

Spec-Driven Development workflow toolkit for Claude Code - Build high-quality features faster with repeatable AI workflows

250 lines (217 loc) 6.83 kB
const fs = require('fs-extra'); const path = require('path'); const { execSync } = require('child_process'); const { printError, printWarning, printSuccess, isWritable } = require('./utils'); /** * Check if Git is installed * @returns {Object} { installed: boolean, version: string|null, error: string|null } */ function checkGit() { try { const version = execSync('git --version', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); return { installed: true, version, error: null }; } catch (error) { return { installed: false, version: null, error: 'Git is not installed or not in PATH' }; } } /** * Check Node.js version * @param {string} requiredVersion - Minimum required version (e.g., '16.0.0') * @returns {Object} { compatible: boolean, current: string, required: string, error: string|null } */ function checkNodeVersion(requiredVersion = '16.0.0') { const current = process.version.slice(1); // Remove 'v' prefix const [reqMajor] = requiredVersion.split('.').map(Number); const [curMajor] = current.split('.').map(Number); if (curMajor >= reqMajor) { return { compatible: true, current, required: requiredVersion, error: null }; } return { compatible: false, current, required: requiredVersion, error: `Node.js ${requiredVersion}+ required, but ${current} is installed` }; } /** * Check if directory exists and is writable * @param {string} dir - Directory path * @returns {Promise<Object>} { valid: boolean, exists: boolean, writable: boolean, error: string|null } */ async function checkDirectory(dir) { const exists = await fs.pathExists(dir); if (!exists) { return { valid: false, exists: false, writable: false, error: `Directory does not exist: ${dir}` }; } const writable = await isWritable(dir); if (!writable) { return { valid: false, exists: true, writable: false, error: `Directory is not writable: ${dir}` }; } return { valid: true, exists: true, writable: true, error: null }; } /** * Check if spec-flow is already installed in directory * @param {string} dir - Directory path * @returns {Promise<Object>} { installed: boolean, hasClaudeDir: boolean, hasSpecFlowDir: boolean } */ async function checkExistingInstallation(dir) { const claudeDir = path.join(dir, '.claude'); const specFlowDir = path.join(dir, '.spec-flow'); const hasClaudeDir = await fs.pathExists(claudeDir); const hasSpecFlowDir = await fs.pathExists(specFlowDir); return { installed: hasClaudeDir || hasSpecFlowDir, hasClaudeDir, hasSpecFlowDir }; } /** * Check if required installer scripts exist in package * @param {string} packageRoot - Package root directory * @returns {Promise<Object>} { valid: boolean, missing: string[], error: string|null } */ async function checkInstallerScripts(packageRoot) { const requiredFiles = [ path.join(packageRoot, '.claude', 'agents'), path.join(packageRoot, '.claude', 'commands'), path.join(packageRoot, '.spec-flow', 'templates'), path.join(packageRoot, '.spec-flow', 'memory'), path.join(packageRoot, 'CLAUDE.md'), path.join(packageRoot, 'QUICKSTART.md') ]; const missing = []; for (const file of requiredFiles) { if (!await fs.pathExists(file)) { missing.push(path.relative(packageRoot, file)); } } if (missing.length > 0) { return { valid: false, missing, error: `Package is corrupted. Missing: ${missing.join(', ')}` }; } return { valid: true, missing: [], error: null }; } /** * Run all pre-flight checks * @param {Object} options - Check options * @param {string} options.targetDir - Target directory * @param {string} options.packageRoot - Package root * @param {boolean} options.verbose - Show detailed output * @returns {Promise<Object>} { passed: boolean, checks: Object[], errors: string[] } */ async function runPreflightChecks(options) { const { targetDir, packageRoot, verbose = false } = options; const checks = []; const errors = []; // Node version const nodeCheck = checkNodeVersion('16.0.0'); checks.push({ name: 'Node.js version', ...nodeCheck }); if (!nodeCheck.compatible) { errors.push(nodeCheck.error); if (verbose) printError(nodeCheck.error); } else if (verbose) { printSuccess(`Node.js ${nodeCheck.current} (compatible)`); } // Git const gitCheck = checkGit(); checks.push({ name: 'Git', ...gitCheck }); if (!gitCheck.installed) { printWarning(gitCheck.error); printWarning('Git is recommended for spec-flow workflow but not required for installation'); } else if (verbose) { printSuccess(`Git ${gitCheck.version}`); } // Target directory const dirCheck = await checkDirectory(targetDir); checks.push({ name: 'Target directory', ...dirCheck }); if (!dirCheck.valid) { errors.push(dirCheck.error); if (verbose) printError(dirCheck.error); } else if (verbose) { printSuccess(`Target directory is writable: ${targetDir}`); } // Package integrity const scriptsCheck = await checkInstallerScripts(packageRoot); checks.push({ name: 'Package integrity', ...scriptsCheck }); if (!scriptsCheck.valid) { errors.push(scriptsCheck.error); if (verbose) printError(scriptsCheck.error); } else if (verbose) { printSuccess('Package integrity verified'); } return { passed: errors.length === 0, checks, errors }; } /** * Health check for installed spec-flow * @param {string} dir - Installation directory * @returns {Promise<Object>} { healthy: boolean, issues: string[], warnings: string[] } */ async function healthCheck(dir) { const issues = []; const warnings = []; const installation = await checkExistingInstallation(dir); if (!installation.installed) { issues.push('Spec-Flow not installed in this directory'); return { healthy: false, issues, warnings }; } // Check key directories const keyDirs = [ '.claude/agents', '.claude/commands', '.spec-flow/templates', '.spec-flow/memory' ]; for (const dir of keyDirs) { const fullPath = path.join(dir, dir); if (!await fs.pathExists(fullPath)) { issues.push(`Missing directory: ${dir}`); } } // Check key files const keyFiles = [ 'CLAUDE.md', 'QUICKSTART.md', '.claude/settings.example.json' ]; for (const file of keyFiles) { const fullPath = path.join(dir, file); if (!await fs.pathExists(fullPath)) { warnings.push(`Missing file: ${file}`); } } return { healthy: issues.length === 0, issues, warnings }; } module.exports = { checkGit, checkNodeVersion, checkDirectory, checkExistingInstallation, checkInstallerScripts, runPreflightChecks, healthCheck };