From 07fe8352d36374cdbd51808bc2d749170cc0da0c Mon Sep 17 00:00:00 2001 From: Kunal Kushwaha Date: Fri, 30 Jan 2026 13:18:22 +0900 Subject: [PATCH] feat(cli): implement template registry and overhaul lifecycle docs Introduces the **Template Registry** system to the `agk` CLI, enabling dynamic template management beyond built-ins. Key Changes: - Refactored `agk init`: Now seamlessly resolves templates from the official registry or custom URLs alongside built-ins. - New Commands: Added `agk template list`, `add`, and `remove` for managing local template caches. - Documentation: Completely rewrote READMEs to align with the "Complete Lifecycle" vision (Create, Distribute, Deploy, Trace). Added `docs/creating-templates.md`. - Cleanup: Standardized documentation format across `agk` and `agenticgokit`, removing inconsistent emojis and aligning roadmap status (e.g., SubWorkflows, MCP). --- README.md | 103 +++++---- cmd/init.go | 124 +++++----- cmd/template.go | 107 +++++++++ docs/creating-templates.md | 190 ++++++++++++++++ go.mod | 30 ++- go.sum | 100 ++++++++- pkg/registry/cache.go | 156 +++++++++++++ pkg/registry/fetcher.go | 144 ++++++++++++ pkg/registry/index.go | 43 ++++ pkg/registry/manifest.go | 146 ++++++++++++ pkg/registry/resolver.go | 157 +++++++++++++ pkg/scaffold/external_generator.go | 140 ++++++++++++ pkg/scaffold/service.go | 10 +- pkg/scaffold/template.go | 55 +---- pkg/scaffold/template_registry.go | 211 ++---------------- .../templates/mcp-tools/README.md.tmpl | 53 ----- pkg/scaffold/templates/mcp-tools/go.mod.tmpl | 5 - pkg/scaffold/templates/mcp-tools/main.go.tmpl | 115 ---------- .../templates/quickstart/main.go.tmpl | 3 +- pkg/scaffold/templates/single-agent/.env.tmpl | 4 - .../templates/single-agent/go.mod.tmpl | 7 - .../templates/single-agent/main.go.tmpl | 81 ------- pkg/scaffold/templates/workflow/main.go.tmpl | 90 +++----- 23 files changed, 1399 insertions(+), 675 deletions(-) create mode 100644 cmd/template.go create mode 100644 docs/creating-templates.md create mode 100644 pkg/registry/cache.go create mode 100644 pkg/registry/fetcher.go create mode 100644 pkg/registry/index.go create mode 100644 pkg/registry/manifest.go create mode 100644 pkg/registry/resolver.go create mode 100644 pkg/scaffold/external_generator.go delete mode 100644 pkg/scaffold/templates/mcp-tools/README.md.tmpl delete mode 100644 pkg/scaffold/templates/mcp-tools/go.mod.tmpl delete mode 100644 pkg/scaffold/templates/mcp-tools/main.go.tmpl delete mode 100644 pkg/scaffold/templates/single-agent/.env.tmpl delete mode 100644 pkg/scaffold/templates/single-agent/go.mod.tmpl delete mode 100644 pkg/scaffold/templates/single-agent/main.go.tmpl diff --git a/README.md b/README.md index 7ccfa9d..bd67519 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # AGK - AgenticGoKit CLI -> **Build production-ready agentic AI systems in seconds.** +> **The Unified Toolchain for Agentic AI Systems** -AGK is the official CLI for **AgenticGoKit**, designed to accelerate your development workflow. From scaffolding simple agents to deploying enterprise-grade multi-agent swarms, AGK handles the boilerplate so you can focus on intelligence. +AGK is the official CLI for **AgenticGoKit**, designed to manage the entire lifecycle of intelligent agents. From scaffolding new projects to distributing templates and observing production workflows, AGK provides a standardized interface for building the next generation of AI software. ![Version](https://img.shields.io/badge/version-0.1.0-blue) ![License](https://img.shields.io/badge/license-Apache%202.0-green) @@ -10,31 +10,32 @@ AGK is the official CLI for **AgenticGoKit**, designed to accelerate your develo --- -## πŸš€ Why AGK? +## Vision: The Complete Lifecycle -- **⚑ Instant Scaffolding**: Generate complete, compilable projects with one command. -- **🧠 Smart Defaults**: Auto-detects configuration for OpenAI, Anthropic, and Ollama. -- **οΏ½ Trace Auditor**: Built-in observability to debug agent thoughts and prompts. -- **πŸ“¦ Production Ready**: Docker support, idiomatic Go code, and established patterns. -- **🌊 Streaming Native**: All templates support real-time token streaming out of the box. +AGK aims to streamline the developer experience across four key pillars: + +1. **Create**: Scaffold powerful agents instantly using a rich registry of templates. +2. **Distribute**: (Planned) Share your agent architectures and workflows with the community or your team. +3. **Deploy**: (Planned) Seamlessly ship agents to cloud platforms, Kubernetes, or edge devices. +4. **Trace**: Gain deep observability into your agent's reasoning, prompts, and performance. --- -## 🏁 Quick Start +## Quick Start ### 1. Installation ```bash # Build from source -cd agk-new +cd agk go build -o agk main.go ``` ### 2. Create Your First Agent ```bash -# Initialize a new project with the single-agent template -./agk init my-agent --template single-agent --llm openai +# Initialize a new project with the quickstart template +./agk init my-agent --template quickstart --llm openai # Navigate to the project cd my-agent @@ -55,21 +56,43 @@ go run main.go --- -## πŸ“¦ Templates +## Templates & Registry + +AGK features a powerful template system that lets you use both built-in and community-created templates. Explore the [Official Template Registry](https://github.com/agk-templates). + +### Use a Template +```bash +./agk init enterprise-bot --template workflow --llm anthropic +``` + +### Manage Templates +Bring in templates from GitHub, local folders, or other sources. + +```bash +# List all available templates (built-in + cached) +agk template list + +# Add a template from a remote source +agk template add github.com/username/my-template + +# Remove a cached template +agk template remove my-template +``` + +> **Want to build your own?** Check out the [Creating Templates Guide](docs/creating-templates.md). -Choose the right foundation for your project: +### Built-in Templates -| Template | Complexity | Best For | Description | -|----------|------------|----------|-------------| -| **Quickstart** | ⭐ | Learning | Minimal setup. Single file. Hardcoded config. Perfect for understanding the basics. | -| **Single-Agent** | ⭐⭐ | Prototypes | Adds tools, memory, and environment config. The "standard" starting point. | -| **Multi-Agent** | ⭐⭐⭐ | Workflows | Sequential pipeline of specialized agents (e.g., Researcher β†’ Writer). | -| **Config-Driven** | ⭐⭐⭐⭐ | Enterprise | Factory patterns, TOML config, shared memory. Built for scale. | -| **Advanced** | ⭐⭐⭐⭐⭐ | Production | Full-stack: REST API, WebSocket, Docker, Frontend integration. | +| Template | Best For | Description | +|----------|----------|-------------| +| **Quickstart** | Learning | Minimal setup. Single file. Hardcoded config. Perfect for understanding the basics. | +| **Workflow** | Pipelines | Multi-step workflow (e.g. Sequential, Parallel) structure. | + +Run `agk init --list` to see all available templates including those from the registry. **Example usage:** ```bash -./agk init enterprise-bot --template config-driven --llm anthropic +./agk init enterprise-bot --template workflow --llm anthropic ``` --- @@ -131,23 +154,25 @@ agk trace mermaid > trace_flow.md --- -## πŸ—ΊοΈ Roadmap - -### βœ… Completed -- Template system (Quickstart, Single-Agent) -- Smart LLM Provider detection -- Streaming support -- **Trace Auditor** (Audit & Mermaid commands) -- **Interactive Trace Viewer** (with content inspection) - -### 🚧 In Progress -- Multi-Agent & Enterprise templates -- Advanced full-stack template - -### πŸ“… Planned -- Interactive init wizard (`agk init -i`) -- MCP Server management -- Project upgrade tools +## Roadmap + +### Completed +- **Template Registry System** (`list`, `add`, `remove`) +- **Smart Scaffolding** (Quickstart, Workflow bases) +- **Trace Auditor** (Interactive TUI & Mermaid export) +- **Streaming Support** (Native across all templates) + +### In Progress +- **Multi-Agent Templates** +- **Advanced Full-Stack Templates** + +### Planned +- **Template Distribution** (`pack`, `push`) +- **Cloud Deployment Engine** (`agk deploy`) +- **Workflow Visualization** (Interactive graph editor) +- **Interactive Init Wizard** (`agk init -i`) +- **MCP Server Management** +- **RAG & Knowledge Base Management** --- diff --git a/cmd/init.go b/cmd/init.go index 50688b8..c5abe18 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -12,6 +12,7 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" + "github.com/agenticgokit/agk/pkg/registry" "github.com/agenticgokit/agk/pkg/scaffold" ) @@ -46,11 +47,20 @@ Examples: # Initialize project interactively agk init my-project - # Initialize with specific template - agk init my-project --template simple-agent + # Initialize with built-in template + agk init my-project --template single-agent + + # Initialize from a community template (registry) + agk init my-project --template rag-agent + + # Initialize from a GitHub repository + agk init my-project --template github.com/username/my-template + + # Initialize from a specific version + agk init my-project --template github.com/username/my-template@v1.0.0 # Non-interactive initialization - agk init my-project --template simple-agent --llm openai --agent-type single --force + agk init my-project --template single-agent --llm openai --agent-type single --force # Initialize in specific directory agk init my-project --output ./projects @@ -109,23 +119,57 @@ func runInitCommand(cmd *cobra.Command, args []string) error { return err } - // Validate and get template type - templateType, err := scaffold.ValidateTemplate(initTemplate) - if err != nil { - span.RecordError(err) - span.SetStatus(codes.Error, "invalid template") - color.Red("βœ— %v", err) - return err - } - span.SetAttributes(attribute.String("template_type", string(templateType))) + // Try to get generator (built-in or external) + var generator scaffold.TemplateGenerator + var metadata scaffold.TemplateMetadata + var templateType scaffold.TemplateType + + // First, check if it's a built-in template + builtInType, err := scaffold.ValidateTemplate(initTemplate) + if err == nil { + // It is built-in + templateType = builtInType + span.SetAttributes(attribute.String("template_type", string(templateType))) + + gen, err := scaffold.GetTemplateGenerator(templateType) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, "failed to get generator") + color.Red("βœ— Failed to get template generator: %v", err) + return err + } + generator = gen + metadata = gen.GetMetadata() + } else { + // Not built-in, try resolving as external template + color.Cyan("ℹ️ Template '%s' not found locally, checking registry...", initTemplate) + + cm, err := registry.NewCacheManager("") + if err != nil { + return fmt.Errorf("failed to init cache manager: %w", err) + } + resolver := registry.NewResolver(cm) + + cached, err := resolver.Resolve(ctx, initTemplate) + if err != nil { + // Failed both built-in and external + err = fmt.Errorf("template '%s' not found (neither built-in nor registry): %w", initTemplate, err) + span.RecordError(err) + span.SetStatus(codes.Error, "invalid template") + color.Red("βœ— %v", err) + return err + } - // Get template generator - generator, err := scaffold.GetTemplateGenerator(templateType) - if err != nil { - span.RecordError(err) - span.SetStatus(codes.Error, "failed to get generator") - color.Red("βœ— Failed to get template generator: %v", err) - return err + gen := scaffold.NewExternalGenerator(cached) + generator = gen + metadata = gen.GetMetadata() + templateType = scaffold.TemplateType("external") // Dummy type for next steps + + span.SetAttributes( + attribute.String("template_type", "external"), + attribute.String("external_source", cached.Source), + ) + color.Green("βœ“ Found template '%s' version %s", cached.Name, cached.Version) } // Prepare generation options @@ -141,7 +185,7 @@ func runInitCommand(cmd *cobra.Command, args []string) error { } // Print header with template info - metadata := generator.GetMetadata() + metadata = generator.GetMetadata() color.Cyan("\nπŸ“¦ Creating new AgenticGoKit project: %s\n", projectName) color.Cyan(" Template: %s (%s) - %s\n", metadata.Name, metadata.Complexity, metadata.Description) color.Cyan(" Files: %d | Features: %v\n", metadata.FileCount, metadata.Features) @@ -203,16 +247,6 @@ func listTemplates() { switch tmpl.Name { case "Quickstart": templateID = "quickstart" - case "Single-Agent": - templateID = "single-agent" - case "Multi-Agent": - templateID = "multi-agent" - case "Config-Driven": - templateID = "config-driven" - case "Advanced": - templateID = "advanced" - case "MCP-Tools": - templateID = "mcp-tools" case "Workflow": templateID = "workflow" } @@ -261,24 +295,10 @@ func printNextSteps(_ string, projectPath string, templateType scaffold.Template case scaffold.TemplateQuickstart: fmt.Printf(" β€’ %s\n", color.CyanString("main.go # Entry point with hardcoded agent config")) fmt.Printf(" β€’ %s\n", color.CyanString("go.mod # Go module definition")) - case scaffold.TemplateSingleAgent: - fmt.Printf(" β€’ %s\n", color.CyanString("main.go # Entry point with streaming support")) - fmt.Printf(" β€’ %s\n", color.CyanString(".env # Environment variables (API keys)")) - fmt.Printf(" β€’ %s\n", color.CyanString("go.mod # Go module definition")) - case scaffold.TemplateMCPTools: - fmt.Printf(" β€’ %s\n", color.CyanString("main.go # Agent with MCP server config")) - fmt.Printf(" β€’ %s\n", color.CyanString("README.md # Documentation for MCP tools")) - fmt.Printf(" β€’ %s\n", color.CyanString("go.mod # Go module definition")) case scaffold.TemplateWorkflow: fmt.Printf(" β€’ %s\n", color.CyanString("main.go # Multi-step workflow pipeline")) fmt.Printf(" β€’ %s\n", color.CyanString("README.md # Documentation for workflow")) fmt.Printf(" β€’ %s\n", color.CyanString("go.mod # Go module definition")) - case scaffold.TemplateMultiAgent, scaffold.TemplateConfigDriven, scaffold.TemplateAdvanced: - // Complex templates structure - fmt.Printf(" β€’ %s\n", color.CyanString("main.go # Entry point")) - fmt.Printf(" β€’ %s\n", color.CyanString("config/ # Configuration files")) - fmt.Printf(" β€’ %s\n", color.CyanString("agents/ # Agent definitions")) - fmt.Printf(" β€’ %s\n", color.CyanString("go.mod # Go module definition")) default: // Generic structure for other templates fmt.Printf(" β€’ %s\n", color.CyanString("main.go # Entry point")) @@ -295,24 +315,10 @@ func printNextSteps(_ string, projectPath string, templateType scaffold.Template fmt.Printf(" β€’ Modify the %s to customize the agent behavior\n", color.CyanString("SystemPrompt")) fmt.Printf(" β€’ Try different LLM providers: %s, %s, %s\n", color.CyanString("openai"), color.CyanString("anthropic"), color.CyanString("ollama")) - case scaffold.TemplateSingleAgent: - fmt.Printf(" β€’ Set API keys in %s (copy from %s)\n", color.CyanString(".env"), color.CyanString(".env.example")) - fmt.Printf(" β€’ Configure LLM provider and model in %s\n", color.CyanString("main.go")) - fmt.Printf(" β€’ Add tools/MCP servers to extend agent capabilities\n") - case scaffold.TemplateMCPTools: - fmt.Printf(" β€’ Run %s to initialize MCP servers\n", color.CyanString("npm install")) - fmt.Printf(" β€’ Add more MCP servers in %s\n", color.CyanString("main.go")) - fmt.Printf(" β€’ Use %s to view traces of tool execution\n", color.CyanString("agk trace")) case scaffold.TemplateWorkflow: fmt.Printf(" β€’ Add new step in %s using .AddStep()\n", color.CyanString("main.go")) fmt.Printf(" β€’ Monitor step progress via streaming output\n") fmt.Printf(" β€’ Use %s to debug workflow execution\n", color.CyanString("agk trace")) - case scaffold.TemplateMultiAgent: - fmt.Printf(" β€’ Define agents in %s\n", color.CyanString("agents/")) - fmt.Printf(" β€’ Configure orchestration in %s\n", color.CyanString("main.go")) - case scaffold.TemplateConfigDriven, scaffold.TemplateAdvanced: - fmt.Printf(" β€’ Modify configuration in %s\n", color.CyanString("config/")) - fmt.Printf(" β€’ Extend functionality by adding new modules\n") default: fmt.Printf(" β€’ Configure your LLM provider and API keys\n") fmt.Printf(" β€’ Explore the generated code to understand the structure\n") @@ -328,7 +334,7 @@ func init() { // Define flags initCmd.Flags().BoolVar(&initListTemplates, "list", false, "List available templates") initCmd.Flags().StringVarP(&initTemplate, "template", "t", "quickstart", - "Template type: quickstart, single-agent, multi-agent, config-driven, advanced, mcp-tools, workflow") + "Template type: quickstart, workflow") initCmd.Flags().StringVarP(&initOutputDir, "output", "o", ".", "Output directory for the project") initCmd.Flags().BoolVarP(&initInteractive, "interactive", "i", false, "Enable interactive prompts") initCmd.Flags().BoolVarP(&initForce, "force", "f", false, "Force overwrite existing files") diff --git a/cmd/template.go b/cmd/template.go new file mode 100644 index 0000000..00bf66b --- /dev/null +++ b/cmd/template.go @@ -0,0 +1,107 @@ +package cmd + +import ( + "fmt" + "os" + "text/tabwriter" + + "github.com/agenticgokit/agk/pkg/registry" + "github.com/fatih/color" + "github.com/spf13/cobra" +) + +var templateCmd = &cobra.Command{ + Use: "template", + Short: "Manage project templates", + Long: `Manage local and remote templates for AGK projects.`, +} + +var templateListCmd = &cobra.Command{ + Use: "list", + Short: "List available templates", + RunE: func(cmd *cobra.Command, args []string) error { + cm, err := registry.NewCacheManager("") + if err != nil { + return err + } + + templates, err := cm.List() + if err != nil { + return err + } + + if len(templates) == 0 { + fmt.Println("No templates found in cache. Add one with 'agk template add'.") + return nil + } + + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + _, _ = fmt.Fprintln(w, "NAME\tVERSION\tSOURCE\tDESCRIPTION") + for _, t := range templates { + _, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", t.Name, t.Version, t.Source, t.Description) + } + _ = w.Flush() + return nil + }, +} + +var templateAddCmd = &cobra.Command{ + Use: "add [source]", + Short: "Add a template to the cache", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + source := args[0] + + fmt.Printf("Fetching template from %s...\n", source) + + cm, err := registry.NewCacheManager("") + if err != nil { + return err + } + + resolver := registry.NewResolver(cm) + + // Use context.Background for now + tmpl, err := resolver.Resolve(cmd.Context(), source) + if err != nil { + return err + } + + color.Green("Successfully added template: %s (%s)", tmpl.Name, tmpl.Version) + return nil + }, +} + +var templateRemoveCmd = &cobra.Command{ + Use: "remove [name|source]", + Short: "Remove a template from the cache", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + source := args[0] + + cm, err := registry.NewCacheManager("") + if err != nil { + return err + } + + // Try to remove by exact source match first, then maybe by name? + // CacheManager.Remove takes source. + // If user passes "rag-agent" (name) but source is "github.com/...", Remove might fail. + // TODO: Implement lookup by name in CacheManager to support removing by name. + // For now, assume source. + + if err := cm.Remove(source, ""); err != nil { + return err + } + + color.Green("Removed template: %s", source) + return nil + }, +} + +func init() { + rootCmd.AddCommand(templateCmd) + templateCmd.AddCommand(templateListCmd) + templateCmd.AddCommand(templateAddCmd) + templateCmd.AddCommand(templateRemoveCmd) +} diff --git a/docs/creating-templates.md b/docs/creating-templates.md new file mode 100644 index 0000000..b691399 --- /dev/null +++ b/docs/creating-templates.md @@ -0,0 +1,190 @@ +# Creating AGK Templates + +This guide explains how to create, test, and publish templates for AgenticGoKit (AGK). Templates allow developers to scaffold new projects with pre-configured agents, workflows, and tools. + +## Overview + +An AGK template is simply a directory containing: +1. **Code**: Source files (Go, Markdown, Configs) that structure the project. +2. **`agk-template.toml`**: A manifest file defining metadata, variables, and behavior. + +Templates are processed using Go's `text/template` engine with [Sprig](http://masterminds.github.io/sprig/) functions enabled, allowing for dynamic content generation based on user input. + +--- + +## 1. Template Structure + +A typical template structure looks like this: + +```text +my-template/ +β”œβ”€β”€ agk-template.toml # Manifest (REQUIRED) +β”œβ”€β”€ go.mod.tmpl # Go module file template +β”œβ”€β”€ main.go.tmpl # Main application code template +β”œβ”€β”€ README.md # Template documentation +β”œβ”€β”€ .gitignore # Standard gitignore +└── pkg/ # Subfolders are fully supported + └── helper.go.tmpl # Will be rendered to /pkg/helper.go +``` + +### File Naming +- Files ending in `.tmpl` will be processed as templates. The `.tmpl` extension is removed in the generated project (e.g., `main.go.tmpl` becomes `main.go`). +- Files *without* `.tmpl` are copied as-is, unless they are binary files or excluded by the manifest. +- You can use template variables in filenames (feature coming soon). + +--- + +## 2. Configuration (`agk-template.toml`) + +The `agk-template.toml` file is the heart of your template. It defines how `agk init` interacts with your template. + +### Full Schema Example + +```toml +[template] +name = "my-custom-agent" +version = "1.0.0" +description = "A specialized agent for data analysis" +author = "Jane Doe" +license = "MIT" +min_agk_version = "0.1.0" + +# Define variables to ask the user during initialization +[template.variables] + [template.variables.agent_name] + type = "string" + description = "Name of your agent" + required = true + default = "MyAgent" + + [template.variables.include_memory] + type = "bool" + description = "Enable vector memory?" + default = true + + [template.variables.model_type] + type = "choice" + description = "Primary model to use" + options = ["gpt-4o", "claude-3-5-sonnet"] + default = "gpt-4o" + +# Control which files are copied +[template.files] +include = ["**/*"] +exclude = [".git", "agk-template.toml", "test-data/**"] + +# Commands to run after generation +[template.hooks] +post_create = ["go mod tidy", "go fmt ./..."] +``` + +### Variable Types +- **`string`**: simple text input. +- **`bool`**: Yes/No prompt. +- **`choice`**: Selection from a list of `options`. + +--- + +## 3. Writing Template Files + +Use standard Go templating syntax (`{{ }}`) in your files. + +### Available Data +The following data is available to your templates: + +| Variable | Description | +|----------|-------------| +| `.ProjectName` | The name of the project folder (e.g., "my-bot") | +| `.Description` | Project description provided by CLI flag | +| `.LLMProvider` | Selected provider (openai, anthropic, ollama) | +| `.LLMModel` | Default model for the provider | +| `.WorkflowName` | Name of the workflow (for workflow templates) | +| `.APIKeyEnv` | Env var name for the key (e.g., `OPENAI_API_KEY`) | +| `.AgentType` | Agent type string | +| `.Variables` | (Planning) Access custom variables defined in TOML | + +### Example: `main.go.tmpl` + +```go +package main + +import ( + "log" + "os" + "github.com/agenticgokit/agenticgokit" +) + +func main() { + // Project: {{ .ProjectName }} + // Generated by AGK + + config := agenticgokit.Config{ + Provider: "{{ .LLMProvider }}", // e.g. openai + Model: "{{ .LLMModel }}", // e.g. gpt-4o + } + + // ... +} +``` + +### Scripting with Sprig +You can use Sprig functions for complex logic: +```go +{{ if eq .LLMProvider "ollama" }} + // Ollama specific setup +{{ end }} + +// Helper: {{ .ProjectName | title }} +``` + +--- + +## 4. Developing Locally + +You can test your template locally without publishing it. + +### Step 1: Create your template folder +```bash +mkdir my-template +cd my-template +# ... create files and agk-template.toml ... +``` + +### Step 2: Add to local registry +Use `agk template add` to point AGK to your local folder. +```bash +# Add current directory as a template +agk template add . + +# Or specify full path +agk template add /absolute/path/to/my-template +``` + +### Step 3: Test generation +Try to initialize a project using your template. +```bash +cd /tmp +agk init test-project --template my-custom-agent +``` +*Note: The `--template` name must match the `name` field in your `agk-template.toml`.* + +### Step 4: Iterate +Make changes to your template files. You typically don't need to re-add the template if you pointed to a local path, but if you cached it, you might need to run `add` again to update the cache. + +--- + +## 5. Publishing + +To share your template with the community: + +1. **Host on GitHub**: Push your template to a public GitHub repository. +2. **Verify Fetch**: Test that AGK can fetch it directly from the URL. + ```bash + agk template add github.com/username/my-template + ``` +3. **Submit to Registry**: + - Fork the [agk-templates/registry](https://github.com/agk-templates/registry) repository. + - Add your template metadata to `index.json`. + - Submit a Pull Request. + +Once accepted, users will be able to see your template in `agk init --list` or use it by name! diff --git a/go.mod b/go.mod index bd93d9e..955930f 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,14 @@ module github.com/agenticgokit/agk go 1.24.1 require ( + github.com/BurntSushi/toml v1.5.0 + github.com/Masterminds/sprig/v3 v3.3.0 github.com/agenticgokit/agenticgokit v0.5.3 github.com/charmbracelet/bubbles v0.21.0 github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/lipgloss v1.1.0 github.com/fatih/color v1.14.1 + github.com/go-git/go-git/v5 v5.16.4 github.com/rs/zerolog v1.34.0 github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.18.0 @@ -15,39 +18,60 @@ require ( ) require ( + dario.cat/mergo v1.0.1 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.3.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.1.6 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/cenkalti/backoff/v5 v5.0.2 // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/x/ansi v0.10.1 // indirect github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/cloudflare/circl v1.6.1 // indirect + github.com/cyphar/filepath-securejoin v0.4.1 // indirect + github.com/emirpasic/gods v1.18.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.6.2 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/pelletier/go-toml/v2 v2.1.1 // indirect + github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/skeema/knownhosts v1.3.1 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/cast v1.7.0 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect @@ -58,7 +82,8 @@ require ( go.opentelemetry.io/otel/trace v1.37.0 // indirect go.opentelemetry.io/proto/otlp v1.7.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect + golang.org/x/crypto v0.39.0 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/net v0.41.0 // indirect golang.org/x/sys v0.36.0 // indirect golang.org/x/text v0.26.0 // indirect @@ -67,5 +92,6 @@ require ( google.golang.org/grpc v1.73.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 841f2a7..f256da7 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,24 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/agenticgokit/agenticgokit v0.5.3 h1:k9/oSwxJbpCVQCDNPY9yWaXkiCNRsttn2DLQbwcQvXY= github.com/agenticgokit/agenticgokit v0.5.3/go.mod h1:0EwU951CZIGYwEOLnC5hJbC9lhNvM85FhrL6NTTDIZo= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= @@ -18,12 +37,20 @@ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0G github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= @@ -32,12 +59,24 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y= +github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -48,10 +87,19 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+u github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= @@ -68,16 +116,25 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= @@ -85,8 +142,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= @@ -95,12 +152,19 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= @@ -110,6 +174,8 @@ github.com/spf13/viper v1.18.0/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMV github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -117,6 +183,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= @@ -143,18 +211,33 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No= -golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= @@ -164,10 +247,15 @@ google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7E google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/registry/cache.go b/pkg/registry/cache.go new file mode 100644 index 0000000..9e718ca --- /dev/null +++ b/pkg/registry/cache.go @@ -0,0 +1,156 @@ +package registry + +import ( + "fmt" + "os" + "path/filepath" + "sort" + "strings" +) + +const ( + VersionLatest = "latest" +) + +// CachedTemplate represents a template stored in the local cache. +type CachedTemplate struct { + Name string // Template name from manifest (e.g., "rag-agent") + Source string // Source URL/Path (e.g., "github.com/user/repo") + Version string // Version tag or "latest" + Description string // Description from manifest + LocalPath string // Absolute path to the template in cache + Manifest *TemplateManifest // Parsed manifest +} + +// CacheManager handles local storage of templates. +type CacheManager struct { + BaseDir string // Root cache directory (e.g., ~/.agk/templates) +} + +// NewCacheManager creates a new cache manager. +// If baseDir is empty, it defaults to ~/.agk/templates. +func NewCacheManager(baseDir string) (*CacheManager, error) { + if baseDir == "" { + home, err := os.UserHomeDir() + if err != nil { + return nil, fmt.Errorf("failed to get user home directory: %w", err) + } + baseDir = filepath.Join(home, ".agk", "templates") + } + + if err := os.MkdirAll(baseDir, 0750); err != nil { + return nil, fmt.Errorf("failed to create cache directory %s: %w", baseDir, err) + } + + return &CacheManager{BaseDir: baseDir}, nil +} + +// GetPath returns the expected local path for a given source and version. +// Source should be a clean URL path like "github.com/user/repo". +// If version is empty, it uses "latest". +func (c *CacheManager) GetPath(source, version string) string { + if version == "" { + version = VersionLatest + } + // Clean source to avoid path traversal + source = filepath.Clean(source) + source = strings.TrimPrefix(source, "/") + source = strings.TrimPrefix(source, "\\") + + // Example: ~/.agk/templates/github.com/user/repo/v1.0.0 + return filepath.Join(c.BaseDir, source, version) +} + +// List returns all templates currently in the cache. +func (c *CacheManager) List() ([]CachedTemplate, error) { + var templates []CachedTemplate + + // Walk the cache directory to find agk-template.toml files + // We expect structure: BaseDir/HOST/jUSER/REPO/VERSION/agk-template.toml + err := filepath.Walk(c.BaseDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + // Skip dot directories like .git + if strings.HasPrefix(info.Name(), ".") && info.Name() != "." { + return filepath.SkipDir + } + return nil + } + + if info.Name() == "agk-template.toml" { + // Found a manifest, parse it + manifest, err := ParseManifest(path) + if err != nil { + // Log error but continue? For now just skip + return nil + } + + // Determine source and version from path relative to BaseDir + relPath, err := filepath.Rel(c.BaseDir, filepath.Dir(path)) + if err != nil { + return nil + } + + // Expected relPath: source/version + // We take the parent of directory as source, and directory name as version + version := filepath.Base(relPath) + source := filepath.Dir(relPath) + + templates = append(templates, CachedTemplate{ + Name: manifest.Template.Name, + Source: filepath.ToSlash(source), + Version: version, + Description: manifest.Template.Description, + LocalPath: filepath.Dir(path), + Manifest: manifest, + }) + } + return nil + }) + + if err != nil { + return nil, fmt.Errorf("failed to walk cache directory: %w", err) + } + + // Sort by name + sort.Slice(templates, func(i, j int) bool { + return templates[i].Name < templates[j].Name + }) + + return templates, nil +} + +// Remove deletes a template from the cache. +// Source should include the domain, e.g., "github.com/user/repo". +// If version is provided, only that version is removed. +// If version is empty, ALL versions of that template are removed. +func (c *CacheManager) Remove(source, version string) error { + path := c.GetPath(source, version) + + if version == "" { + // Remove all versions: ~/.agk/templates/github.com/user/repo + // Note: GetPath appends /latest if version is empty, so we need to grab Dir of that + path = filepath.Dir(path) + } + + if _, err := os.Stat(path); os.IsNotExist(err) { + return fmt.Errorf("template not found: %s", path) + } + + if err := os.RemoveAll(path); err != nil { + return fmt.Errorf("failed to remove template %s: %w", path, err) + } + + return nil +} + +// Clear removes all cached templates. +func (c *CacheManager) Clear() error { + if err := os.RemoveAll(c.BaseDir); err != nil { + return fmt.Errorf("failed to clear cache: %w", err) + } + return os.MkdirAll(c.BaseDir, 0750) +} diff --git a/pkg/registry/fetcher.go b/pkg/registry/fetcher.go new file mode 100644 index 0000000..70d6374 --- /dev/null +++ b/pkg/registry/fetcher.go @@ -0,0 +1,144 @@ +package registry + +import ( + "context" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" +) + +// Fetcher defines the interface for fetching templates. +type Fetcher interface { + // Fetch downloads the template from source/version to dest directory. + Fetch(ctx context.Context, source, version, dest string) error +} + +// GitFetcher downloads templates from Git repositories. +type GitFetcher struct{} + +// Fetch implements Fetcher for Git repositories. +// It supports cloning specific tags or the latest default branch. +func (f *GitFetcher) Fetch(ctx context.Context, source, version, dest string) error { + // Ensure destination directory doesn't exist to avoid git clone errors + if err := os.RemoveAll(dest); err != nil { + return fmt.Errorf("failed to clear destination: %w", err) + } + + // Construct Git URL + // Simple heuristic: if it looks like github.com/user/repo, add https:// + url := source + if !strings.Contains(url, "://") && !strings.HasPrefix(url, "git@") { + url = "https://" + url + } + + cloneOpts := &git.CloneOptions{ + URL: url, + Progress: os.Stdout, // Should ideally be controlled by logger/context + Depth: 1, // Default to shallow clone + Tags: git.NoTags, + } + + // If version is specified and not "latest", try to checkout that tag + if version != "" && version != "latest" { + cloneOpts.ReferenceName = plumbing.ReferenceName("refs/tags/" + version) + cloneOpts.SingleBranch = true + cloneOpts.Depth = 1 // Shallow clone of tag is supported + } + + // Perform clone + _, err := git.PlainCloneContext(ctx, dest, false, cloneOpts) + if err != nil { + // Fallback: If tag checkout failed, maybe try full clone then checkout? + // But for now return error. + return fmt.Errorf("git clone failed for %s@%s: %w", url, version, err) + } + + // Cleanup .git directory as we don't need history in the template cache + if err := os.RemoveAll(filepath.Join(dest, ".git")); err != nil { + return fmt.Errorf("failed to remove .git directory: %w", err) + } + + return nil +} + +// LocalFetcher copies templates from a local path. +type LocalFetcher struct{} + +// Fetch implements Fetcher for local paths. +// Source is assumed to be an absolute or relative file path. +// Version is ignored for local paths. +func (f *LocalFetcher) Fetch(ctx context.Context, source, version, dest string) error { + // Ensure source exists + info, err := os.Stat(source) + if err != nil { + return fmt.Errorf("local source not found: %w", err) + } + if !info.IsDir() { + return fmt.Errorf("local source %s is not a directory", source) + } + + // Ensure destination directory doesn't exist + if err := os.RemoveAll(dest); err != nil { + return fmt.Errorf("failed to clear destination: %w", err) + } + + // Copy directory + return copyDir(source, dest) +} + +// copyDir recursively copies a directory tree, attempting to preserve permissions. +func copyDir(src string, dst string) error { + src = filepath.Clean(src) + dst = filepath.Clean(dst) + + err := filepath.Walk(src, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // Construct relative path + relPath, err := filepath.Rel(src, path) + if err != nil { + return err + } + dstPath := filepath.Join(dst, relPath) + + if info.IsDir() { + // Create directory + return os.MkdirAll(dstPath, info.Mode()) + } + + // Copy file + if err := copyFile(path, dstPath, info.Mode()); err != nil { + return fmt.Errorf("failed to copy file %s: %w", path, err) + } + return nil + }) + + return err +} + +func copyFile(src, dst string, mode os.FileMode) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer func() { _ = in.Close() }() + + out, err := os.Create(dst) + if err != nil { + return err + } + defer func() { _ = out.Close() }() + + if _, err := io.Copy(out, in); err != nil { + return err + } + + return os.Chmod(dst, mode) +} diff --git a/pkg/registry/index.go b/pkg/registry/index.go new file mode 100644 index 0000000..133c968 --- /dev/null +++ b/pkg/registry/index.go @@ -0,0 +1,43 @@ +package registry + +import ( + "encoding/json" + "fmt" + "net/http" + "time" +) + +const ( + // DefaultRegistryURL is the URL of the official AGK template registry. + // For production we'd point to the official repo. + DefaultRegistryURL = "https://raw.githubusercontent.com/agk-templates/registry/main/index.json" +) + +// RegistryIndex represents the structure of the registry index.json file. +type RegistryIndex struct { + Templates map[string]string `json:"templates"` +} + +// FetchIndex fetches and parses the registry index from the given URL. +func FetchIndex(url string) (*RegistryIndex, error) { + client := &http.Client{ + Timeout: 10 * time.Second, + } + + resp, err := client.Get(url) + if err != nil { + return nil, fmt.Errorf("failed to fetch registry index: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("registry returned status: %s", resp.Status) + } + + var index RegistryIndex + if err := json.NewDecoder(resp.Body).Decode(&index); err != nil { + return nil, fmt.Errorf("failed to decode registry index: %w", err) + } + + return &index, nil +} diff --git a/pkg/registry/manifest.go b/pkg/registry/manifest.go new file mode 100644 index 0000000..922ffbf --- /dev/null +++ b/pkg/registry/manifest.go @@ -0,0 +1,146 @@ +// Package registry provides template registry functionality for AGK. +// It handles fetching, caching, and validating templates from various sources +// including GitHub repositories, local paths, and the official AGK registry. +package registry + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/BurntSushi/toml" +) + +// TemplateManifest represents the agk-template.toml file structure. +// This is the main configuration file for AGK templates. +type TemplateManifest struct { + Template TemplateInfo `toml:"template"` +} + +// TemplateInfo contains metadata and configuration for a template. +type TemplateInfo struct { + // Basic metadata + Name string `toml:"name"` + Version string `toml:"version"` + Description string `toml:"description"` + Author string `toml:"author"` + License string `toml:"license"` + + // Compatibility + MinAGKVersion string `toml:"min_agk_version"` + + // Template variables that users can customize + Variables map[string]Variable `toml:"variables"` + + // File inclusion/exclusion rules + Files FileConfig `toml:"files"` + + // Hooks to run after template generation + Hooks HookConfig `toml:"hooks"` + + // Dependencies required by generated project + Dependencies map[string]string `toml:"dependencies"` +} + +// Variable defines a template variable that can be customized during init. +type Variable struct { + Type string `toml:"type"` // "string", "bool", "choice" + Description string `toml:"description"` + Required bool `toml:"required"` + Default any `toml:"default"` + Options []string `toml:"options"` // For "choice" type +} + +// FileConfig specifies which files to include/exclude from the template. +type FileConfig struct { + Include []string `toml:"include"` // Glob patterns to include + Exclude []string `toml:"exclude"` // Glob patterns to exclude +} + +// HookConfig defines commands to run after template generation. +type HookConfig struct { + PostCreate []string `toml:"post_create"` // Commands like "go mod tidy" +} + +// ParseManifest reads and parses an agk-template.toml file. +func ParseManifest(path string) (*TemplateManifest, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read manifest: %w", err) + } + + return ParseManifestData(data) +} + +// ParseManifestData parses manifest content from bytes. +func ParseManifestData(data []byte) (*TemplateManifest, error) { + var manifest TemplateManifest + if err := toml.Unmarshal(data, &manifest); err != nil { + return nil, fmt.Errorf("failed to parse manifest: %w", err) + } + + return &manifest, nil +} + +// FindManifest searches for agk-template.toml in the given directory. +func FindManifest(dir string) (string, error) { + manifestPath := filepath.Join(dir, "agk-template.toml") + if _, err := os.Stat(manifestPath); err != nil { + if os.IsNotExist(err) { + return "", fmt.Errorf("no agk-template.toml found in %s", dir) + } + return "", fmt.Errorf("failed to check manifest: %w", err) + } + return manifestPath, nil +} + +// Validate checks if the manifest is valid. +func (m *TemplateManifest) Validate() error { + if m.Template.Name == "" { + return fmt.Errorf("template name is required") + } + if m.Template.Version == "" { + return fmt.Errorf("template version is required") + } + + // Validate variables + for name, v := range m.Template.Variables { + if err := validateVariable(name, v); err != nil { + return err + } + } + + return nil +} + +// validateVariable checks if a variable definition is valid. +func validateVariable(name string, v Variable) error { + validTypes := map[string]bool{ + "string": true, + "bool": true, + "choice": true, + } + + if !validTypes[v.Type] { + return fmt.Errorf("variable %q has invalid type %q (must be string, bool, or choice)", name, v.Type) + } + + if v.Type == "choice" && len(v.Options) == 0 { + return fmt.Errorf("variable %q is type 'choice' but has no options", name) + } + + return nil +} + +// GetVariable returns a variable by name, or nil if not found. +func (m *TemplateManifest) GetVariable(name string) *Variable { + if v, ok := m.Template.Variables[name]; ok { + return &v + } + return nil +} + +// HasHooks returns true if the manifest defines any hooks. +func (m *TemplateManifest) HasHooks() bool { + return len(m.Template.Hooks.PostCreate) > 0 +} diff --git a/pkg/registry/resolver.go b/pkg/registry/resolver.go new file mode 100644 index 0000000..78d3175 --- /dev/null +++ b/pkg/registry/resolver.go @@ -0,0 +1,157 @@ +package registry + +import ( + "context" + "fmt" + "path/filepath" + "strings" +) + +const ( + FetcherTypeGit = "git" + FetcherTypeLocal = "local" +) + +// Resolver handles resolving template references to cached templates. +// It orchestrates fetching and caching. +type Resolver struct { + cache *CacheManager + fetchers map[string]Fetcher // "git", "local" +} + +// NewResolver creates a new template resolver. +func NewResolver(cache *CacheManager) *Resolver { + return &Resolver{ + cache: cache, + fetchers: map[string]Fetcher{ + FetcherTypeGit: &GitFetcher{}, + FetcherTypeLocal: &LocalFetcher{}, + }, + } +} + +// Resolve locates a template, fetching it if necessary, and returns the cached template. +// Source can be: +// - GitHub URL: github.com/user/repo or https://github.com/user/repo +// - Versioned: github.com/user/repo@v1.0.0 +// - Local path: ./my-template or /abs/path/to/template +// Resolve locates a template, fetching it if necessary, and returns the cached template. +// Source can be: +// - GitHub URL: github.com/user/repo or https://github.com/user/repo +// - Versioned: github.com/user/repo@v1.0.0 +// - Local path: ./my-template or /abs/path/to/template +func (r *Resolver) Resolve(ctx context.Context, sourceRef string) (*CachedTemplate, error) { + source, version := parseSourceRef(sourceRef) + isLocal := isLocalPath(source) + + fetcherType, resolvedSource, err := r.resolveFetcherType(source, isLocal) + if err != nil { + return nil, err + } + source = resolvedSource + + // Determine cache path + cacheKey := source + if isLocal { + cacheKey = "local/" + filepath.Base(source) + } + + destPath := r.cache.GetPath(cacheKey, version) + + // Check if exists in cache + if _, err := ParseManifest(filepath.Join(destPath, "agk-template.toml")); err == nil { + return r.loadFromCache(destPath, cacheKey, version) + } + + // Fetch it + fetcher, ok := r.fetchers[fetcherType] + if !ok { + return nil, fmt.Errorf("no fetcher for type %s", fetcherType) + } + + if err := fetcher.Fetch(ctx, source, version, destPath); err != nil { + return nil, fmt.Errorf("failed to fetch template: %w", err) + } + + return r.loadFromCache(destPath, cacheKey, version) +} + +func (r *Resolver) resolveFetcherType(source string, isLocal bool) (string, string, error) { + if isLocal { + absPath, err := filepath.Abs(source) + if err == nil { + source = absPath + } + return FetcherTypeLocal, source, nil + } + + // Check if valid URL or git source + if strings.Contains(source, "://") || strings.HasPrefix(source, "git@") || strings.Contains(source, "github.com") { + return FetcherTypeGit, source, nil + } + + // Try registry lookup + return r.resolveFromRegistry(source) +} + +func (r *Resolver) resolveFromRegistry(source string) (string, string, error) { + index, err := FetchIndex(DefaultRegistryURL) + if err != nil { + return "", "", fmt.Errorf("failed to fetch registry index to resolve '%s': %w", source, err) + } + + if repoURL, ok := index.Templates[source]; ok { + return FetcherTypeGit, repoURL, nil + } + + if strings.HasPrefix(source, "agk/") { + stripped := strings.TrimPrefix(source, "agk/") + if repoURL, ok := index.Templates[stripped]; ok { + return FetcherTypeGit, repoURL, nil + } + return "", "", fmt.Errorf("template '%s' (nor '%s') not found in registry", source, stripped) + } + + return "", "", fmt.Errorf("template '%s' not found in registry and is not a valid URL or local path", source) +} + +func (r *Resolver) loadFromCache(path, source, version string) (*CachedTemplate, error) { + manifest, err := ParseManifest(filepath.Join(path, "agk-template.toml")) + if err != nil { + return nil, fmt.Errorf("invalid template (missing or invalid agk-template.toml): %w", err) + } + + return &CachedTemplate{ + Name: manifest.Template.Name, + Source: source, + Version: version, + Description: manifest.Template.Description, + LocalPath: path, + Manifest: manifest, + }, nil +} + +// parseSourceRef splits "source@version" into "source" and "version" +func parseSourceRef(ref string) (string, string) { + parts := strings.Split(ref, "@") + if len(parts) > 1 { + // Handle cases like "git@github.com:..." where @ is part of auth + // If using https/github.com style, last @ is version + lastIdx := strings.LastIndex(ref, "@") + if lastIdx > 0 { + // Check if it looks like git user? git@... + // If contains / and @ is before /, it's auth. + // Version @ is usually at the end. + return ref[:lastIdx], ref[lastIdx+1:] + } + } + return ref, VersionLatest +} + +func isLocalPath(s string) bool { + return strings.HasPrefix(s, ".") || + strings.HasPrefix(s, "/") || + strings.HasPrefix(s, "\\") || + filepath.IsAbs(s) || + strings.Contains(s, string(filepath.Separator)) && !strings.Contains(s, "://") && !strings.HasPrefix(s, "github.com") +} diff --git a/pkg/scaffold/external_generator.go b/pkg/scaffold/external_generator.go new file mode 100644 index 0000000..69ea270 --- /dev/null +++ b/pkg/scaffold/external_generator.go @@ -0,0 +1,140 @@ +package scaffold + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + "text/template" + + "github.com/Masterminds/sprig/v3" + "github.com/agenticgokit/agk/pkg/registry" +) + +// ExternalGenerator generates a project from a cached external template +type ExternalGenerator struct { + Cached *registry.CachedTemplate +} + +func NewExternalGenerator(cached *registry.CachedTemplate) *ExternalGenerator { + return &ExternalGenerator{Cached: cached} +} + +func (g *ExternalGenerator) GetMetadata() TemplateMetadata { + return TemplateMetadata{ + Name: g.Cached.Name, + Description: g.Cached.Description, + Complexity: "External", // Could come from manifest + FileCount: 0, // Could calculate this + Features: []string{"External Template", g.Cached.Version}, + } +} + +func (g *ExternalGenerator) Generate(ctx context.Context, opts GenerateOptions) error { + // Create project directory + if err := os.MkdirAll(opts.ProjectPath, 0750); err != nil { + return fmt.Errorf("failed to create project directory: %w", err) + } + + manifest := g.Cached.Manifest + srcDir := g.Cached.LocalPath + + // Prepare template data + data := TemplateData{ + ProjectName: opts.ProjectName, + LLMModel: getLLMModel(opts.LLMProvider), + LLMProvider: opts.LLMProvider, + Description: opts.Description, + AgentType: opts.AgentType, + APIKeyEnv: getAPIKeyEnv(opts.LLMProvider), + } + + // Walk through the template directory + err := filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // Calculate relative path + relPath, err := filepath.Rel(srcDir, path) + if err != nil { + return err + } + + if info.IsDir() { + // Skip .git and ignored directories + if strings.HasPrefix(info.Name(), ".") && info.Name() != "." { + return filepath.SkipDir + } + + // Create directory in destination + destPath := filepath.Join(opts.ProjectPath, relPath) + return os.MkdirAll(destPath, 0750) + } + + // Skip manifest file + if info.Name() == "agk-template.toml" { + return nil + } + + // Skip excluded files (simple check for now) + if shouldExclude(relPath, manifest.Template.Files.Exclude) { + return nil + } + + // Use text/template if it's a .tmpl file or generally text? + // Usually external templates might just be normal files we treat as templates + // OR they explicitly have .tmpl extension. + // For simplicity/power, let's try to render ALL non-binary files. + // Or stick to .tmpl convention? + // Most "cookiecutter" style tools render everything. + + destPath := filepath.Join(opts.ProjectPath, relPath) + // Remove .tmpl extension if present + destPath = strings.TrimSuffix(destPath, ".tmpl") + + // Read file content + content, err := os.ReadFile(path) + if err != nil { + return err + } + + // Attempt to render + rendered, err := renderContent(string(content), data) + if err != nil { + // If render fails (e.g. binary file), just copy original + // Ideally check for binary before rendering + return os.WriteFile(destPath, content, info.Mode()) + } + + return os.WriteFile(destPath, []byte(rendered), info.Mode()) + }) + + return err +} + +func renderContent(content string, data TemplateData) (string, error) { + // Create template with Sprig functions + tmpl, err := template.New("external").Funcs(sprig.TxtFuncMap()).Parse(content) + if err != nil { + return "", err + } + + var buf strings.Builder + if err := tmpl.Execute(&buf, data); err != nil { + return "", err + } + + return buf.String(), nil +} + +func shouldExclude(path string, patterns []string) bool { + for _, p := range patterns { + matched, _ := filepath.Match(p, path) + if matched { + return true + } + } + return false +} diff --git a/pkg/scaffold/service.go b/pkg/scaffold/service.go index bed58e5..15c889b 100644 --- a/pkg/scaffold/service.go +++ b/pkg/scaffold/service.go @@ -48,18 +48,12 @@ func (s *Service) GenerateProject(ctx context.Context, opts GenerateOptions) err switch opts.Template { case "quickstart": templateType = TemplateQuickstart - case "single-agent": - templateType = TemplateSingleAgent - case "multi-agent": - templateType = TemplateMultiAgent - case "mcp-tools": - templateType = TemplateMCPTools case "workflow": templateType = TemplateWorkflow default: - // Default to single-agent if not specified or unknown + // Default to quickstart if not specified or unknown if opts.Template == "" { - templateType = TemplateSingleAgent + templateType = TemplateQuickstart } else { // Try to match string to type, otherwise error templateType = TemplateType(opts.Template) diff --git a/pkg/scaffold/template.go b/pkg/scaffold/template.go index 6e2cd13..4e6badc 100644 --- a/pkg/scaffold/template.go +++ b/pkg/scaffold/template.go @@ -11,13 +11,8 @@ type TemplateType string // Template type constants const ( - TemplateQuickstart TemplateType = "quickstart" - TemplateSingleAgent TemplateType = "single-agent" - TemplateMultiAgent TemplateType = "multi-agent" - TemplateConfigDriven TemplateType = "config-driven" - TemplateAdvanced TemplateType = "advanced" - TemplateMCPTools TemplateType = "mcp-tools" - TemplateWorkflow TemplateType = "workflow" + TemplateQuickstart TemplateType = "quickstart" + TemplateWorkflow TemplateType = "workflow" ) // TemplateMetadata contains information about a template @@ -41,20 +36,15 @@ type TemplateGenerator interface { // ValidateTemplate validates and returns a TemplateType from a string func ValidateTemplate(templateStr string) (TemplateType, error) { validTemplates := map[string]TemplateType{ - "quickstart": TemplateQuickstart, - "single-agent": TemplateSingleAgent, - "multi-agent": TemplateMultiAgent, - "config-driven": TemplateConfigDriven, - "advanced": TemplateAdvanced, - "mcp-tools": TemplateMCPTools, - "workflow": TemplateWorkflow, + "quickstart": TemplateQuickstart, + "workflow": TemplateWorkflow, } if tt, ok := validTemplates[templateStr]; ok { return tt, nil } - return "", fmt.Errorf("invalid template '%s'. Valid options: quickstart, single-agent, multi-agent, config-driven, advanced, mcp-tools, workflow", templateStr) + return "", fmt.Errorf("invalid template '%s'. Valid options: quickstart, workflow", templateStr) } // GetAllTemplates returns all available templates @@ -67,41 +57,6 @@ func GetAllTemplates() []TemplateMetadata { FileCount: 2, Features: []string{"Agent", "Hardcoded Config"}, }, - { - Name: "Single-Agent", - Description: "Single agent with tools and memory", - Complexity: "⭐⭐", - FileCount: 5, - Features: []string{"Agent", "Tools/MCP", "Memory", ".env Config"}, - }, - { - Name: "Multi-Agent", - Description: "Multiple agents with workflow pipeline", - Complexity: "⭐⭐⭐", - FileCount: 8, - Features: []string{"Agents", "Workflow", "Sequential Pipeline", ".env Config"}, - }, - { - Name: "Config-Driven", - Description: "Enterprise setup with TOML configuration", - Complexity: "⭐⭐⭐⭐", - FileCount: 12, - Features: []string{"Agents", "Workflow", "Factory Pattern", "TOML Config", "Memory"}, - }, - { - Name: "Advanced", - Description: "Full-stack with server, frontend, and Docker", - Complexity: "⭐⭐⭐⭐⭐", - FileCount: 20, - Features: []string{"Agents", "Workflow", "Server", "Frontend", "WebSocket", "Docker", "TOML Config"}, - }, - { - Name: "MCP-Tools", - Description: "Agent with MCP server tool integration", - Complexity: "⭐⭐", - FileCount: 3, - Features: []string{"Agent", "MCP Tools", "Streaming", "Observability"}, - }, { Name: "Workflow", Description: "Multi-step streaming workflow pipeline", diff --git a/pkg/scaffold/template_registry.go b/pkg/scaffold/template_registry.go index 5c7cbe7..e0ac119 100644 --- a/pkg/scaffold/template_registry.go +++ b/pkg/scaffold/template_registry.go @@ -8,7 +8,7 @@ import ( ) const ( - DefaultGpt4Turbo = "gpt-4-turbo" + DefaultOpenAIModel = "gpt-4o" ProviderAnthropic = "anthropic" ProviderOllama = "ollama" @@ -22,26 +22,13 @@ func GetTemplateGenerator(templateType TemplateType) (TemplateGenerator, error) case TemplateQuickstart: return NewQuickstartGenerator(), nil - case TemplateSingleAgent: - return NewSingleAgentGenerator(), nil - - case TemplateMultiAgent: - return NewMultiAgentGenerator(), nil - - case TemplateConfigDriven: - return NewConfigDrivenGenerator(), nil - - case TemplateAdvanced: - return NewAdvancedGenerator(), nil - - case TemplateMCPTools: - return NewMCPToolsGenerator(), nil - case TemplateWorkflow: return NewWorkflowGenerator(), nil default: - return nil, fmt.Errorf("unknown template type: %s", templateType) + // Attempt to fallback to registry/external generator if not built-in? + // But GetTemplateGenerator is for built-ins usually. + return nil, fmt.Errorf("unknown built-in template type: %s", templateType) } } @@ -96,7 +83,7 @@ func (g *QuickstartGenerator) Generate(ctx context.Context, opts GenerateOptions // Prepare template data data := TemplateData{ ProjectName: opts.ProjectName, - LLMModel: "gpt-4o-mini", // Default for quickstart + LLMModel: getLLMModel(opts.LLMProvider), // Dynamic model selection LLMProvider: opts.LLMProvider, Description: opts.Description, AgentType: opts.AgentType, @@ -129,149 +116,30 @@ func (g *QuickstartGenerator) Generate(ctx context.Context, opts GenerateOptions // ===== GENERATORS ===== -// SingleAgentGenerator generates a single-agent template -type SingleAgentGenerator struct{} - -func NewSingleAgentGenerator() *SingleAgentGenerator { - return &SingleAgentGenerator{} -} - -func (g *SingleAgentGenerator) GetMetadata() TemplateMetadata { - return TemplateMetadata{ - Name: "Single-Agent", - Description: "Single agent with tools and memory", - Complexity: "⭐⭐", - FileCount: 5, - Features: []string{"Agent", "Tools/MCP", "Memory", ".env Config"}, - } -} - -func (g *SingleAgentGenerator) Generate(ctx context.Context, opts GenerateOptions) error { - // Create project directory - if err := os.MkdirAll(opts.ProjectPath, 0750); err != nil { - return fmt.Errorf("failed to create project directory: %w", err) - } - - // Determine LLM model based on provider - llmModel := DefaultGpt4Turbo - if opts.LLMProvider == ProviderAnthropic { - llmModel = "claude-3-sonnet-20240229" - } else if opts.LLMProvider == ProviderOllama { - llmModel = "llama3.2" - } - - // Prepare template data - data := TemplateData{ - ProjectName: opts.ProjectName, - LLMModel: llmModel, - LLMProvider: opts.LLMProvider, - Description: opts.Description, - AgentType: opts.AgentType, - } - - // Files to generate: go.mod, main.go, .env - files := map[string]string{ - "go.mod": "templates/single-agent/go.mod.tmpl", - "main.go": "templates/single-agent/main.go.tmpl", - ".env": "templates/single-agent/.env.tmpl", - } - - for fileName, templatePath := range files { - content, err := RenderTemplate(templatePath, data) - if err != nil { - return err - } - - filePath := filepath.Join(opts.ProjectPath, fileName) - if err := os.WriteFile(filePath, []byte(content), 0600); err != nil { - return fmt.Errorf("failed to create %s: %w", fileName, err) - } - } - - return nil -} - -// MultiAgentGenerator generates a multi-agent template -type MultiAgentGenerator struct{} +// WorkflowGenerator generates a streaming workflow template +type WorkflowGenerator struct{} -func NewMultiAgentGenerator() *MultiAgentGenerator { - return &MultiAgentGenerator{} +func NewWorkflowGenerator() *WorkflowGenerator { + return &WorkflowGenerator{} } -func (g *MultiAgentGenerator) GetMetadata() TemplateMetadata { +func (g *WorkflowGenerator) GetMetadata() TemplateMetadata { return TemplateMetadata{ - Name: "Multi-Agent", - Description: "Multiple agents with workflow pipeline", + Name: "Workflow", + Description: "Multi-step streaming workflow pipeline", Complexity: "⭐⭐⭐", - FileCount: 8, - Features: []string{"Agents", "Workflow", "Sequential Pipeline", ".env Config"}, - } -} - -func (g *MultiAgentGenerator) Generate(ctx context.Context, opts GenerateOptions) error { - // TODO: Phase 2 - Implement multi-agent generator - return fmt.Errorf("multi-agent template not yet implemented") -} - -// ConfigDrivenGenerator generates a config-driven template -type ConfigDrivenGenerator struct{} - -func NewConfigDrivenGenerator() *ConfigDrivenGenerator { - return &ConfigDrivenGenerator{} -} - -func (g *ConfigDrivenGenerator) GetMetadata() TemplateMetadata { - return TemplateMetadata{ - Name: "Config-Driven", - Description: "Enterprise setup with TOML configuration", - Complexity: "⭐⭐⭐⭐", - FileCount: 12, - Features: []string{"Agents", "Workflow", "Factory Pattern", "TOML Config", "Memory"}, - } -} - -func (g *ConfigDrivenGenerator) Generate(ctx context.Context, opts GenerateOptions) error { - // TODO: Phase 2 - Implement config-driven generator - return fmt.Errorf("config-driven template not yet implemented") -} - -// AdvancedGenerator generates an advanced template -type AdvancedGenerator struct{} - -func NewAdvancedGenerator() *AdvancedGenerator { - return &AdvancedGenerator{} -} - -func (g *AdvancedGenerator) GetMetadata() TemplateMetadata { - return TemplateMetadata{ - Name: "Advanced", - Description: "Full-stack with server, frontend, and Docker", - Complexity: "⭐⭐⭐⭐⭐", - FileCount: 20, - Features: []string{"Agents", "Workflow", "Server", "Frontend", "WebSocket", "Docker", "TOML Config"}, + FileCount: 3, + Features: []string{"Workflow", "Multi-Agent", "Streaming", "Step Tracking"}, } } -func (g *AdvancedGenerator) Generate(ctx context.Context, opts GenerateOptions) error { - // TODO: Phase 2 - Implement advanced generator - return fmt.Errorf("advanced template not yet implemented") -} - -// MCPToolsGenerator generates an MCP-enabled agent template -type MCPToolsGenerator struct{} - -func NewMCPToolsGenerator() *MCPToolsGenerator { - return &MCPToolsGenerator{} -} - -func (g *MCPToolsGenerator) GetMetadata() TemplateMetadata { - return TemplateMetadata{ - Name: "MCP-Tools", - Description: "Agent with MCP server tool integration", - Complexity: "⭐⭐", - FileCount: 3, - Features: []string{"Agent", "MCP Tools", "Streaming", "Observability"}, +func (g *WorkflowGenerator) Generate(ctx context.Context, opts GenerateOptions) error { + files := map[string]string{ + "go.mod": "templates/workflow/go.mod.tmpl", + "main.go": "templates/workflow/main.go.tmpl", + "README.md": "templates/workflow/README.md.tmpl", } + return generateTemplateFiles(opts, files) } func generateTemplateFiles(opts GenerateOptions, files map[string]string) error { @@ -303,41 +171,6 @@ func generateTemplateFiles(opts GenerateOptions, files map[string]string) error return nil } -func (g *MCPToolsGenerator) Generate(ctx context.Context, opts GenerateOptions) error { - files := map[string]string{ - "go.mod": "templates/mcp-tools/go.mod.tmpl", - "main.go": "templates/mcp-tools/main.go.tmpl", - "README.md": "templates/mcp-tools/README.md.tmpl", - } - return generateTemplateFiles(opts, files) -} - -// WorkflowGenerator generates a streaming workflow template -type WorkflowGenerator struct{} - -func NewWorkflowGenerator() *WorkflowGenerator { - return &WorkflowGenerator{} -} - -func (g *WorkflowGenerator) GetMetadata() TemplateMetadata { - return TemplateMetadata{ - Name: "Workflow", - Description: "Multi-step streaming workflow pipeline", - Complexity: "⭐⭐⭐", - FileCount: 3, - Features: []string{"Workflow", "Multi-Agent", "Streaming", "Step Tracking"}, - } -} - -func (g *WorkflowGenerator) Generate(ctx context.Context, opts GenerateOptions) error { - files := map[string]string{ - "go.mod": "templates/workflow/go.mod.tmpl", - "main.go": "templates/workflow/main.go.tmpl", - "README.md": "templates/workflow/README.md.tmpl", - } - return generateTemplateFiles(opts, files) -} - // Helper to get default model for provider func getLLMModel(provider string) string { switch provider { @@ -346,9 +179,9 @@ func getLLMModel(provider string) string { case ProviderOllama: return "llama3.2" case ProviderOpenAI: - return "gpt-4-turbo" + return "gpt-4o" default: - return "gpt-4-turbo" + return "gpt-4o" } } diff --git a/pkg/scaffold/templates/mcp-tools/README.md.tmpl b/pkg/scaffold/templates/mcp-tools/README.md.tmpl deleted file mode 100644 index 1759478..0000000 --- a/pkg/scaffold/templates/mcp-tools/README.md.tmpl +++ /dev/null @@ -1,53 +0,0 @@ -# {{.ProjectName}} - MCP Tools Agent - -An AgenticGoKit agent with MCP (Model Context Protocol) tool integration. - -## Features - -- **MCP Tool Support**: Access external tools via MCP servers -- **Streaming**: Real-time response streaming -- **Observability**: Built-in tracing with `AGK_TRACE=true` - -## Quick Start - -```bash -# Install dependencies -go mod tidy - -# Set your API key -export OPENAI_API_KEY=your-key-here -# OR for Ollama (no API key needed) -# export LLM_PROVIDER=ollama - -# Run with tracing -AGK_TRACE=true go run main.go -``` - -## Available MCP Servers - -1. **filesystem**: Read/write files and directories -2. **fetch**: Make HTTP requests -3. **brave-search**: Web search (requires BRAVE_API_KEY) -4. **memory**: Persistent key-value storage - -## Adding More Tools - -Edit `main.go` and add more MCP servers to the `Servers` slice: - -```go -{ - Name: "your-server", - Command: "npx", - Args: []string{"-y", "@your-org/mcp-server"}, -} -``` - -## Viewing Traces - -```bash -# Install agk CLI -go install github.com/agenticgokit/agk@latest - -# View traces -agk trace -``` diff --git a/pkg/scaffold/templates/mcp-tools/go.mod.tmpl b/pkg/scaffold/templates/mcp-tools/go.mod.tmpl deleted file mode 100644 index b32187c..0000000 --- a/pkg/scaffold/templates/mcp-tools/go.mod.tmpl +++ /dev/null @@ -1,5 +0,0 @@ -module {{.ProjectName}} - -go 1.24.1 - -require github.com/agenticgokit/agenticgokit v0.5.3 diff --git a/pkg/scaffold/templates/mcp-tools/main.go.tmpl b/pkg/scaffold/templates/mcp-tools/main.go.tmpl deleted file mode 100644 index 2a5cb5d..0000000 --- a/pkg/scaffold/templates/mcp-tools/main.go.tmpl +++ /dev/null @@ -1,115 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log" - "os" - "time" - - // Import MCP plugins to enable MCP functionality - _ "github.com/agenticgokit/agenticgokit/plugins/mcp/default" - _ "github.com/agenticgokit/agenticgokit/plugins/mcp/unified" - - agk "github.com/agenticgokit/agenticgokit/v1beta" -) - -func main() { - ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) - defer cancel() - - // Get service version from environment or use default - serviceVersion := os.Getenv("SERVICE_VERSION") - if serviceVersion == "" { - serviceVersion = "0.1.0" - } - - fmt.Println("Initializing Agent with MCP Tools...") - - // Create agent with MCP tools and observability - // Tracing is enabled if AGK_TRACE=true environment variable is set - agent, err := agk.NewBuilder("{{.ProjectName}}"). - - WithObservability("{{.ProjectName}}", serviceVersion). - WithConfig(&agk.Config{ - Name: "{{.ProjectName}}", - SystemPrompt: "You are a helpful assistant with access to tools. Use the available MCP tools to help the user accomplish their tasks. Always explain what tools you're using and why.", - Timeout: 120 * time.Second, - LLM: agk.LLMConfig{ - Provider: "{{.LLMProvider}}", - Model: "{{.LLMModel}}", - Temperature: 0.7, - MaxTokens: 4000, - }, - Tools: &agk.ToolsConfig{ - Enabled: true, - // Configure MCP servers for tool access - // MCP (Model Context Protocol) enables agents to use external tools - MCP: &agk.MCPConfig{ - Enabled: true, - Discovery: false, - ConnectionTimeout: 10 * time.Second, - Servers: []agk.MCPServer{ - { - Name: "filesystem", - Type: "stdio", - // Note: The current stdio implementation does not support arguments. - // You must point to a specific executable or a wrapper script. - // Example: "./mcp-server-filesystem.exe" or "./run-filesystem.bat" - Command: "npx", // Placeholder - likely needs a wrapper script - Enabled: true, - }, - // Example of HTTP SSE configuration (alternative to stdio) - // { - // Name: "weather-server", - // Type: "http_sse", - // Address: "http://localhost:8080", - // Enabled: true, - // }, - }, - }, - }, - }). - Build() - if err != nil { - log.Fatalf("Failed to create agent: %v", err) - } - defer agent.Cleanup(ctx) - - // Wait for MCP servers to initialize - fmt.Println("Initializing MCP tools...") - time.Sleep(2 * time.Second) - - // Example: Use tools to help with a task - userMessage := "List the files in the current directory and tell me what this project is about." - - fmt.Printf("\nUser: %s\n\n", userMessage) - fmt.Println("Assistant:") - - // Use streaming for real-time response - stream, err := agent.RunStream(ctx, userMessage) - if err != nil { - log.Fatalf("Failed to start streaming: %v", err) - } - - printStreamingResponse(stream) -} - -// printStreamingResponse prints the streaming response as tokens arrive -func printStreamingResponse(stream agk.Stream) { - for chunk := range stream.Chunks() { - if chunk.Error != nil { - fmt.Printf("\nError: %v\n", chunk.Error) - break - } - - switch chunk.Type { - case agk.ChunkTypeDelta: - fmt.Print(chunk.Delta) - case agk.ChunkTypeToolCall: - fmt.Printf("\n[Tool Call: %s]\n", chunk.ToolName) - case agk.ChunkTypeDone: - fmt.Println("\n\nCompleted") - } - } -} diff --git a/pkg/scaffold/templates/quickstart/main.go.tmpl b/pkg/scaffold/templates/quickstart/main.go.tmpl index 2f5e8c6..cd1bbb9 100644 --- a/pkg/scaffold/templates/quickstart/main.go.tmpl +++ b/pkg/scaffold/templates/quickstart/main.go.tmpl @@ -8,6 +8,7 @@ import ( "time" agk "github.com/agenticgokit/agenticgokit/v1beta" + _ "github.com/agenticgokit/agenticgokit/plugins/llm/{{if .LLMProvider}}{{.LLMProvider}}{{else}}openai{{end}}" ) func main() { @@ -33,7 +34,7 @@ func main() { defer agent.Cleanup(ctx) // Simple conversation - userMessage := "What is AgenticGoKit?" + userMessage := "Write a haiku about coding." fmt.Printf("User: %s\n\n", userMessage) fmt.Println("Assistant:") diff --git a/pkg/scaffold/templates/single-agent/.env.tmpl b/pkg/scaffold/templates/single-agent/.env.tmpl deleted file mode 100644 index 6287376..0000000 --- a/pkg/scaffold/templates/single-agent/.env.tmpl +++ /dev/null @@ -1,4 +0,0 @@ -# LLM Configuration -OPENAI_API_KEY=your-api-key-here -LLM_MODEL={{.LLMModel}} -LLM_TEMPERATURE=0.7 diff --git a/pkg/scaffold/templates/single-agent/go.mod.tmpl b/pkg/scaffold/templates/single-agent/go.mod.tmpl deleted file mode 100644 index b5475c9..0000000 --- a/pkg/scaffold/templates/single-agent/go.mod.tmpl +++ /dev/null @@ -1,7 +0,0 @@ -module github.com/example/{{.ProjectName}} - -go 1.21 - -require ( - github.com/agenticgokit/agenticgokit v0.5.3 -) diff --git a/pkg/scaffold/templates/single-agent/main.go.tmpl b/pkg/scaffold/templates/single-agent/main.go.tmpl deleted file mode 100644 index a03bccd..0000000 --- a/pkg/scaffold/templates/single-agent/main.go.tmpl +++ /dev/null @@ -1,81 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log" - "os" - "time" - - agk "github.com/agenticgokit/agenticgokit/v1beta" -) - -func main() { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - // Get service version from environment or use default - serviceVersion := os.Getenv("SERVICE_VERSION") - if serviceVersion == "" { - serviceVersion = "0.1.0" - } - - // Create an agent with tools and memory - // Observability is automatically configured via the builder pattern: - // - If AGK_TRACE=true, tracing is automatically enabled - // - Default exporter is file (creates traces-{runID}.jsonl) - // - Can be customized via env vars: AGK_TRACE_EXPORTER, AGK_TRACE_ENDPOINT, AGK_TRACE_SAMPLE - // Usage: AGK_TRACE=true go run main.go - agent, err := agk.NewBuilder("{{.ProjectName}}"). - - WithObservability("{{.ProjectName}}", serviceVersion). - WithConfig(&agk.Config{ - Name: "{{.ProjectName}}", - SystemPrompt: "You are a helpful research assistant. Provide detailed, well-structured answers with examples when appropriate.", - Timeout: 30 * time.Second, - LLM: agk.LLMConfig{ - Provider: "{{.LLMProvider}}", - Model: "{{.LLMModel}}", - Temperature: 0.7, - MaxTokens: 2000, - }, - }). - Build() - if err != nil { - log.Fatalf("Failed to create agent: %v", err) - } - defer agent.Cleanup(ctx) - - // Example with tools would be added here - // For now, basic conversation with streaming - - userMessage := "Help me understand AgenticGoKit's architecture" - - fmt.Printf("User: %s\n\n", userMessage) - fmt.Println("Assistant:") - - // Use streaming for real-time response and better timeout handling - stream, err := agent.RunStream(ctx, userMessage) - if err != nil { - log.Fatalf("Failed to start streaming: %v", err) - } - - printStreamingResponse(stream) -} - -// printStreamingResponse prints the streaming response as tokens arrive -func printStreamingResponse(stream agk.Stream) { - for chunk := range stream.Chunks() { - if chunk.Error != nil { - fmt.Printf("\n❌ Error: %v\n", chunk.Error) - break - } - - switch chunk.Type { - case agk.ChunkTypeDelta: - fmt.Print(chunk.Delta) - case agk.ChunkTypeDone: - fmt.Println("\n\nβœ… Completed") - } - } -} diff --git a/pkg/scaffold/templates/workflow/main.go.tmpl b/pkg/scaffold/templates/workflow/main.go.tmpl index 537ee73..aa7b9d8 100644 --- a/pkg/scaffold/templates/workflow/main.go.tmpl +++ b/pkg/scaffold/templates/workflow/main.go.tmpl @@ -8,7 +8,7 @@ import ( "time" agk "github.com/agenticgokit/agenticgokit/v1beta" - _ "github.com/agenticgokit/agenticgokit/plugins/llm/{{.LLMProvider}}" + _ "github.com/agenticgokit/agenticgokit/plugins/llm/{{if .LLMProvider}}{{.LLMProvider}}{{else}}openai{{end}}" ) func main() { @@ -28,67 +28,45 @@ func main() { log.Fatalf("Failed to create workflow: %v", err) } - // Create agents for each step - // Note: Observability/tracing is handled by the workflow - no need to add WithObservability to agents - researcherConfig := &agk.Config{ - Name: "researcher", - SystemPrompt: "You are a research assistant. When given a topic, provide detailed, factual information about it. Include key concepts, important details, and relevant context.", - Timeout: 60 * time.Second, - LLM: agk.LLMConfig{ - Provider: "{{.LLMProvider}}", - Model: "{{.LLMModel}}", - Temperature: 0.7, - MaxTokens: 2000, - APIKey: os.Getenv("{{.APIKeyEnv}}"), - }, + // Shared base configuration + baseLLM := agk.LLMConfig{ + Provider: "{{.LLMProvider}}", + Model: "{{.LLMModel}}", + APIKey: os.Getenv("{{.APIKeyEnv}}"), + MaxTokens: 2000, } - researcher, err := agk.NewBuilder("researcher"). - WithConfig(researcherConfig). - Build() - if err != nil { - log.Fatalf("Failed to create researcher agent: %v", err) + // Helper to create agents cleanly + createAgent := func(name, prompt string, temp float64) agk.Agent { + cfg := baseLLM + cfg.Temperature = float32(temp) // Convert float64 to float32 + + // Create agent with specific config + agent, err := agk.NewBuilder(name). + WithConfig(&agk.Config{ + Name: name, + SystemPrompt: prompt, + Timeout: 60 * time.Second, + LLM: cfg, + }).Build() + if err != nil { + log.Fatalf("Failed to create %s: %v", name, err) + } + return agent } - summarizerConfig := &agk.Config{ - Name: "summarizer", - SystemPrompt: "You are a summarization expert. Take the provided content and create a clear, concise summary that captures the key points. Use bullet points for clarity.", - Timeout: 60 * time.Second, - LLM: agk.LLMConfig{ - Provider: "{{.LLMProvider}}", - Model: "{{.LLMModel}}", - Temperature: 0.5, - MaxTokens: 1000, - APIKey: os.Getenv("{{.APIKeyEnv}}"), - }, - } + // Create agents + researcher := createAgent("researcher", + "You are a research assistant. When given a topic, provide detailed, factual information about it. Include key concepts, important details, and relevant context.", + 0.7) - summarizer, err := agk.NewBuilder("summarizer"). - WithConfig(summarizerConfig). - Build() - if err != nil { - log.Fatalf("Failed to create summarizer agent: %v", err) - } + summarizer := createAgent("summarizer", + "You are a summarization expert. Take the provided content and create a clear, concise summary that captures the key points. Use bullet points for clarity.", + 0.5) - formatterConfig := &agk.Config{ - Name: "formatter", - SystemPrompt: "You are a content formatter. Take the provided summary and format it as a professional report with sections, headers, and clear structure.", - Timeout: 60 * time.Second, - LLM: agk.LLMConfig{ - Provider: "{{.LLMProvider}}", - Model: "{{.LLMModel}}", - Temperature: 0.3, - MaxTokens: 1500, - APIKey: os.Getenv("{{.APIKeyEnv}}"), - }, - } - - formatter, err := agk.NewBuilder("formatter"). - WithConfig(formatterConfig). - Build() - if err != nil { - log.Fatalf("Failed to create formatter agent: %v", err) - } + formatter := createAgent("formatter", + "You are a content formatter. Take the provided summary and format it as a professional report with sections, headers, and clear structure.", + 0.3) // Add steps to the workflow // Step 1: Research - gather information