diff --git a/bin/coderrr.js b/bin/coderrr.js index 69fff06..e791e7b 100644 --- a/bin/coderrr.js +++ b/bin/coderrr.js @@ -15,15 +15,10 @@ const Agent = require('../src/agent'); const configManager = require('../src/configManager'); const { getProviderChoices, getModelChoices, getProvider, validateApiKey } = require('../src/providers'); const { tryExtractJSON } = require('../src/utils'); - +const { displayRecipeList } = require('../src/recipeUI'); +const recipeManager = require('../src/recipeManager'); const { displayInsights } = require('../src/insightsUI'); -program - .command('insights') - .description('Display local usage statistics and task history') - .action(() => { - displayInsights(); - }); // Optional: Load .env from user's home directory (for advanced users who want custom backend) const homeConfigPath = path.join(os.homedir(), '.coderrr', '.env'); if (fs.existsSync(homeConfigPath)) { @@ -41,6 +36,33 @@ program .description('AI Coding Agent CLI - Your personal coding assistant') .version('1.0.0'); +// Recipe command - manage and run custom coding recipes +program + .command('recipe [name]') + .description('Manage and run custom coding recipes') + .option('-l, --list', 'List all available recipes') + .action((name, options) => { + if (options.list || !name) { + displayRecipeList(); + } else { + const recipe = recipeManager.getRecipe(name); + if (recipe) { + console.log(`Running recipe: ${recipe.name}...`); + // Logic to pass tasks to the agent would go here + } else { + console.log(`Recipe "${name}" not found.`); + } + } + }); + +// Insights command - display local usage statistics +program + .command('insights') + .description('Display local usage statistics and task history') + .action(() => { + displayInsights(); + }); + // Config command - configure provider and API key program .command('config') diff --git a/docs/recipes.md b/docs/recipes.md new file mode 100644 index 0000000..7905a6c --- /dev/null +++ b/docs/recipes.md @@ -0,0 +1,18 @@ +# Recipe System + +Recipes are pre-defined sets of tasks that Coderrr can execute. + +## Usage +List available recipes: +`coderrr recipe --list` + +Run a recipe: +`coderrr recipe ` + +## Creating your own +Save a `.json` file in `~/.coderrr/recipes/`: +```json +{ + "name": "Quick Express", + "tasks": ["Initialize npm", "Install express", "Create app.js"] +} \ No newline at end of file diff --git a/src/recipeManager.js b/src/recipeManager.js new file mode 100644 index 0000000..7383624 --- /dev/null +++ b/src/recipeManager.js @@ -0,0 +1,42 @@ +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +const RECIPES_DIR = path.join(os.homedir(), '.coderrr', 'recipes'); + +class RecipeManager { + constructor() { + this.ensureDirectory(); + } + + ensureDirectory() { + if (!fs.existsSync(RECIPES_DIR)) { + fs.mkdirSync(RECIPES_DIR, { recursive: true }); + // Add a default "Hello World" recipe + const defaultRecipe = { + name: "ping", + description: "A simple health check for the recipe system", + tasks: ["Create a file named ALIVE.md with the text 'Coderrr is here'"] + }; + fs.writeFileSync(path.join(RECIPES_DIR, 'ping.json'), JSON.stringify(defaultRecipe, null, 2)); + } + } + + listRecipes() { + const files = fs.readdirSync(RECIPES_DIR).filter(f => f.endsWith('.json')); + return files.map(f => { + const content = JSON.parse(fs.readFileSync(path.join(RECIPES_DIR, f), 'utf8')); + return { id: f.replace('.json', ''), ...content }; + }); + } + + getRecipe(name) { + const filePath = path.join(RECIPES_DIR, `${name}.json`); + if (fs.existsSync(filePath)) { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); + } + return null; + } +} + +module.exports = new RecipeManager(); \ No newline at end of file diff --git a/src/recipeUI.js b/src/recipeUI.js new file mode 100644 index 0000000..d185624 --- /dev/null +++ b/src/recipeUI.js @@ -0,0 +1,19 @@ +const chalk = require('chalk'); +const recipeManager = require('./recipeManager'); + +function displayRecipeList() { + const recipes = recipeManager.listRecipes(); + console.log('\n' + chalk.magenta.bold('📜 AVAILABLE CODERRR RECIPES')); + console.log(chalk.gray('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')); + + if (recipes.length === 0) { + console.log(chalk.yellow('No recipes found in ~/.coderrr/recipes')); + } else { + recipes.forEach(r => { + console.log(`${chalk.cyan.bold(r.id)}: ${chalk.white(r.description || 'No description')}`); + }); + } + console.log(chalk.gray('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n')); +} + +module.exports = { displayRecipeList }; \ No newline at end of file diff --git a/src/utils/recipeValidator.js b/src/utils/recipeValidator.js new file mode 100644 index 0000000..981fa15 --- /dev/null +++ b/src/utils/recipeValidator.js @@ -0,0 +1,16 @@ +/** + * Validates the structure of a custom recipe + */ +const validateRecipe = (recipe) => { + const errors = []; + if (!recipe.name) errors.push("Missing 'name' field"); + if (!Array.isArray(recipe.tasks) || recipe.tasks.length === 0) { + errors.push("'tasks' must be a non-empty array"); + } + return { + valid: errors.length === 0, + errors + }; +}; + +module.exports = { validateRecipe }; \ No newline at end of file diff --git a/test/recipes.test.js b/test/recipes.test.js new file mode 100644 index 0000000..cdb3256 --- /dev/null +++ b/test/recipes.test.js @@ -0,0 +1,10 @@ +const recipeManager = require('../src/recipeManager'); + +describe('Recipe System', () => { + test('should find the default ping recipe', () => { + const recipes = recipeManager.listRecipes(); + const ping = recipes.find(r => r.id === 'ping'); + expect(ping).toBeDefined(); + expect(ping.name).toBe('ping'); + }); +}); \ No newline at end of file