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}