1package main
  2
  3import (
  4	"encoding/json"
  5	"fmt"
  6	"os"
  7	"reflect"
  8	"slices"
  9	"strings"
 10
 11	"github.com/charmbracelet/crush/internal/config"
 12)
 13
 14// JSONSchema represents a JSON Schema
 15type JSONSchema struct {
 16	Schema               string                 `json:"$schema,omitempty"`
 17	Title                string                 `json:"title,omitempty"`
 18	Description          string                 `json:"description,omitempty"`
 19	Type                 string                 `json:"type,omitempty"`
 20	Properties           map[string]*JSONSchema `json:"properties,omitempty"`
 21	Items                *JSONSchema            `json:"items,omitempty"`
 22	Required             []string               `json:"required,omitempty"`
 23	AdditionalProperties any                    `json:"additionalProperties,omitempty"`
 24	Enum                 []any                  `json:"enum,omitempty"`
 25	Default              any                    `json:"default,omitempty"`
 26	Definitions          map[string]*JSONSchema `json:"definitions,omitempty"`
 27	Ref                  string                 `json:"$ref,omitempty"`
 28	OneOf                []*JSONSchema          `json:"oneOf,omitempty"`
 29	AnyOf                []*JSONSchema          `json:"anyOf,omitempty"`
 30	AllOf                []*JSONSchema          `json:"allOf,omitempty"`
 31	Not                  *JSONSchema            `json:"not,omitempty"`
 32	Format               string                 `json:"format,omitempty"`
 33	Pattern              string                 `json:"pattern,omitempty"`
 34	MinLength            *int                   `json:"minLength,omitempty"`
 35	MaxLength            *int                   `json:"maxLength,omitempty"`
 36	Minimum              *float64               `json:"minimum,omitempty"`
 37	Maximum              *float64               `json:"maximum,omitempty"`
 38	ExclusiveMinimum     *float64               `json:"exclusiveMinimum,omitempty"`
 39	ExclusiveMaximum     *float64               `json:"exclusiveMaximum,omitempty"`
 40	MultipleOf           *float64               `json:"multipleOf,omitempty"`
 41	MinItems             *int                   `json:"minItems,omitempty"`
 42	MaxItems             *int                   `json:"maxItems,omitempty"`
 43	UniqueItems          *bool                  `json:"uniqueItems,omitempty"`
 44	MinProperties        *int                   `json:"minProperties,omitempty"`
 45	MaxProperties        *int                   `json:"maxProperties,omitempty"`
 46}
 47
 48// SchemaGenerator generates JSON schemas from Go types
 49type SchemaGenerator struct {
 50	definitions map[string]*JSONSchema
 51	visited     map[reflect.Type]bool
 52}
 53
 54// NewSchemaGenerator creates a new schema generator
 55func NewSchemaGenerator() *SchemaGenerator {
 56	return &SchemaGenerator{
 57		definitions: make(map[string]*JSONSchema),
 58		visited:     make(map[reflect.Type]bool),
 59	}
 60}
 61
 62func main() {
 63	// Enable mock providers to avoid API calls during schema generation
 64	config.UseMockProviders = true
 65
 66	generator := NewSchemaGenerator()
 67	schema := generator.GenerateSchema()
 68
 69	// Pretty print the schema
 70	encoder := json.NewEncoder(os.Stdout)
 71	encoder.SetIndent("", "  ")
 72	if err := encoder.Encode(schema); err != nil {
 73		fmt.Fprintf(os.Stderr, "Error encoding schema: %v\n", err)
 74		os.Exit(1)
 75	}
 76}
 77
 78// GenerateSchema generates the complete JSON schema for the Crush configuration
 79func (g *SchemaGenerator) GenerateSchema() *JSONSchema {
 80	// Generate schema for the main Config struct
 81	configType := reflect.TypeOf(config.Config{})
 82	configSchema := g.generateTypeSchema(configType)
 83
 84	// Create the root schema
 85	schema := &JSONSchema{
 86		Schema:      "http://json-schema.org/draft-07/schema#",
 87		Title:       "Crush Configuration",
 88		Description: "Configuration schema for the Crush application",
 89		Type:        configSchema.Type,
 90		Properties:  configSchema.Properties,
 91		Required:    configSchema.Required,
 92		Definitions: g.definitions,
 93	}
 94
 95	// Add custom enhancements
 96	g.enhanceSchema(schema)
 97
 98	return schema
 99}
100
101// generateTypeSchema generates a JSON schema for a given Go type
102func (g *SchemaGenerator) generateTypeSchema(t reflect.Type) *JSONSchema {
103	// Handle pointers
104	if t.Kind() == reflect.Ptr {
105		return g.generateTypeSchema(t.Elem())
106	}
107
108	// Check if we've already processed this type
109	if g.visited[t] {
110		// Return a reference to avoid infinite recursion
111		return &JSONSchema{
112			Ref: fmt.Sprintf("#/definitions/%s", t.Name()),
113		}
114	}
115
116	switch t.Kind() {
117	case reflect.String:
118		return &JSONSchema{Type: "string"}
119	case reflect.Bool:
120		return &JSONSchema{Type: "boolean"}
121	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
122		reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
123		return &JSONSchema{Type: "integer"}
124	case reflect.Float32, reflect.Float64:
125		return &JSONSchema{Type: "number"}
126	case reflect.Slice, reflect.Array:
127		itemSchema := g.generateTypeSchema(t.Elem())
128		return &JSONSchema{
129			Type:  "array",
130			Items: itemSchema,
131		}
132	case reflect.Map:
133		valueSchema := g.generateTypeSchema(t.Elem())
134		return &JSONSchema{
135			Type:                 "object",
136			AdditionalProperties: valueSchema,
137		}
138	case reflect.Struct:
139		return g.generateStructSchema(t)
140	case reflect.Interface:
141		// For interface{} types, allow any value
142		return &JSONSchema{}
143	default:
144		// Fallback for unknown types
145		return &JSONSchema{}
146	}
147}
148
149// generateStructSchema generates a JSON schema for a struct type
150func (g *SchemaGenerator) generateStructSchema(t reflect.Type) *JSONSchema {
151	// Mark as visited to prevent infinite recursion
152	g.visited[t] = true
153
154	schema := &JSONSchema{
155		Type:       "object",
156		Properties: make(map[string]*JSONSchema),
157	}
158
159	var required []string
160
161	for i := range t.NumField() {
162		field := t.Field(i)
163
164		// Skip unexported fields
165		if !field.IsExported() {
166			continue
167		}
168
169		// Get JSON tag
170		jsonTag := field.Tag.Get("json")
171		if jsonTag == "-" {
172			continue
173		}
174
175		// Parse JSON tag
176		jsonName, options := parseJSONTag(jsonTag)
177		if jsonName == "" {
178			jsonName = strings.ToLower(field.Name)
179		}
180
181		// Generate field schema
182		fieldSchema := g.generateTypeSchema(field.Type)
183
184		// Add description from field name if not present
185		if fieldSchema.Description == "" {
186			fieldSchema.Description = generateFieldDescription(field.Name, field.Type)
187		}
188
189		// Check if field is required (not omitempty and not a pointer)
190		if !slices.Contains(options, "omitempty") && field.Type.Kind() != reflect.Ptr {
191			required = append(required, jsonName)
192		}
193
194		schema.Properties[jsonName] = fieldSchema
195	}
196
197	if len(required) > 0 {
198		schema.Required = required
199	}
200
201	// Store in definitions if it's a named type
202	if t.Name() != "" {
203		g.definitions[t.Name()] = schema
204	}
205
206	return schema
207}
208
209// parseJSONTag parses a JSON struct tag
210func parseJSONTag(tag string) (name string, options []string) {
211	if tag == "" {
212		return "", nil
213	}
214
215	parts := strings.Split(tag, ",")
216	name = parts[0]
217	if len(parts) > 1 {
218		options = parts[1:]
219	}
220	return name, options
221}
222
223// generateFieldDescription generates a description for a field based on its name and type
224func generateFieldDescription(fieldName string, fieldType reflect.Type) string {
225	// Convert camelCase to words
226	words := camelCaseToWords(fieldName)
227	description := strings.Join(words, " ")
228
229	// Add type-specific information
230	switch fieldType.Kind() {
231	case reflect.Bool:
232		if !strings.Contains(strings.ToLower(description), "enable") &&
233			!strings.Contains(strings.ToLower(description), "disable") {
234			description = "Enable " + strings.ToLower(description)
235		}
236	case reflect.Slice:
237		if !strings.HasSuffix(description, "s") {
238			description = description + " list"
239		}
240	case reflect.Map:
241		description = description + " configuration"
242	}
243
244	return description
245}
246
247// camelCaseToWords converts camelCase to separate words
248func camelCaseToWords(s string) []string {
249	var words []string
250	var currentWord strings.Builder
251
252	for i, r := range s {
253		if i > 0 && r >= 'A' && r <= 'Z' {
254			if currentWord.Len() > 0 {
255				words = append(words, currentWord.String())
256				currentWord.Reset()
257			}
258		}
259		currentWord.WriteRune(r)
260	}
261
262	if currentWord.Len() > 0 {
263		words = append(words, currentWord.String())
264	}
265
266	return words
267}
268
269// enhanceSchema adds custom enhancements to the generated schema
270func (g *SchemaGenerator) enhanceSchema(schema *JSONSchema) {
271	// Add provider enums
272	g.addProviderEnums(schema)
273
274	// Add model enums
275	g.addModelEnums(schema)
276
277	// Add agent enums
278	g.addAgentEnums(schema)
279
280	// Add tool enums
281	g.addToolEnums(schema)
282
283	// Add MCP type enums
284	g.addMCPTypeEnums(schema)
285
286	// Add model type enums
287	g.addModelTypeEnums(schema)
288
289	// Add default values
290	g.addDefaultValues(schema)
291
292	// Add custom descriptions
293	g.addCustomDescriptions(schema)
294}
295
296// addProviderEnums adds provider enums to the schema
297func (g *SchemaGenerator) addProviderEnums(schema *JSONSchema) {
298	providers := config.Providers()
299	var providerIDs []any
300	for _, p := range providers {
301		providerIDs = append(providerIDs, string(p.ID))
302	}
303
304	// Add to PreferredModel provider field
305	if preferredModelDef, exists := schema.Definitions["PreferredModel"]; exists {
306		if providerProp, exists := preferredModelDef.Properties["provider"]; exists {
307			providerProp.Enum = providerIDs
308		}
309	}
310
311	// Add to ProviderConfig ID field
312	if providerConfigDef, exists := schema.Definitions["ProviderConfig"]; exists {
313		if idProp, exists := providerConfigDef.Properties["id"]; exists {
314			idProp.Enum = providerIDs
315		}
316	}
317}
318
319// addModelEnums adds model enums to the schema
320func (g *SchemaGenerator) addModelEnums(schema *JSONSchema) {
321	providers := config.Providers()
322	var modelIDs []any
323	for _, p := range providers {
324		for _, m := range p.Models {
325			modelIDs = append(modelIDs, m.ID)
326		}
327	}
328
329	// Add to PreferredModel model_id field
330	if preferredModelDef, exists := schema.Definitions["PreferredModel"]; exists {
331		if modelIDProp, exists := preferredModelDef.Properties["model_id"]; exists {
332			modelIDProp.Enum = modelIDs
333		}
334	}
335}
336
337// addAgentEnums adds agent ID enums to the schema
338func (g *SchemaGenerator) addAgentEnums(schema *JSONSchema) {
339	agentIDs := []any{
340		string(config.AgentCoder),
341		string(config.AgentTask),
342	}
343
344	if agentDef, exists := schema.Definitions["Agent"]; exists {
345		if idProp, exists := agentDef.Properties["id"]; exists {
346			idProp.Enum = agentIDs
347		}
348	}
349}
350
351// addToolEnums adds tool enums to the schema
352func (g *SchemaGenerator) addToolEnums(schema *JSONSchema) {
353	tools := []any{
354		"bash", "edit", "fetch", "glob", "grep", "ls", "sourcegraph", "view", "write", "agent",
355	}
356
357	if agentDef, exists := schema.Definitions["Agent"]; exists {
358		if allowedToolsProp, exists := agentDef.Properties["allowed_tools"]; exists {
359			if allowedToolsProp.Items != nil {
360				allowedToolsProp.Items.Enum = tools
361			}
362		}
363	}
364}
365
366// addMCPTypeEnums adds MCP type enums to the schema
367func (g *SchemaGenerator) addMCPTypeEnums(schema *JSONSchema) {
368	mcpTypes := []any{
369		string(config.MCPStdio),
370		string(config.MCPSse),
371	}
372
373	if mcpDef, exists := schema.Definitions["MCP"]; exists {
374		if typeProp, exists := mcpDef.Properties["type"]; exists {
375			typeProp.Enum = mcpTypes
376		}
377	}
378}
379
380// addModelTypeEnums adds model type enums to the schema
381func (g *SchemaGenerator) addModelTypeEnums(schema *JSONSchema) {
382	modelTypes := []any{
383		string(config.LargeModel),
384		string(config.SmallModel),
385	}
386
387	if agentDef, exists := schema.Definitions["Agent"]; exists {
388		if modelProp, exists := agentDef.Properties["model"]; exists {
389			modelProp.Enum = modelTypes
390		}
391	}
392}
393
394// addDefaultValues adds default values to the schema
395func (g *SchemaGenerator) addDefaultValues(schema *JSONSchema) {
396	// Add default context paths
397	if optionsDef, exists := schema.Definitions["Options"]; exists {
398		if contextPathsProp, exists := optionsDef.Properties["context_paths"]; exists {
399			contextPathsProp.Default = []any{
400				".github/copilot-instructions.md",
401				".cursorrules",
402				".cursor/rules/",
403				"CLAUDE.md",
404				"CLAUDE.local.md",
405				"GEMINI.md",
406				"gemini.md",
407				"crush.md",
408				"crush.local.md",
409				"Crush.md",
410				"Crush.local.md",
411				"CRUSH.md",
412				"CRUSH.local.md",
413			}
414		}
415		if dataDirProp, exists := optionsDef.Properties["data_directory"]; exists {
416			dataDirProp.Default = ".crush"
417		}
418		if debugProp, exists := optionsDef.Properties["debug"]; exists {
419			debugProp.Default = false
420		}
421		if debugLSPProp, exists := optionsDef.Properties["debug_lsp"]; exists {
422			debugLSPProp.Default = false
423		}
424		if disableAutoSummarizeProp, exists := optionsDef.Properties["disable_auto_summarize"]; exists {
425			disableAutoSummarizeProp.Default = false
426		}
427	}
428
429	// Add default MCP type
430	if mcpDef, exists := schema.Definitions["MCP"]; exists {
431		if typeProp, exists := mcpDef.Properties["type"]; exists {
432			typeProp.Default = string(config.MCPStdio)
433		}
434	}
435
436	// Add default TUI options
437	if tuiOptionsDef, exists := schema.Definitions["TUIOptions"]; exists {
438		if compactModeProp, exists := tuiOptionsDef.Properties["compact_mode"]; exists {
439			compactModeProp.Default = false
440		}
441	}
442
443	// Add default provider disabled
444	if providerConfigDef, exists := schema.Definitions["ProviderConfig"]; exists {
445		if disabledProp, exists := providerConfigDef.Properties["disabled"]; exists {
446			disabledProp.Default = false
447		}
448	}
449
450	// Add default agent disabled
451	if agentDef, exists := schema.Definitions["Agent"]; exists {
452		if disabledProp, exists := agentDef.Properties["disabled"]; exists {
453			disabledProp.Default = false
454		}
455	}
456
457	// Add default LSP disabled
458	if lspConfigDef, exists := schema.Definitions["LSPConfig"]; exists {
459		if disabledProp, exists := lspConfigDef.Properties["enabled"]; exists {
460			disabledProp.Default = true
461		}
462	}
463}
464
465// addCustomDescriptions adds custom descriptions to improve the schema
466func (g *SchemaGenerator) addCustomDescriptions(schema *JSONSchema) {
467	// Enhance main config descriptions
468	if schema.Properties != nil {
469		if modelsProp, exists := schema.Properties["models"]; exists {
470			modelsProp.Description = "Preferred model configurations for large and small model types"
471		}
472		if providersProp, exists := schema.Properties["providers"]; exists {
473			providersProp.Description = "LLM provider configurations"
474		}
475		if agentsProp, exists := schema.Properties["agents"]; exists {
476			agentsProp.Description = "Agent configurations for different tasks"
477		}
478		if mcpProp, exists := schema.Properties["mcp"]; exists {
479			mcpProp.Description = "Model Control Protocol server configurations"
480		}
481		if lspProp, exists := schema.Properties["lsp"]; exists {
482			lspProp.Description = "Language Server Protocol configurations"
483		}
484		if optionsProp, exists := schema.Properties["options"]; exists {
485			optionsProp.Description = "General application options and settings"
486		}
487	}
488
489	// Enhance specific field descriptions
490	if providerConfigDef, exists := schema.Definitions["ProviderConfig"]; exists {
491		if apiKeyProp, exists := providerConfigDef.Properties["api_key"]; exists {
492			apiKeyProp.Description = "API key for authenticating with the provider"
493		}
494		if baseURLProp, exists := providerConfigDef.Properties["base_url"]; exists {
495			baseURLProp.Description = "Base URL for the provider API (required for custom providers)"
496		}
497		if extraHeadersProp, exists := providerConfigDef.Properties["extra_headers"]; exists {
498			extraHeadersProp.Description = "Additional HTTP headers to send with requests"
499		}
500		if extraParamsProp, exists := providerConfigDef.Properties["extra_params"]; exists {
501			extraParamsProp.Description = "Additional provider-specific parameters"
502		}
503	}
504
505	if agentDef, exists := schema.Definitions["Agent"]; exists {
506		if allowedToolsProp, exists := agentDef.Properties["allowed_tools"]; exists {
507			allowedToolsProp.Description = "List of tools this agent is allowed to use (if nil, all tools are allowed)"
508		}
509		if allowedMCPProp, exists := agentDef.Properties["allowed_mcp"]; exists {
510			allowedMCPProp.Description = "Map of MCP servers this agent can use and their allowed tools"
511		}
512		if allowedLSPProp, exists := agentDef.Properties["allowed_lsp"]; exists {
513			allowedLSPProp.Description = "List of LSP servers this agent can use (if nil, all LSPs are allowed)"
514		}
515		if contextPathsProp, exists := agentDef.Properties["context_paths"]; exists {
516			contextPathsProp.Description = "Custom context paths for this agent (additive to global context paths)"
517		}
518	}
519
520	if mcpDef, exists := schema.Definitions["MCP"]; exists {
521		if commandProp, exists := mcpDef.Properties["command"]; exists {
522			commandProp.Description = "Command to execute for stdio MCP servers"
523		}
524		if urlProp, exists := mcpDef.Properties["url"]; exists {
525			urlProp.Description = "URL for SSE MCP servers"
526		}
527		if headersProp, exists := mcpDef.Properties["headers"]; exists {
528			headersProp.Description = "HTTP headers for SSE MCP servers"
529		}
530	}
531}