agent-rules-generator
Version:
Interactive CLI tool to generate .agent.md and .windsurfrules files for AI-assisted development
427 lines (361 loc) ⢠12.7 kB
JavaScript
/**
* Recipe Management Module
* Handles recipe selection, searching, and application
*
* This module provides a comprehensive interface for managing recipes,
* including browsing, searching, applying, and refreshing recipes from
* remote repositories. It integrates with both standard recipes and
* Windsurf-specific recipes.
*
* @module RecipeManager
* @author ubuntupunk
* @since 1.2.0
*/
const chalk = require('chalk');
const inquirer = require('inquirer').default;
const {
loadRecipes,
searchRecipes,
refreshRecipes,
getRecipe
} = require('./recipes_lib');
const {
fetchWindsurfRecipes,
listWindsurfRecipes,
searchWindsurfRecipes,
refreshWindsurfRecipes
} = require('./windsurf_scraper');
/**
* Recipe Manager Class
*
* Manages all recipe-related operations including selection, application,
* searching, and integration with Windsurf recipes.
*
* @class RecipeManager
*/
class RecipeManager {
/**
* Creates a new RecipeManager instance
*
* @param {Object} config - Configuration object containing project settings
* @param {Object} config.overview - Project overview information
* @param {Object} config.technologyStack - Technology stack configuration
*/
constructor(config) {
this.config = config;
}
async selectRecipe() {
console.log(chalk.blue('\nš³ Recipe Selection'));
const { choice } = await inquirer.prompt([
{
type: 'list',
name: 'choice',
message: 'How would you like to set up your project?',
choices: [
'Use a recipe (recommended)',
'Manual setup',
'Search recipes',
'Windsurf recipes',
'Refresh recipes',
'Cache management',
'Repository settings'
]
}
]);
if (choice === 'Use a recipe (recommended)') {
return await this.browseRecipes();
}
if (choice === 'Manual setup') {
return 'manual';
}
if (choice === 'Search recipes') {
await this.searchRecipesCommand();
return await this.selectRecipe();
}
if (choice === 'Windsurf recipes') {
await this.handleWindsurfRecipes();
return await this.selectRecipe();
}
if (choice === 'Refresh recipes') {
await this.refreshRecipesCommand();
return await this.selectRecipe();
}
if (choice === 'Cache management') {
return 'cache_management';
}
if (choice === 'Repository settings') {
return 'repository_settings';
}
}
/**
* Browse and select from available recipes
*
* Loads recipes from the remote repository, displays them to the user,
* and allows selection and application of a chosen recipe.
*
* @async
* @returns {Promise<string>} Status of the operation ('recipe_applied', etc.)
* @throws {Error} When recipe loading fails
*/
async browseRecipes() {
console.log(chalk.blue('\nš Loading recipes...'));
try {
const recipes = await loadRecipes();
const recipeKeys = Object.keys(recipes);
if (recipeKeys.length === 0) {
console.log(chalk.yellow('No recipes found. Try refreshing recipes first.'));
return await this.selectRecipe();
}
console.log(chalk.green(`\nā
Found ${recipeKeys.length} recipes:\n`));
recipeKeys.forEach((key, index) => {
const recipe = recipes[key];
console.log(chalk.cyan(`${index + 1}. ${recipe.name}`));
console.log(chalk.gray(` ${recipe.description}`));
console.log(chalk.gray(` Category: ${recipe.category}`));
console.log();
});
const { selectedRecipe } = await inquirer.prompt([
{
type: 'list',
name: 'selectedRecipe',
message: 'Select a recipe:',
choices: recipeKeys.map(key => ({
name: `${recipes[key].name} - ${recipes[key].description}`,
value: key
}))
}
]);
await this.applyRecipe(selectedRecipe, recipes);
return 'recipe_applied';
} catch (error) {
console.log(chalk.red(`ā Error loading recipes: ${error.message}`));
return await this.selectRecipe();
}
}
async applyRecipe(recipeKey, recipes) {
const recipe = recipes[recipeKey];
console.log(chalk.green(`\nā
Selected recipe: ${recipe.name}`));
// Apply the recipe's tech stack to our configuration
this.config.technologyStack = { ...recipe.techStack };
console.log(chalk.gray(`š Applied tech stack: ${JSON.stringify(recipe.techStack)}`));
// Ask if user wants to customize
const { customize } = await inquirer.prompt([
{
type: 'confirm',
name: 'customize',
message: 'Would you like to customize the tech stack?',
default: false
}
]);
if (customize) {
return 'customize_tech_stack';
}
return 'recipe_applied';
}
async searchRecipesCommand() {
const { query } = await inquirer.prompt([
{
type: 'input',
name: 'query',
message: 'Search recipes (technology, framework, etc.):',
validate: input => input.trim().length > 0 || 'Please enter a search term'
}
]);
console.log(chalk.blue(`\nš Searching recipes for: "${query}"`));
try {
const recipes = await loadRecipes();
const matchingKeys = await searchRecipes(query, recipes);
if (matchingKeys.length === 0) {
console.log(chalk.yellow('No matching recipes found.'));
return;
}
console.log(chalk.green(`\nā
Found ${matchingKeys.length} matching recipes:`));
matchingKeys.forEach((key, index) => {
const recipe = recipes[key];
console.log(chalk.cyan(`${index + 1}. ${recipe.name}`));
console.log(chalk.gray(` ${recipe.description}`));
console.log(chalk.gray(` Category: ${recipe.category}`));
console.log();
});
} catch (error) {
console.log(chalk.red(`ā Error searching recipes: ${error.message}`));
}
}
async refreshRecipesCommand() {
console.log(chalk.blue('\nš Refreshing recipes from remote repository...'));
try {
const recipes = await refreshRecipes();
const count = Object.keys(recipes).length;
console.log(chalk.green(`ā
Successfully refreshed ${count} recipes`));
} catch (error) {
console.error(chalk.red(`ā Error refreshing recipes: ${error.message}`));
}
}
/**
* Handle Windsurf recipes menu and operations
*
* Provides a menu interface for browsing, searching, and refreshing
* Windsurf-specific recipes scraped from the Windsurf directory.
*
* @async
* @returns {Promise<void>}
*/
async handleWindsurfRecipes() {
console.log(chalk.blue('\nš Windsurf Recipes'));
const { action } = await inquirer.prompt([
{
type: 'list',
name: 'action',
message: 'What would you like to do with Windsurf recipes?',
choices: [
'Browse Windsurf recipes',
'Search Windsurf recipes',
'Refresh Windsurf recipes',
'Back to main menu'
]
}
]);
if (action === 'Browse Windsurf recipes') {
return await this.browseWindsurfRecipes();
} else if (action === 'Search Windsurf recipes') {
return await this.searchWindsurfRecipes();
} else if (action === 'Refresh Windsurf recipes') {
await this.refreshWindsurfRecipes();
}
}
async browseWindsurfRecipes() {
console.log(chalk.blue('\nš Loading Windsurf recipes...'));
try {
const recipes = await listWindsurfRecipes();
if (recipes.length === 0) {
console.log(chalk.yellow('No Windsurf recipes found. Try refreshing first.'));
return;
}
console.log(chalk.green(`\nā
Found ${recipes.length} Windsurf recipes:\n`));
recipes.forEach((recipe, index) => {
console.log(chalk.cyan(`${index + 1}. ${recipe.name}`));
console.log(chalk.gray(` Category: ${recipe.category}`));
console.log(chalk.gray(` Tech Stack: ${JSON.stringify(recipe.techStack)}`));
console.log();
});
const { useWindsurfRecipe } = await inquirer.prompt([
{
type: 'confirm',
name: 'useWindsurfRecipe',
message: 'Would you like to use one of these Windsurf recipes?',
default: false
}
]);
if (useWindsurfRecipe) {
const { selectedRecipe } = await inquirer.prompt([
{
type: 'list',
name: 'selectedRecipe',
message: 'Select a Windsurf recipe:',
choices: recipes.map((recipe) => ({
name: `${recipe.name} (${recipe.category})`,
value: recipe.key
}))
}
]);
return await this.applyWindsurfRecipe(selectedRecipe);
}
} catch (error) {
console.log(chalk.red(`ā Error loading Windsurf recipes: ${error.message}`));
}
}
async applyWindsurfRecipe(recipeKey) {
console.log(chalk.blue(`\nš Applying Windsurf recipe: ${recipeKey}`));
try {
const windsurfRecipes = await fetchWindsurfRecipes();
const recipe = windsurfRecipes[recipeKey];
if (!recipe) {
console.log(chalk.red('ā Recipe not found'));
return;
}
this.config.technologyStack = { ...recipe.techStack };
this.config.windsurfRules = recipe.windsurfRules;
console.log(chalk.green(`ā
Applied Windsurf recipe: ${recipe.name}`));
console.log(chalk.gray(`š Tech stack: ${JSON.stringify(recipe.techStack)}`));
const { customize } = await inquirer.prompt([
{
type: 'confirm',
name: 'customize',
message: 'Would you like to customize the tech stack?',
default: false
}
]);
if (customize) {
return 'customize_tech_stack';
}
} catch (error) {
console.log(chalk.red(`ā Error applying Windsurf recipe: ${error.message}`));
}
}
async searchWindsurfRecipes() {
const { query } = await inquirer.prompt([
{
type: 'input',
name: 'query',
message: 'Search Windsurf recipes (technology, framework, etc.):',
validate: input => input.trim().length > 0 || 'Please enter a search term'
}
]);
console.log(chalk.blue(`\nš Searching Windsurf recipes for: "${query}"`));
try {
const matchingKeys = await searchWindsurfRecipes(query);
if (matchingKeys.length === 0) {
console.log(chalk.yellow('No matching Windsurf recipes found.'));
return;
}
console.log(chalk.green(`\nā
Found ${matchingKeys.length} matching Windsurf recipes:`));
const windsurfRecipes = await fetchWindsurfRecipes();
matchingKeys.forEach((key, index) => {
const recipe = windsurfRecipes[key];
console.log(chalk.cyan(`${index + 1}. ${recipe.name}`));
console.log(chalk.gray(` Category: ${recipe.category}`));
console.log(chalk.gray(` Tech Stack: ${JSON.stringify(recipe.techStack)}`));
console.log();
});
// Ask if user wants to select one of the found recipes
const { useFoundRecipe } = await inquirer.prompt([
{
type: 'confirm',
name: 'useFoundRecipe',
message: 'Would you like to use one of these Windsurf recipes?',
default: true
}
]);
if (useFoundRecipe) {
const { selectedRecipe } = await inquirer.prompt([
{
type: 'list',
name: 'selectedRecipe',
message: 'Select a Windsurf recipe:',
choices: matchingKeys.map((key) => {
const recipe = windsurfRecipes[key];
return {
name: `${recipe.name} (${recipe.category})`,
value: key
};
})
}
]);
return await this.applyWindsurfRecipe(selectedRecipe);
}
} catch (error) {
console.log(chalk.red(`ā Error searching Windsurf recipes: ${error.message}`));
}
}
async refreshWindsurfRecipes() {
console.log(chalk.blue('\nš Refreshing Windsurf recipes...'));
try {
const recipes = await refreshWindsurfRecipes();
const count = Object.keys(recipes).length;
console.log(chalk.green(`ā
Successfully refreshed ${count} Windsurf recipes`));
} catch (error) {
console.log(chalk.red(`ā Failed to refresh Windsurf recipes: ${error.message}`));
}
}
}
module.exports = { RecipeManager };