-
-
Notifications
You must be signed in to change notification settings - Fork 28
feat: add extensible recipe registry and CLI command #126
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
225d83e
6460639
e3dd274
dbc3297
eac657d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <name>` | ||
|
|
||
| ## Creating your own | ||
| Save a `.json` file in `~/.coderrr/recipes/`: | ||
| ```json | ||
| { | ||
| "name": "Quick Express", | ||
| "tasks": ["Initialize npm", "Install express", "Create app.js"] | ||
| } | ||
|
Comment on lines
+13
to
+18
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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 }; | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
|
Comment on lines
+27
to
+30
|
||||||||||||||||||||||||||||||||
| return files.map(f => { | |
| const content = JSON.parse(fs.readFileSync(path.join(RECIPES_DIR, f), 'utf8')); | |
| return { id: f.replace('.json', ''), ...content }; | |
| }); | |
| return files.reduce((recipes, f) => { | |
| try { | |
| const content = JSON.parse(fs.readFileSync(path.join(RECIPES_DIR, f), 'utf8')); | |
| recipes.push({ id: f.replace('.json', ''), ...content }); | |
| } catch (error) { | |
| // Skip malformed recipe files to avoid crashing the application | |
| } | |
| return recipes; | |
| }, []); |
Copilot
AI
Jan 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The getRecipe() method will throw an error if the recipe file contains malformed JSON. Similar to listRecipes(), there's no error handling. Following the error handling pattern seen in configManager.js (lines 31-40), consider wrapping the file read and JSON parse operations in a try-catch block and returning null on error, which is already the expected return value when a file doesn't exist.
| if (fs.existsSync(filePath)) { | |
| return JSON.parse(fs.readFileSync(filePath, 'utf8')); | |
| } | |
| return null; | |
| if (!fs.existsSync(filePath)) { | |
| return null; | |
| } | |
| try { | |
| const content = fs.readFileSync(filePath, 'utf8'); | |
| return JSON.parse(content); | |
| } catch (error) { | |
| // If the recipe file is malformed or unreadable, treat it as missing | |
| return null; | |
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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.cyan.bold(r.id)}: ${chalk.white(r.description || 'No description')}`); | |
| const hasDescription = typeof r.description === 'string' && r.description.trim().length > 0; | |
| if (hasDescription) { | |
| console.log(`${chalk.cyan.bold(r.id)}: ${chalk.white(r.description)}`); | |
| } else { | |
| console.log(chalk.cyan.bold(r.id)); | |
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+2
to
+9
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 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"); | |
| } | |
| * Validates the structure of a custom recipe. | |
| * | |
| * Notes: | |
| * - `name` is the human-readable display name for the recipe. | |
| * - The recipe "ID" is derived from the recipe filename (without `.json`), | |
| * not from any field within the recipe object. | |
| * - `description` is optional, but when present it should be a string. | |
| */ | |
| const validateRecipe = (recipe) => { | |
| const errors = []; | |
| // Basic shape check | |
| if (!recipe || typeof recipe !== 'object') { | |
| errors.push("Recipe must be an object"); | |
| } else { | |
| // Validate display name | |
| if (typeof recipe.name !== 'string' || recipe.name.trim().length === 0) { | |
| errors.push("Missing or invalid 'name' field (expected non-empty string)"); | |
| } | |
| // Validate tasks array | |
| if (!Array.isArray(recipe.tasks) || recipe.tasks.length === 0) { | |
| errors.push("'tasks' must be a non-empty array"); | |
| } | |
| // Validate optional description | |
| if (Object.prototype.hasOwnProperty.call(recipe, 'description') && | |
| typeof recipe.description !== 'string') { | |
| errors.push("If provided, 'description' must be a string"); | |
| } | |
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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'); | ||
|
||
| }); | ||
| }); | ||
|
Comment on lines
+3
to
+10
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The command syntax shown in the documentation uses backticks for inline code formatting, but the commands are split across two lines without proper markdown formatting. Line 7 shows
coderrr recipe --listas inline code, which is correct. However, lines 9-10 split the command explanation across two lines in a way that might be confusing. Consider reformatting to match line 6-7's pattern, using backticks around the command on a single line for consistency.