Skip to content

Commit 9b29474

Browse files
committed
feat(cli): implement dynamic template discovery and enhance template management
1 parent 0b0ec48 commit 9b29474

File tree

9 files changed

+611
-193
lines changed

9 files changed

+611
-193
lines changed

src/client/acontext-cli/cmd/create.go

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ func runCreate(cmd *cobra.Command, args []string) error {
107107
if err != nil {
108108
return err
109109
}
110+
if err != nil {
111+
return err
112+
}
110113
fmt.Printf("✓ Selected template: %s\n", preset.Name)
111114
fmt.Println()
112115

@@ -117,15 +120,25 @@ func runCreate(cmd *cobra.Command, args []string) error {
117120
return fmt.Errorf("invalid template key: %s", templateKey)
118121
}
119122

123+
// Try to get template from config first
120124
tmpl, err := config.GetTemplate(parts[0], parts[1])
121125
if err != nil {
122-
return fmt.Errorf("failed to get template: %w", err)
123-
}
124-
125-
templateConfig = &template.Config{
126-
Repo: tmpl.Repo,
127-
Path: tmpl.Path,
128-
Description: tmpl.Description,
126+
// If not found in config, construct path dynamically
127+
cfg, err := config.LoadTemplatesConfig()
128+
if err != nil {
129+
return fmt.Errorf("failed to load templates config: %w", err)
130+
}
131+
templateConfig = &template.Config{
132+
Repo: cfg.Repo,
133+
Path: fmt.Sprintf("%s/%s", parts[0], parts[1]),
134+
Description: fmt.Sprintf("%s template", templateKey),
135+
}
136+
} else {
137+
templateConfig = &template.Config{
138+
Repo: tmpl.Repo,
139+
Path: tmpl.Path,
140+
Description: tmpl.Description,
141+
}
129142
}
130143
}
131144

@@ -134,9 +147,11 @@ func runCreate(cmd *cobra.Command, args []string) error {
134147
return fmt.Errorf("failed to create project directory: %w", err)
135148
}
136149

137-
// 7. Download template
138-
139-
if err := template.DownloadTemplate(templateConfig, projectDir); err != nil {
150+
// 7. Download template with project name variable
151+
vars := map[string]string{
152+
"project_name": projectName,
153+
}
154+
if err := template.DownloadTemplateWithVars(templateConfig, projectDir, vars); err != nil {
140155
return fmt.Errorf("failed to download template: %w", err)
141156
}
142157
fmt.Println()
@@ -237,7 +252,24 @@ func promptLanguage() (string, error) {
237252

238253
// promptTemplate prompts user to select a template
239254
func promptTemplate(language string) (string, *config.Preset, error) {
255+
// Check if we need to discover templates dynamically
256+
needsDiscovery, err := config.NeedsTemplateDiscovery(language)
257+
if err != nil {
258+
return "", nil, fmt.Errorf("failed to check template discovery: %w", err)
259+
}
260+
261+
// Show loading indicator if we need to discover templates
262+
if needsDiscovery {
263+
fmt.Print("🔍 Discovering templates from repository...")
264+
}
265+
240266
presets, err := config.GetPresets(language)
267+
268+
// Clear loading message if it was shown
269+
if needsDiscovery {
270+
fmt.Print("\r" + strings.Repeat(" ", 50) + "\r")
271+
}
272+
241273
if err != nil {
242274
return "", nil, fmt.Errorf("failed to get presets: %w", err)
243275
}
@@ -246,17 +278,13 @@ func promptTemplate(language string) (string, *config.Preset, error) {
246278
return "", nil, fmt.Errorf("no presets available for language: %s", language)
247279
}
248280

249-
// Create options list
281+
// Create options list (simplified - just show template names)
250282
options := make([]string, len(presets))
251-
optionsWithDesc := make(map[string]*config.Preset)
283+
optionsMap := make(map[string]*config.Preset)
252284

253285
for i, preset := range presets {
254-
optionText := preset.Name
255-
if preset.Description != "" {
256-
optionText += " - " + preset.Description
257-
}
258-
options[i] = optionText
259-
optionsWithDesc[optionText] = &presets[i]
286+
options[i] = preset.Name
287+
optionsMap[preset.Name] = &presets[i]
260288
}
261289

262290
var selectedOption string
@@ -270,7 +298,7 @@ func promptTemplate(language string) (string, *config.Preset, error) {
270298
return "", nil, fmt.Errorf("failed to select template: %w", err)
271299
}
272300

273-
preset, ok := optionsWithDesc[selectedOption]
301+
preset, ok := optionsMap[selectedOption]
274302
if !ok {
275303
return "", nil, fmt.Errorf("selected preset not found")
276304
}

src/client/acontext-cli/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.25.1
44

55
require (
66
github.com/AlecAivazis/survey/v2 v2.3.7
7+
github.com/pelletier/go-toml/v2 v2.2.4
78
github.com/spf13/cobra v1.8.0
89
github.com/stretchr/testify v1.9.0
910
gopkg.in/yaml.v3 v3.0.1

src/client/acontext-cli/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE
2828
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
2929
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
3030
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
31+
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
32+
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
3133
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
3234
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
3335
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=

src/client/acontext-cli/internal/config/templates.go

Lines changed: 131 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ package config
33
import (
44
_ "embed"
55
"fmt"
6+
"io"
7+
"os"
8+
"os/exec"
9+
"path/filepath"
10+
"strings"
611

712
"gopkg.in/yaml.v3"
813
)
@@ -21,8 +26,9 @@ type Preset struct {
2126
}
2227

2328
type TemplatesConfig struct {
29+
Repo string `yaml:"repo"`
2430
Templates map[string]map[string]TemplateConfig `yaml:"templates"`
25-
Presets map[string][]Preset `yaml:"presets"`
31+
Presets map[string][]Preset `yaml:"presets,omitempty"`
2632
}
2733

2834
//go:embed templates.yaml
@@ -65,19 +71,37 @@ func GetTemplate(language, templateKey string) (*TemplateConfig, error) {
6571
return &template, nil
6672
}
6773

74+
// NeedsTemplateDiscovery checks if templates need to be discovered from repository
75+
func NeedsTemplateDiscovery(language string) (bool, error) {
76+
config, err := LoadTemplatesConfig()
77+
if err != nil {
78+
return false, err
79+
}
80+
81+
// If presets are defined in YAML, no need to discover
82+
if presets, ok := config.Presets[language]; ok && len(presets) > 0 {
83+
return false, nil
84+
}
85+
86+
// Otherwise, need to discover templates from repository
87+
return true, nil
88+
}
89+
6890
// GetPresets gets preset list for specified language
91+
// If presets are defined in YAML, use them; otherwise, dynamically discover from repo
6992
func GetPresets(language string) ([]Preset, error) {
7093
config, err := LoadTemplatesConfig()
7194
if err != nil {
7295
return nil, err
7396
}
7497

75-
presets, ok := config.Presets[language]
76-
if !ok {
77-
return nil, fmt.Errorf("no presets for language: %s", language)
98+
// If presets are defined in YAML, use them
99+
if presets, ok := config.Presets[language]; ok && len(presets) > 0 {
100+
return presets, nil
78101
}
79102

80-
return presets, nil
103+
// Otherwise, dynamically discover templates from repository
104+
return discoverTemplates(config.Repo, language)
81105
}
82106

83107
// GetLanguages gets all supported languages
@@ -87,9 +111,109 @@ func GetLanguages() []string {
87111
return []string{}
88112
}
89113

90-
languages := make([]string, 0, len(config.Presets))
91-
for lang := range config.Presets {
114+
// If presets are defined, use them
115+
if len(config.Presets) > 0 {
116+
languages := make([]string, 0, len(config.Presets))
117+
for lang := range config.Presets {
118+
languages = append(languages, lang)
119+
}
120+
return languages
121+
}
122+
123+
// Otherwise, use templates
124+
languages := make([]string, 0, len(config.Templates))
125+
for lang := range config.Templates {
92126
languages = append(languages, lang)
93127
}
94128
return languages
95129
}
130+
131+
// discoverTemplates dynamically discovers templates from the repository
132+
func discoverTemplates(repo, language string) ([]Preset, error) {
133+
// Create temporary directory
134+
tempDir, err := os.MkdirTemp("", "acontext-discover-*")
135+
if err != nil {
136+
return nil, fmt.Errorf("failed to create temp directory: %w", err)
137+
}
138+
defer func() {
139+
_ = os.RemoveAll(tempDir)
140+
}()
141+
142+
// Sparse clone repository
143+
cmd := exec.Command(
144+
"git", "clone",
145+
"--filter=blob:none",
146+
"--sparse",
147+
"--depth=1",
148+
"--quiet",
149+
repo,
150+
tempDir,
151+
)
152+
cmd.Stdout = io.Discard
153+
cmd.Stderr = io.Discard
154+
if err := cmd.Run(); err != nil {
155+
return nil, fmt.Errorf("failed to clone repo: %w", err)
156+
}
157+
158+
// Enable sparse-checkout for the language directory
159+
cmd = exec.Command("git", "sparse-checkout", "init", "--cone")
160+
cmd.Dir = tempDir
161+
cmd.Stdout = io.Discard
162+
cmd.Stderr = io.Discard
163+
if err := cmd.Run(); err != nil {
164+
return nil, fmt.Errorf("failed to init sparse-checkout: %w", err)
165+
}
166+
167+
cmd = exec.Command("git", "sparse-checkout", "set", language)
168+
cmd.Dir = tempDir
169+
cmd.Stdout = io.Discard
170+
cmd.Stderr = io.Discard
171+
if err := cmd.Run(); err != nil {
172+
return nil, fmt.Errorf("failed to set sparse-checkout: %w", err)
173+
}
174+
175+
// List subdirectories in the language folder
176+
langDir := filepath.Join(tempDir, language)
177+
entries, err := os.ReadDir(langDir)
178+
if err != nil {
179+
return nil, fmt.Errorf("failed to read language directory: %w", err)
180+
}
181+
182+
var presets []Preset
183+
for _, entry := range entries {
184+
if entry.IsDir() {
185+
templateName := entry.Name()
186+
// Skip hidden directories and common non-template folders
187+
if strings.HasPrefix(templateName, ".") || templateName == "node_modules" {
188+
continue
189+
}
190+
191+
presets = append(presets, Preset{
192+
Name: formatTemplateName(language, templateName),
193+
Template: fmt.Sprintf("%s.%s", language, templateName),
194+
})
195+
}
196+
}
197+
198+
if len(presets) == 0 {
199+
return nil, fmt.Errorf("no templates found for language: %s", language)
200+
}
201+
202+
return presets, nil
203+
}
204+
205+
// formatTemplateName formats template name for display
206+
func formatTemplateName(language, templateName string) string {
207+
// Capitalize first letter and replace hyphens/underscores with spaces
208+
parts := strings.FieldsFunc(templateName, func(r rune) bool {
209+
return r == '-' || r == '_'
210+
})
211+
212+
for i, part := range parts {
213+
if len(part) > 0 {
214+
parts[i] = strings.ToUpper(part[:1]) + part[1:]
215+
}
216+
}
217+
218+
return strings.Join(parts, " ")
219+
}
Lines changed: 24 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,31 @@
11
# Acontext CLI Template Configuration File
2-
# Defines template mapping relationships for different combinations
32
#
43
# Template Repository: https://github.com/memodb-io/Acontext-Examples
54
#
6-
# SDK Notes:
7-
# - Python: acontext SDK (built-in openai, anthropic)
8-
# - TypeScript: acontext SDK + @vercel/ai SDK (supports multiple LLM providers)
5+
# Templates are dynamically discovered from the repository structure.
6+
# The CLI will automatically list all subdirectories under each language folder.
7+
#
8+
# Repository Structure:
9+
# python/
10+
# ├── openai/
11+
# ├── anthropic/
12+
# └── ...
13+
# typescript/
14+
# ├── vercel-ai/
15+
# ├── langchain/
16+
# └── ...
917

10-
templates:
11-
# Python Language Templates
12-
python:
13-
# Python + OpenAI
14-
openai:
15-
repo: "https://github.com/memodb-io/Acontext-Examples"
16-
path: "python/openai"
17-
description: "Python template with OpenAI integration"
18-
19-
# TypeScript Language Templates
20-
typescript:
21-
# TypeScript + Vercel AI SDK (Universal AI SDK)
22-
vercel-ai:
23-
repo: "https://github.com/memodb-io/Acontext-Examples"
24-
path: "typescript/vercel-ai"
25-
description: "TypeScript with Vercel AI SDK (supports OpenAI, Anthropic, etc.)"
18+
# Repository URL
19+
repo: "https://github.com/memodb-io/Acontext-Examples"
2620

27-
# Simplified Selection Mapping
28-
presets:
29-
python:
30-
- name: "Python + OpenAI"
31-
description: "OpenAI integration (built-in SDK)"
32-
combination:
33-
language: "python"
34-
sdk: "acontext"
35-
provider: "openai"
36-
template: "python.openai"
21+
# Supported languages (templates will be auto-discovered from these folders)
22+
templates:
23+
python: {}
24+
typescript: {}
3725

38-
typescript:
39-
- name: "TypeScript + Vercel AI SDK"
40-
description: "Universal AI SDK supporting multiple providers"
41-
combination:
42-
language: "typescript"
43-
sdk: "vercel-ai"
44-
template: "typescript.vercel-ai"
26+
# Optional: Define custom presets if you want to override auto-discovery
27+
# If not specified, templates will be dynamically discovered from the repository
28+
# presets:
29+
# python:
30+
# - name: "OpenAI"
31+
# template: "python.openai"

0 commit comments

Comments
 (0)