Major refactor of molecular chemistry to make protos ephemeral: - Formulas are now cooked directly to in-memory TemplateSubgraph - No more proto beads stored in the database Changes: - cook.go: Add cookFormulaToSubgraph() and resolveAndCookFormula() for in-memory formula cooking - template.go: Add VarDefs field to TemplateSubgraph for default value handling, add extractRequiredVariables() and applyVariableDefaults() helpers - pour.go: Try formula loading first for any name (not just mol-) - wisp.go: Same pattern as pour - mol_bond.go: Use resolveOrCookToSubgraph() for in-memory subgraphs - mol_catalog.go: List formulas from disk instead of DB proto beads - mol_distill.go: Output .formula.json files instead of proto beads Flow: Formula (.formula.json) -> pour/wisp (cook inline) -> Mol/Wisp 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
135 lines
3.6 KiB
Go
135 lines
3.6 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/steveyegge/beads/internal/ui"
|
|
)
|
|
|
|
// CatalogEntry represents a formula in the catalog.
|
|
type CatalogEntry struct {
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
Description string `json:"description"`
|
|
Source string `json:"source"`
|
|
Steps int `json:"steps"`
|
|
Vars []string `json:"vars,omitempty"`
|
|
}
|
|
|
|
var molCatalogCmd = &cobra.Command{
|
|
Use: "catalog",
|
|
Aliases: []string{"list", "ls"},
|
|
Short: "List available molecule formulas",
|
|
Long: `List formulas available for bd pour / bd wisp create.
|
|
|
|
Formulas are ephemeral proto definitions stored as .formula.json files.
|
|
They are cooked inline when pouring, never stored as database beads.
|
|
|
|
Search paths (in priority order):
|
|
1. .beads/formulas/ (project-level)
|
|
2. ~/.beads/formulas/ (user-level)
|
|
3. ~/gt/.beads/formulas/ (Gas Town level)`,
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
typeFilter, _ := cmd.Flags().GetString("type")
|
|
|
|
// Get all search paths and scan for formulas
|
|
searchPaths := getFormulaSearchPaths()
|
|
seen := make(map[string]bool)
|
|
var entries []CatalogEntry
|
|
|
|
for _, dir := range searchPaths {
|
|
formulas, err := scanFormulaDir(dir)
|
|
if err != nil {
|
|
continue // Skip inaccessible directories
|
|
}
|
|
|
|
for _, f := range formulas {
|
|
if seen[f.Formula] {
|
|
continue // Skip shadowed formulas
|
|
}
|
|
seen[f.Formula] = true
|
|
|
|
// Apply type filter
|
|
if typeFilter != "" && string(f.Type) != typeFilter {
|
|
continue
|
|
}
|
|
|
|
// Extract variable names
|
|
var varNames []string
|
|
for name := range f.Vars {
|
|
varNames = append(varNames, name)
|
|
}
|
|
sort.Strings(varNames)
|
|
|
|
entries = append(entries, CatalogEntry{
|
|
Name: f.Formula,
|
|
Type: string(f.Type),
|
|
Description: truncateDescription(f.Description, 60),
|
|
Source: f.Source,
|
|
Steps: countSteps(f.Steps),
|
|
Vars: varNames,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Sort by name
|
|
sort.Slice(entries, func(i, j int) bool {
|
|
return entries[i].Name < entries[j].Name
|
|
})
|
|
|
|
if jsonOutput {
|
|
outputJSON(entries)
|
|
return
|
|
}
|
|
|
|
if len(entries) == 0 {
|
|
fmt.Println("No formulas found.")
|
|
fmt.Println("\nTo create a formula, write a .formula.json file:")
|
|
fmt.Println(" .beads/formulas/my-workflow.formula.json")
|
|
fmt.Println("\nOr distill from existing work:")
|
|
fmt.Println(" bd mol distill <epic-id> my-workflow")
|
|
fmt.Println("\nTo instantiate from formula:")
|
|
fmt.Println(" bd pour <formula-name> --var key=value # persistent mol")
|
|
fmt.Println(" bd wisp create <formula-name> --var key=value # ephemeral wisp")
|
|
return
|
|
}
|
|
|
|
fmt.Printf("%s\n\n", ui.RenderPass("Formulas (for bd pour / bd wisp create):"))
|
|
|
|
// Group by type for display
|
|
byType := make(map[string][]CatalogEntry)
|
|
for _, e := range entries {
|
|
byType[e.Type] = append(byType[e.Type], e)
|
|
}
|
|
|
|
// Print workflow types first (most common for pour/wisp)
|
|
typeOrder := []string{"workflow", "expansion", "aspect"}
|
|
for _, t := range typeOrder {
|
|
typeEntries := byType[t]
|
|
if len(typeEntries) == 0 {
|
|
continue
|
|
}
|
|
|
|
typeIcon := getTypeIcon(t)
|
|
fmt.Printf("%s %s:\n", typeIcon, strings.Title(t))
|
|
|
|
for _, e := range typeEntries {
|
|
varInfo := ""
|
|
if len(e.Vars) > 0 {
|
|
varInfo = fmt.Sprintf(" (vars: %s)", strings.Join(e.Vars, ", "))
|
|
}
|
|
fmt.Printf(" %s: %s%s\n", ui.RenderAccent(e.Name), e.Description, varInfo)
|
|
}
|
|
fmt.Println()
|
|
}
|
|
},
|
|
}
|
|
|
|
func init() {
|
|
molCatalogCmd.Flags().String("type", "", "Filter by formula type (workflow, expansion, aspect)")
|
|
molCmd.AddCommand(molCatalogCmd)
|
|
}
|