-
-
Notifications
You must be signed in to change notification settings - Fork 29
feat: add extensible recipe registry and CLI command #125
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
3c45703
6460639
e3dd274
dbc3297
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"] | ||
| } | ||
| 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Add a default "Hello World" recipe | |
| // Add a default "ping" health check recipe |
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.
JSON.parse is called on each recipe file without any error handling, so a single malformed or partially-written JSON file in the recipes directory will throw and break listRecipes() entirely. Consider wrapping this read/parse in a try/catch and either skipping invalid recipes or surfacing a clear validation error, potentially reusing the recipe validator utility.
| 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')); | |
| const recipes = []; | |
| for (const f of files) { | |
| const filePath = path.join(RECIPES_DIR, f); | |
| try { | |
| const raw = fs.readFileSync(filePath, 'utf8'); | |
| const content = JSON.parse(raw); | |
| recipes.push({ id: f.replace('.json', ''), ...content }); | |
| } catch (error) { | |
| console.error(`Failed to read or parse recipe file "${f}": ${error.message}`); | |
| } | |
| } | |
| return recipes; | |
| } | |
| getRecipe(name) { | |
| const filePath = path.join(RECIPES_DIR, `${name}.json`); | |
| if (fs.existsSync(filePath)) { | |
| try { | |
| const raw = fs.readFileSync(filePath, 'utf8'); | |
| return JSON.parse(raw); | |
| } catch (error) { | |
| throw new Error(`Failed to read or parse recipe "${name}": ${error.message}`); | |
| } |
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.
getRecipe also calls JSON.parse without guarding against invalid JSON, which means a corrupt recipe file will cause the CLI command to throw instead of returning null or a structured error. It would be safer to catch parse errors here and either return null or propagate a user-friendly error, ideally using the shared recipe validation logic.
| return JSON.parse(fs.readFileSync(filePath, 'utf8')); | |
| const content = fs.readFileSync(filePath, 'utf8'); | |
| try { | |
| return JSON.parse(content); | |
| } catch (error) { | |
| // If the recipe file contains invalid JSON, treat it as unavailable | |
| console.error(`Failed to parse recipe "${name}" from ${filePath}: ${error.message}`); | |
| 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.gray('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n')); | ||
| } | ||
|
|
||
| module.exports = { displayRecipeList }; |
| 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"); | ||
| } | ||
| return { | ||
| valid: errors.length === 0, | ||
| errors | ||
| }; | ||
| }; | ||
|
|
||
| module.exports = { validateRecipe }; | ||
|
Comment on lines
+4
to
+16
|
||
| 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
+8
|
||
| }); | ||
| }); | ||
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 JSON example code block is not closed with a matching fence, which will cause the Markdown renderer to treat the rest of the document as part of the code block. Please add a closing ``` line after the JSON snippet so the usage text renders correctly.