const path = require("path"); const fs = require("fs/promises"); /** * Feature Loader - Handles loading and selecting features from feature_list.json */ class FeatureLoader { /** * Load features from .automaker/feature_list.json */ async loadFeatures(projectPath) { const featuresPath = path.join( projectPath, ".automaker", "feature_list.json" ); try { const content = await fs.readFile(featuresPath, "utf-8"); const features = JSON.parse(content); // Ensure each feature has an ID return features.map((f, index) => ({ ...f, id: f.id || `feature-${index}-${Date.now()}`, })); } catch (error) { console.error("[FeatureLoader] Failed to load features:", error); return []; } } /** * Update feature status in .automaker/feature_list.json * @param {string} featureId - The ID of the feature to update * @param {string} status - The new status * @param {string} projectPath - Path to the project * @param {string} [summary] - Optional summary of what was done * @param {string} [error] - Optional error message if feature errored */ async updateFeatureStatus(featureId, status, projectPath, summary, error) { const featuresPath = path.join( projectPath, ".automaker", "feature_list.json" ); // 🛡️ SAFETY: Create backup before any modification const backupPath = path.join( projectPath, ".automaker", "feature_list.backup.json" ); try { const originalContent = await fs.readFile(featuresPath, "utf-8"); await fs.writeFile(backupPath, originalContent, "utf-8"); console.log(`[FeatureLoader] Created backup at ${backupPath}`); } catch (error) { console.warn(`[FeatureLoader] Could not create backup: ${error.message}`); } const features = await this.loadFeatures(projectPath); // 🛡️ VALIDATION: Ensure we loaded features successfully if (!Array.isArray(features)) { throw new Error("CRITICAL: features is not an array - aborting to prevent data loss"); } if (features.length === 0) { console.warn(`[FeatureLoader] WARNING: Feature list is empty. This may indicate corruption.`); // Try to restore from backup try { const backupContent = await fs.readFile(backupPath, "utf-8"); const backupFeatures = JSON.parse(backupContent); if (Array.isArray(backupFeatures) && backupFeatures.length > 0) { console.log(`[FeatureLoader] Restored ${backupFeatures.length} features from backup`); // Use backup features instead features.length = 0; features.push(...backupFeatures); } } catch (backupError) { console.error(`[FeatureLoader] Could not restore from backup: ${backupError.message}`); } } const feature = features.find((f) => f.id === featureId); if (!feature) { console.error(`[FeatureLoader] Feature ${featureId} not found`); return; } // Update the status field feature.status = status; // Update the summary field if provided if (summary) { feature.summary = summary; } // Update the error field (set or clear) if (error) { feature.error = error; } else { // Clear any previous error when status changes without error delete feature.error; } // Save back to file const toSave = features.map((f) => { const featureData = { id: f.id, category: f.category, description: f.description, steps: f.steps, status: f.status, }; // Preserve optional fields if they exist if (f.skipTests !== undefined) { featureData.skipTests = f.skipTests; } if (f.images !== undefined) { featureData.images = f.images; } if (f.imagePaths !== undefined) { featureData.imagePaths = f.imagePaths; } if (f.startedAt !== undefined) { featureData.startedAt = f.startedAt; } if (f.summary !== undefined) { featureData.summary = f.summary; } if (f.model !== undefined) { featureData.model = f.model; } if (f.thinkingLevel !== undefined) { featureData.thinkingLevel = f.thinkingLevel; } if (f.error !== undefined) { featureData.error = f.error; } // Preserve worktree info if (f.worktreePath !== undefined) { featureData.worktreePath = f.worktreePath; } if (f.branchName !== undefined) { featureData.branchName = f.branchName; } return featureData; }); // 🛡️ FINAL VALIDATION: Ensure we're not writing an empty array if (!Array.isArray(toSave) || toSave.length === 0) { throw new Error("CRITICAL: Attempted to save empty feature list - aborting to prevent data loss"); } await fs.writeFile(featuresPath, JSON.stringify(toSave, null, 2), "utf-8"); console.log(`[FeatureLoader] Updated feature ${featureId}: status=${status}${summary ? `, summary="${summary}"` : ""}`); console.log(`[FeatureLoader] Successfully saved ${toSave.length} features to feature_list.json`); } /** * Select the next feature to implement * Prioritizes: earlier features in the list that are not verified or waiting_approval */ selectNextFeature(features) { // Find first feature that is in backlog or in_progress status // Skip verified and waiting_approval (which needs user input) return features.find((f) => f.status !== "verified" && f.status !== "waiting_approval"); } /** * Update worktree info for a feature * @param {string} featureId - The ID of the feature to update * @param {string} projectPath - Path to the project * @param {string|null} worktreePath - Path to the worktree (null to clear) * @param {string|null} branchName - Name of the feature branch (null to clear) */ async updateFeatureWorktree(featureId, projectPath, worktreePath, branchName) { const featuresPath = path.join( projectPath, ".automaker", "feature_list.json" ); const features = await this.loadFeatures(projectPath); if (!Array.isArray(features) || features.length === 0) { console.error("[FeatureLoader] Cannot update worktree: feature list is empty"); return; } const feature = features.find((f) => f.id === featureId); if (!feature) { console.error(`[FeatureLoader] Feature ${featureId} not found`); return; } // Update or clear worktree info if (worktreePath) { feature.worktreePath = worktreePath; feature.branchName = branchName; } else { delete feature.worktreePath; delete feature.branchName; } // Save back to file (reuse the same mapping logic) const toSave = features.map((f) => { const featureData = { id: f.id, category: f.category, description: f.description, steps: f.steps, status: f.status, }; if (f.skipTests !== undefined) featureData.skipTests = f.skipTests; if (f.images !== undefined) featureData.images = f.images; if (f.imagePaths !== undefined) featureData.imagePaths = f.imagePaths; if (f.startedAt !== undefined) featureData.startedAt = f.startedAt; if (f.summary !== undefined) featureData.summary = f.summary; if (f.model !== undefined) featureData.model = f.model; if (f.thinkingLevel !== undefined) featureData.thinkingLevel = f.thinkingLevel; if (f.error !== undefined) featureData.error = f.error; if (f.worktreePath !== undefined) featureData.worktreePath = f.worktreePath; if (f.branchName !== undefined) featureData.branchName = f.branchName; return featureData; }); await fs.writeFile(featuresPath, JSON.stringify(toSave, null, 2), "utf-8"); console.log(`[FeatureLoader] Updated feature ${featureId}: worktreePath=${worktreePath}, branchName=${branchName}`); } } module.exports = new FeatureLoader();