UNPKG

spec-flow

Version:

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

396 lines (335 loc) 12.4 kB
const fs = require('fs-extra'); const path = require('path'); const inquirer = require('inquirer'); const chalk = require('chalk'); const { install, update, USER_DATA_DIRECTORIES } = require('./install'); const { printHeader, printSuccess, printStep, printWarning, printError } = require('./utils'); const { detectConflicts, formatConflicts, formatActions, STRATEGIES } = require('./conflicts'); const { checkExistingInstallation } = require('./validate'); const VERSION = require('../package.json').version; /** * Run interactive installation wizard * @param {Object} options - Wizard options * @param {string} options.targetDir - Target directory * @param {boolean} options.nonInteractive - Skip prompts, use defaults * @param {string} options.conflictStrategy - Conflict resolution strategy (optional) * @returns {Promise<Object>} { success: boolean, error: string|null } */ async function runWizard(options) { const { targetDir, nonInteractive = false, conflictStrategy: providedStrategy } = options; printHeader('Spec-Flow Installation Wizard'); // Check if already installed BEFORE starting wizard const existing = await checkExistingInstallation(targetDir); if (existing.installed) { printWarning('Spec-Flow is already installed in this directory'); console.log(chalk.gray(' .claude directory: ') + (existing.hasClaudeDir ? chalk.green('✓') : chalk.gray('✗'))); console.log(chalk.gray(' .spec-flow directory: ') + (existing.hasSpecFlowDir ? chalk.green('✓') : chalk.gray('✗'))); console.log(''); if (nonInteractive) { // Auto-update in non-interactive mode printStep('Auto-updating existing installation...'); console.log(''); const updateResult = await update({ targetDir, force: false, verbose: true }); if (!updateResult.success) { return updateResult; } printSuccess('\nUpdate complete!'); console.log(chalk.cyan(`\nSpec-Flow version: ${chalk.bold(VERSION)}`)); // Show backup information if (updateResult.backupPaths && Object.keys(updateResult.backupPaths).length > 0) { console.log(chalk.cyan('\nBackups created:')); for (const [dir, backupPath] of Object.entries(updateResult.backupPaths)) { console.log(chalk.gray(` ${dir}: ${path.basename(backupPath)}`)); } console.log(chalk.yellow('\nNote: Backups preserved for safety. Remove *-backup-* folders when confident.\n')); } return { success: true, error: null }; } // Interactive mode - ask user what to do const { action } = await inquirer.prompt([ { type: 'list', name: 'action', message: 'What would you like to do?', default: 'update', choices: [ { name: 'Update existing installation (recommended) - Preserves your data', value: 'update' }, { name: 'Cancel installation', value: 'cancel' } ] } ]); if (action === 'cancel') { return { success: false, error: 'Installation cancelled by user' }; } if (action === 'update') { console.log(''); printStep('Updating Spec-Flow'); console.log(''); const updateResult = await update({ targetDir, force: false, verbose: true }); if (!updateResult.success) { return updateResult; } printSuccess('\nUpdate complete!'); console.log(chalk.cyan(`\nSpec-Flow version: ${chalk.bold(VERSION)}`)); // Show backup information if (updateResult.backupPaths && Object.keys(updateResult.backupPaths).length > 0) { console.log(chalk.cyan('\nBackups created:')); for (const [dir, backupPath] of Object.entries(updateResult.backupPaths)) { console.log(chalk.gray(` ${dir}: ${path.basename(backupPath)}`)); } console.log(chalk.yellow('\nNote: Backups preserved for safety. Remove *-backup-* folders when confident.')); } console.log(''); console.log(chalk.white('Next steps:')); console.log(chalk.green(' 1. Open project in Claude Code')); console.log(chalk.green(' 2. Run /roadmap') + chalk.gray(' to plan features')); console.log(chalk.green(' 3. Run /spec-flow "feature-name"') + chalk.gray(' to start building\n')); return { success: true, error: null }; } } // Fresh installation for non-existing installations if (nonInteractive) { printStep('Running in non-interactive mode (using defaults)'); console.log(''); // Run installation with defaults (merge strategy or provided strategy) // CRITICAL: excludeDirectories protects user data in brownfield projects return await install({ targetDir, preserveMemory: false, verbose: true, conflictStrategy: providedStrategy || STRATEGIES.MERGE, excludeDirectories: USER_DATA_DIRECTORIES }); } // Interactive mode console.log(chalk.white('This wizard will help you set up Spec-Flow for your project.\n')); // Step 1: Confirm target directory printStep('Step 1: Target Directory'); console.log(chalk.gray(` Installing to: ${targetDir}\n`)); const { confirmDir } = await inquirer.prompt([ { type: 'confirm', name: 'confirmDir', message: 'Is this the correct directory?', default: true } ]); if (!confirmDir) { return { success: false, error: 'Installation cancelled by user' }; } // Step 2: Check for conflicts printStep('Step 2: Conflict Detection'); const filesToCheck = ['CLAUDE.md', 'QUICKSTART.md', 'LICENSE', '.claude', '.spec-flow']; const { conflicts, hasConflicts } = await detectConflicts(targetDir, filesToCheck); let conflictStrategy = providedStrategy || STRATEGIES.MERGE; // Use provided strategy or default if (hasConflicts) { if (providedStrategy) { // Strategy provided via CLI console.log(chalk.yellow('\n⚠ Conflicts detected:\n')); console.log(formatConflicts(conflicts)); console.log(''); console.log(chalk.cyan(`Using conflict strategy: ${providedStrategy}\n`)); } else { // Interactive mode - prompt for strategy console.log(chalk.yellow('\n⚠ Conflicts detected:\n')); console.log(formatConflicts(conflicts)); console.log(''); const { strategy } = await inquirer.prompt([ { type: 'list', name: 'strategy', message: 'How to handle conflicts?', default: STRATEGIES.MERGE, choices: [ { name: 'Smart merge (recommended) - Append to CLAUDE.md, rename others', value: STRATEGIES.MERGE }, { name: 'Backup & overwrite - Create backups, then install', value: STRATEGIES.BACKUP }, { name: 'Skip conflicts - Only install new files', value: STRATEGIES.SKIP }, { name: 'Force overwrite - Replace everything', value: STRATEGIES.FORCE } ] } ]); conflictStrategy = strategy; // Confirm destructive actions if (strategy === STRATEGIES.FORCE) { const { confirmForce } = await inquirer.prompt([ { type: 'confirm', name: 'confirmForce', message: 'This will permanently overwrite existing files. Continue?', default: false } ]); if (!confirmForce) { return { success: false, error: 'Installation cancelled to prevent data loss' }; } } } } else { printSuccess('No conflicts detected\n'); } // Step 3: Project setup questions console.log(''); printStep('Step 3: Project Configuration (Optional)'); console.log(chalk.gray(' These will be saved to .spec-flow/memory/constitution.md\n')); const config = await inquirer.prompt([ { type: 'input', name: 'projectName', message: 'Project name:', default: path.basename(targetDir) }, { type: 'input', name: 'description', message: 'Project description (optional):', default: '' }, { type: 'checkbox', name: 'stack', message: 'Select your tech stack:', choices: [ { name: 'Next.js (React)', value: 'nextjs' }, { name: 'FastAPI (Python)', value: 'fastapi' }, { name: 'Express (Node.js)', value: 'express' }, { name: 'Django (Python)', value: 'django' }, { name: 'PostgreSQL', value: 'postgresql' }, { name: 'MongoDB', value: 'mongodb' }, { name: 'TypeScript', value: 'typescript' }, { name: 'Other', value: 'other' } ] }, { type: 'confirm', name: 'setupMemory', message: 'Initialize memory files (roadmap, constitution, design inspirations)?', default: true } ]); // Step 4: Run installation console.log(''); printStep('Step 4: Installing Spec-Flow'); console.log(''); const result = await install({ targetDir, preserveMemory: false, verbose: true, conflictStrategy, excludeDirectories: USER_DATA_DIRECTORIES }); if (!result.success) { return result; } // Step 5: Initialize memory files if requested if (config.setupMemory) { console.log(''); printStep('Step 5: Initializing memory files'); await initializeMemory(targetDir, config); printSuccess('Memory files initialized'); } // Success! console.log(''); printSuccess('Installation complete!'); console.log(chalk.cyan(`\nSpec-Flow version: ${chalk.bold(VERSION)}`)); // Show conflict resolutions if any if (result.conflictActions && result.conflictActions.length > 0) { console.log(''); console.log(chalk.white('Conflict resolutions:')); console.log(formatActions(result.conflictActions)); } console.log(''); console.log(chalk.white('Next steps:')); console.log(chalk.gray(' 1. cd ' + targetDir)); console.log(chalk.gray(' 2. Open in Claude Code')); if (config.setupMemory) { console.log(chalk.green(' 3. Review .spec-flow/memory/ files')); console.log(chalk.green(' 4. Run /roadmap') + chalk.gray(' to plan your first features')); } else { console.log(chalk.green(' 3. Run /constitution') + chalk.gray(' to customize your standards')); console.log(chalk.green(' 4. Run /roadmap') + chalk.gray(' to plan your first features')); } console.log(chalk.green(' 5. Run /spec-flow "feature-name"') + chalk.gray(' to start building\n')); return { success: true, error: null }; } /** * Initialize memory files with user configuration * @param {string} targetDir - Target directory * @param {Object} config - Configuration from wizard */ async function initializeMemory(targetDir, config) { const memoryDir = path.join(targetDir, '.spec-flow', 'memory'); // Update constitution.md with project info const constitutionPath = path.join(memoryDir, 'constitution.md'); if (await fs.pathExists(constitutionPath)) { let constitution = await fs.readFile(constitutionPath, 'utf8'); // Replace placeholders constitution = constitution.replace('[Your Project Name]', config.projectName); if (config.description) { constitution = constitution.replace( '[Brief description of your project]', config.description ); } if (config.stack && config.stack.length > 0) { const stackList = config.stack.map(s => `- ${s}`).join('\n'); constitution = constitution.replace( '- [Your primary tech stack]', stackList ); } await fs.writeFile(constitutionPath, constitution, 'utf8'); } // Initialize empty roadmap const roadmapPath = path.join(memoryDir, 'roadmap.md'); if (await fs.pathExists(roadmapPath)) { const roadmapContent = `# Product Roadmap: ${config.projectName} ## Vision ${config.description || '[Add your product vision here]'} ## Now (In Progress) ## Next (Validated & Ready) ## Later (Backlog) ## Shipped (Completed) --- Last updated: ${new Date().toISOString().split('T')[0]} `; await fs.writeFile(roadmapPath, roadmapContent, 'utf8'); } } module.exports = { runWizard };