package main
import (
"flag"
"fmt"
"go/format"
"maps"
"os"
"path/filepath"
"slices"
"strings"
"github.com/navidrome/navidrome/plugins/cmd/ndpgen/internal"
)
type config struct {
inputDir string
outputDir string
goOutputDir string
rustOutputDir string
pkgName string
hostOnly bool
hostWrappers bool
capabilityOnly bool
schemasOnly bool
pdkOnly bool
sharedTypes bool
sharedDir string
generateGoClient bool
generateRsClient bool
verbose bool
dryRun bool
}
func main() {
cfg, err := parseConfig()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if cfg.schemasOnly {
if err := runSchemaGeneration(cfg); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
return
}
if cfg.sharedTypes {
if err := runSharedTypesGeneration(cfg); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
return
}
if cfg.pdkOnly {
if err := runPDKGeneration(cfg); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
return
}
if cfg.capabilityOnly {
if err := runCapabilityGeneration(cfg); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
return
}
if cfg.hostWrappers {
if err := runHostWrapperGeneration(cfg); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
return
}
services, err := parseServices(cfg)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if len(services) == 0 {
return
}
if err := generateAllCode(cfg, services); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
func runCapabilityGeneration(cfg *config) error {
capabilities, err := parseCapabilities(cfg)
if err != nil {
return err
}
if len(capabilities) == 0 {
if cfg.verbose {
fmt.Println("No capabilities found")
}
return nil
}
return generateCapabilityCode(cfg, capabilities)
}
func runSchemaGeneration(cfg *config) error {
capabilities, err := parseCapabilities(cfg)
if err != nil {
return err
}
if len(capabilities) == 0 {
if cfg.verbose {
fmt.Println("No capabilities found")
}
return nil
}
return generateSchemas(cfg, capabilities)
}
func writeGenerated(dir, name string, content []byte, dryRun, verbose bool) error {
path := filepath.Join(dir, name)
if dryRun {
fmt.Printf("=== %s ===\n%s\n", path, content)
return nil
}
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("creating output directory %s: %w", dir, err)
}
if err := os.WriteFile(path, content, 0600); err != nil {
return fmt.Errorf("writing %s: %w", path, err)
}
if verbose {
fmt.Printf("Generated: %s\n", path)
}
return nil
}
func runSharedTypesGeneration(cfg *config) error {
structs, err := internal.LoadSharedTypes(cfg.inputDir)
if err != nil {
return err
}
if len(structs) == 0 {
return nil
}
list := slices.Collect(maps.Values(structs))
if cfg.generateGoClient {
code, err := internal.GenerateSharedTypesGo(list, "types")
if err != nil {
return fmt.Errorf("generating Go types: %w", err)
}
formatted, err := format.Source(code)
if err != nil {
return fmt.Errorf("formatting Go types: %w\n%s", err, code)
}
dir := filepath.Join(cfg.outputDir, "go", "types")
if err := writeGenerated(dir, "types.go", formatted, cfg.dryRun, cfg.verbose); err != nil {
return err
}
}
if cfg.generateRsClient {
if err := generateSharedTypesRust(list, cfg); err != nil {
return err
}
}
return nil
}
func generateSharedTypesRust(structs []internal.StructDef, cfg *config) error {
code, err := internal.GenerateSharedTypesRust(structs)
if err != nil {
return fmt.Errorf("generating Rust types: %w", err)
}
dir := filepath.Join(cfg.outputDir, "rust", "nd-pdk-types", "src")
return writeGenerated(dir, "lib.rs", code, cfg.dryRun, cfg.verbose)
}
func runPDKGeneration(cfg *config) error {
outputDir := filepath.Join(cfg.outputDir, "go", "pdk")
return generatePDKPackageWithParsing(outputDir, cfg.dryRun, cfg.verbose)
}
func generatePDKPackageWithParsing(outputDir string, dryRun, verbose bool) error {
if verbose {
fmt.Println("Parsing extism/go-pdk to extract exported symbols...")
}
symbols, err := internal.ParseExtismPDK()
if err != nil {
return fmt.Errorf("parsing extism/go-pdk: %w", err)
}
if verbose {
fmt.Printf("Found %d types, %d constants, %d functions\n",
len(symbols.Types), len(symbols.Consts), len(symbols.Functions))
for _, t := range symbols.Types {
fmt.Printf(" Type %s: %d methods, %d fields\n", t.Name, len(t.Methods), len(t.Fields))
for _, m := range t.Methods {
fmt.Printf(" Method: %s (receiver: %s)\n", m.Name, m.Receiver)
}
}
fmt.Printf("Generating PDK abstraction layer to: %s\n", outputDir)
}
pdkCode, err := internal.GeneratePDKGo(symbols)
if err != nil {
return fmt.Errorf("generating pdk.go: %w", err)
}
formatted, err := format.Source(pdkCode)
if err != nil {
return fmt.Errorf("formatting pdk.go: %w\nRaw code:\n%s", err, pdkCode)
}
pdkFile := filepath.Join(outputDir, "pdk.go")
if dryRun {
fmt.Printf("=== %s ===\n%s\n", pdkFile, formatted)
} else {
if err := os.MkdirAll(outputDir, 0755); err != nil {
return fmt.Errorf("creating output directory: %w", err)
}
if err := os.WriteFile(pdkFile, formatted, 0600); err != nil {
return fmt.Errorf("writing pdk.go: %w", err)
}
if verbose {
fmt.Printf("Generated: %s\n", pdkFile)
}
}
typesStubCode, err := internal.GeneratePDKTypesStub(symbols)
if err != nil {
return fmt.Errorf("generating types_stub.go: %w", err)
}
formattedTypesStub, err := format.Source(typesStubCode)
if err != nil {
return fmt.Errorf("formatting types_stub.go: %w\nRaw code:\n%s", err, typesStubCode)
}
typesStubFile := filepath.Join(outputDir, "types_stub.go")
if dryRun {
fmt.Printf("=== %s ===\n%s\n", typesStubFile, formattedTypesStub)
} else {
if err := os.WriteFile(typesStubFile, formattedTypesStub, 0600); err != nil {
return fmt.Errorf("writing types_stub.go: %w", err)
}
if verbose {
fmt.Printf("Generated: %s\n", typesStubFile)
}
}
stubCode, err := internal.GeneratePDKGoStub(symbols)
if err != nil {
return fmt.Errorf("generating pdk_stub.go: %w", err)
}
formattedStub, err := format.Source(stubCode)
if err != nil {
return fmt.Errorf("formatting pdk_stub.go: %w\nRaw code:\n%s", err, stubCode)
}
stubFile := filepath.Join(outputDir, "pdk_stub.go")
if dryRun {
fmt.Printf("=== %s ===\n%s\n", stubFile, formattedStub)
} else {
if err := os.WriteFile(stubFile, formattedStub, 0600); err != nil {
return fmt.Errorf("writing pdk_stub.go: %w", err)
}
if verbose {
fmt.Printf("Generated: %s\n", stubFile)
}
}
return nil
}
func generatePDKPackage(outputDir string, dryRun, verbose bool) error {
return generatePDKPackageWithParsing(outputDir, dryRun, verbose)
}
func runHostWrapperGeneration(cfg *config) error {
services, err := parseServices(cfg)
if err != nil {
return err
}
if len(services) == 0 {
if cfg.verbose {
fmt.Println("No host services found")
}
return nil
}
for _, svc := range services {
if err := generateHostWrapperCode(svc, cfg.inputDir, cfg.pkgName, cfg.dryRun, cfg.verbose); err != nil {
return fmt.Errorf("generating host wrapper for %s: %w", svc.Name, err)
}
}
return nil
}
func parseConfig() (*config, error) {
var (
inputDir = flag.String("input", ".", "Input directory containing Go source files")
outputDir = flag.String("output", "", "Base output directory for generated files (default: same as input)")
pkgName = flag.String("package", "", "Output package name for Go (default: host for host-only, auto for capabilities)")
hostOnly = flag.Bool("host-only", false, "Generate only host function wrappers")
hostWrappers = flag.Bool("host-wrappers", false, "Generate host wrappers (used by Navidrome server, output to input directory)")
capabilityOnly = flag.Bool("capability-only", false, "Generate only capability export wrappers")
schemasOnly = flag.Bool("schemas", false, "Generate XTP YAML schemas from capabilities (output to input directory)")
pdkOnly = flag.Bool("extism-pdk", false, "Generate PDK abstraction layer by parsing extism/go-pdk")
sharedTypes = flag.Bool("shared-types", false, "Generate the shared types package")
shared = flag.String("shared", "", "Directory of shared types to load as a registry")
goClient = flag.Bool("go", false, "Generate Go client wrappers")
rsClient = flag.Bool("rust", false, "Generate Rust client wrappers")
verbose = flag.Bool("v", false, "Verbose output")
dryRun = flag.Bool("dry-run", false, "Preview generated code without writing files")
)
flag.Parse()
modeCount := 0
if *hostOnly {
modeCount++
}
if *hostWrappers {
modeCount++
}
if *capabilityOnly {
modeCount++
}
if *schemasOnly {
modeCount++
}
if *pdkOnly {
modeCount++
}
if *sharedTypes {
modeCount++
}
if modeCount == 0 {
*hostOnly = true
}
if modeCount > 1 {
return nil, fmt.Errorf("cannot specify multiple modes (-host-only, -host-wrappers, -capability-only, -schemas, -extism-pdk, -shared-types)")
}
if *outputDir == "" {
*outputDir = *inputDir
}
if *pkgName == "" {
if *hostOnly {
*pkgName = "host"
}
}
absInput, err := filepath.Abs(*inputDir)
if err != nil {
return nil, fmt.Errorf("resolving input path: %w", err)
}
absOutput, err := filepath.Abs(*outputDir)
if err != nil {
return nil, fmt.Errorf("resolving output path: %w", err)
}
absShared := ""
if *shared != "" {
absShared, err = filepath.Abs(*shared)
if err != nil {
return nil, fmt.Errorf("resolving shared path: %w", err)
}
}
absGoOutput := filepath.Join(absOutput, "go", "host")
absRustOutput := filepath.Join(absOutput, "rust", "nd-pdk-host")
anyLangFlag := *goClient || *rsClient
return &config{
inputDir: absInput,
outputDir: absOutput,
goOutputDir: absGoOutput,
rustOutputDir: absRustOutput,
pkgName: *pkgName,
hostOnly: *hostOnly,
hostWrappers: *hostWrappers,
capabilityOnly: *capabilityOnly,
schemasOnly: *schemasOnly,
pdkOnly: *pdkOnly,
sharedTypes: *sharedTypes,
sharedDir: absShared,
generateGoClient: *goClient || !anyLangFlag,
generateRsClient: *rsClient,
verbose: *verbose,
dryRun: *dryRun,
}, nil
}
func parseServices(cfg *config) ([]internal.Service, error) {
if cfg.verbose {
fmt.Printf("Input directory: %s\n", cfg.inputDir)
fmt.Printf("Base output directory: %s\n", cfg.outputDir)
if cfg.generateGoClient {
fmt.Printf("Go output directory: %s\n", cfg.goOutputDir)
}
if cfg.generateRsClient {
fmt.Printf("Rust output directory: %s\n", cfg.rustOutputDir)
}
fmt.Printf("Package name: %s\n", cfg.pkgName)
fmt.Printf("Host-only mode: %v\n", cfg.hostOnly)
fmt.Printf("Generate Go client code: %v\n", cfg.generateGoClient)
fmt.Printf("Generate Rust client code: %v\n", cfg.generateRsClient)
}
shared, err := internal.LoadSharedTypes(cfg.sharedDir)
if err != nil {
return nil, fmt.Errorf("loading shared types: %w", err)
}
services, err := internal.ParseDirectoryWithShared(cfg.inputDir, shared)
if err != nil {
return nil, fmt.Errorf("parsing source files: %w", err)
}
if len(services) == 0 {
if cfg.verbose {
fmt.Println("No host services found")
}
return nil, nil
}
if cfg.verbose {
fmt.Printf("Found %d host service(s)\n", len(services))
for _, svc := range services {
fmt.Printf(" - %s (%d methods)\n", svc.Name, len(svc.Methods))
}
}
return services, nil
}
func parseCapabilities(cfg *config) ([]internal.Capability, error) {
if cfg.verbose {
fmt.Printf("Input directory: %s\n", cfg.inputDir)
fmt.Printf("Base output directory: %s\n", cfg.outputDir)
fmt.Printf("Capability-only mode: %v\n", cfg.capabilityOnly)
}
shared, err := internal.LoadSharedTypes(cfg.sharedDir)
if err != nil {
return nil, fmt.Errorf("loading shared types: %w", err)
}
capabilities, err := internal.ParseCapabilitiesWithShared(cfg.inputDir, shared)
if err != nil {
return nil, fmt.Errorf("parsing capability files: %w", err)
}
if len(capabilities) == 0 {
return nil, nil
}
if cfg.verbose {
fmt.Printf("Found %d capability(ies)\n", len(capabilities))
for _, cap := range capabilities {
fmt.Printf(" - %s (%d exports, required=%v)\n", cap.Name, len(cap.Methods), cap.Required)
}
}
return capabilities, nil
}
func generateCapabilityCode(cfg *config, capabilities []internal.Capability) error {
for _, cap := range capabilities {
outputDir := filepath.Join(cfg.outputDir, "go", cap.Name)
if err := generateCapabilityGoCode(cap, outputDir, cfg.dryRun, cfg.verbose); err != nil {
return fmt.Errorf("generating Go capability code for %s: %w", cap.Name, err)
}
}
if cfg.generateRsClient {
rustOutputDir := filepath.Join(cfg.outputDir, "rust", "nd-pdk-capabilities", "src")
if err := generateCapabilityRustCode(capabilities, rustOutputDir, cfg.dryRun, cfg.verbose); err != nil {
return fmt.Errorf("generating Rust capability code: %w", err)
}
}
return nil
}
func generateCapabilityGoCode(cap internal.Capability, outputDir string, dryRun, verbose bool) error {
pkgName := cap.Name
code, err := internal.GenerateCapabilityGo(cap, pkgName)
if err != nil {
return fmt.Errorf("generating code: %w", err)
}
formatted, err := format.Source(code)
if err != nil {
return fmt.Errorf("formatting code: %w\nRaw code:\n%s", err, code)
}
mainFile := filepath.Join(outputDir, cap.Name+".go")
if dryRun {
fmt.Printf("=== %s ===\n%s\n", mainFile, formatted)
} else {
if err := os.MkdirAll(outputDir, 0755); err != nil {
return fmt.Errorf("creating output directory: %w", err)
}
if err := os.WriteFile(mainFile, formatted, 0600); err != nil {
return fmt.Errorf("writing file: %w", err)
}
if verbose {
fmt.Printf("Generated capability code: %s\n", mainFile)
}
}
stubCode, err := internal.GenerateCapabilityGoStub(cap, pkgName)
if err != nil {
return fmt.Errorf("generating stub code: %w", err)
}
formattedStub, err := format.Source(stubCode)
if err != nil {
return fmt.Errorf("formatting stub code: %w\nRaw code:\n%s", err, stubCode)
}
stubFile := filepath.Join(outputDir, cap.Name+"_stub.go")
if dryRun {
fmt.Printf("=== %s ===\n%s\n", stubFile, formattedStub)
} else {
if err := os.WriteFile(stubFile, formattedStub, 0600); err != nil {
return fmt.Errorf("writing stub file: %w", err)
}
if verbose {
fmt.Printf("Generated capability stub: %s\n", stubFile)
}
}
return nil
}
func generateCapabilityRustCode(capabilities []internal.Capability, outputDir string, dryRun, verbose bool) error {
for _, cap := range capabilities {
code, err := internal.GenerateCapabilityRust(cap)
if err != nil {
return fmt.Errorf("generating Rust code for %s: %w", cap.Name, err)
}
fileName := internal.ToSnakeCase(cap.Name) + ".rs"
filePath := filepath.Join(outputDir, fileName)
if dryRun {
fmt.Printf("=== %s ===\n%s\n", filePath, code)
} else {
if err := os.MkdirAll(outputDir, 0755); err != nil {
return fmt.Errorf("creating output directory: %w", err)
}
if err := os.WriteFile(filePath, code, 0600); err != nil {
return fmt.Errorf("writing file %s: %w", filePath, err)
}
if verbose {
fmt.Printf("Generated Rust capability code: %s\n", filePath)
}
}
}
libCode, err := internal.GenerateCapabilityRustLib(capabilities)
if err != nil {
return fmt.Errorf("generating lib.rs: %w", err)
}
libPath := filepath.Join(outputDir, "lib.rs")
if dryRun {
fmt.Printf("=== %s ===\n%s\n", libPath, libCode)
} else {
if err := os.WriteFile(libPath, libCode, 0600); err != nil {
return fmt.Errorf("writing lib.rs: %w", err)
}
if verbose {
fmt.Printf("Generated Rust lib.rs: %s\n", libPath)
}
}
return nil
}
func generateAllCode(cfg *config, services []internal.Service) error {
for _, svc := range services {
if cfg.generateGoClient {
if err := generateGoClientCode(svc, cfg.goOutputDir, cfg.pkgName, cfg.dryRun, cfg.verbose); err != nil {
return fmt.Errorf("generating Go client code for %s: %w", svc.Name, err)
}
}
if cfg.generateRsClient {
if err := generateRustClientCode(svc, cfg.rustOutputDir, cfg.dryRun, cfg.verbose); err != nil {
return fmt.Errorf("generating Rust client code for %s: %w", svc.Name, err)
}
}
}
if cfg.generateRsClient && len(services) > 0 {
if err := generateRustLibFile(services, cfg.rustOutputDir, cfg.dryRun, cfg.verbose); err != nil {
return fmt.Errorf("generating Rust lib.rs: %w", err)
}
}
if cfg.generateGoClient && len(services) > 0 {
if err := generateGoDocFile(services, cfg.goOutputDir, cfg.pkgName, cfg.dryRun, cfg.verbose); err != nil {
return fmt.Errorf("generating Go doc.go: %w", err)
}
if err := generateGoModFile(cfg.goOutputDir, cfg.dryRun, cfg.verbose); err != nil {
return fmt.Errorf("generating Go go.mod: %w", err)
}
pdkDir := filepath.Join(filepath.Dir(cfg.goOutputDir), "pdk")
if err := generatePDKPackage(pdkDir, cfg.dryRun, cfg.verbose); err != nil {
return fmt.Errorf("generating PDK package: %w", err)
}
}
return nil
}
func generateHostWrapperCode(svc internal.Service, outputDir, pkgName string, dryRun, verbose bool) error {
code, err := internal.GenerateHost(svc, pkgName)
if err != nil {
return fmt.Errorf("generating code: %w", err)
}
formatted, err := format.Source(code)
if err != nil {
return fmt.Errorf("formatting code: %w\nRaw code:\n%s", err, code)
}
hostFile := filepath.Join(outputDir, strings.ToLower(svc.Name)+"_gen.go")
if dryRun {
fmt.Printf("=== %s ===\n%s\n", hostFile, formatted)
} else {
if err := os.WriteFile(hostFile, formatted, 0600); err != nil {
return fmt.Errorf("writing file: %w", err)
}
if verbose {
fmt.Printf("Generated host wrapper: %s\n", hostFile)
}
}
return nil
}
func generateGoClientCode(svc internal.Service, outputDir, pkgName string, dryRun, verbose bool) error {
code, err := internal.GenerateClientGo(svc, pkgName)
if err != nil {
return fmt.Errorf("generating code: %w", err)
}
formatted, err := format.Source(code)
if err != nil {
return fmt.Errorf("formatting code: %w\nRaw code:\n%s", err, code)
}
clientFile := filepath.Join(outputDir, "nd_host_"+strings.ToLower(svc.Name)+".go")
if dryRun {
fmt.Printf("=== %s ===\n%s\n", clientFile, formatted)
} else {
if err := os.MkdirAll(outputDir, 0755); err != nil {
return fmt.Errorf("creating output directory: %w", err)
}
if err := os.WriteFile(clientFile, formatted, 0600); err != nil {
return fmt.Errorf("writing file: %w", err)
}
if verbose {
fmt.Printf("Generated Go client code: %s\n", clientFile)
}
}
return generateGoClientStubCode(svc, outputDir, pkgName, dryRun, verbose)
}
func generateGoClientStubCode(svc internal.Service, outputDir, pkgName string, dryRun, verbose bool) error {
code, err := internal.GenerateClientGoStub(svc, pkgName)
if err != nil {
return fmt.Errorf("generating stub code: %w", err)
}
formatted, err := format.Source(code)
if err != nil {
return fmt.Errorf("formatting stub code: %w\nRaw code:\n%s", err, code)
}
stubFile := filepath.Join(outputDir, "nd_host_"+strings.ToLower(svc.Name)+"_stub.go")
if dryRun {
fmt.Printf("=== %s ===\n%s\n", stubFile, formatted)
return nil
}
if err := os.MkdirAll(outputDir, 0755); err != nil {
return fmt.Errorf("creating output directory: %w", err)
}
if err := os.WriteFile(stubFile, formatted, 0600); err != nil {
return fmt.Errorf("writing stub file: %w", err)
}
if verbose {
fmt.Printf("Generated Go client stub: %s\n", stubFile)
}
return nil
}
func generateRustClientCode(svc internal.Service, outputDir string, dryRun, verbose bool) error {
code, err := internal.GenerateClientRust(svc)
if err != nil {
return fmt.Errorf("generating code: %w", err)
}
srcDir := filepath.Join(outputDir, "src")
clientFile := filepath.Join(srcDir, "nd_host_"+strings.ToLower(svc.Name)+".rs")
if dryRun {
fmt.Printf("=== %s ===\n%s\n", clientFile, code)
return nil
}
if err := os.MkdirAll(srcDir, 0755); err != nil {
return fmt.Errorf("creating rust src directory: %w", err)
}
if err := os.WriteFile(clientFile, code, 0600); err != nil {
return fmt.Errorf("writing file: %w", err)
}
if verbose {
fmt.Printf("Generated Rust client code: %s\n", clientFile)
}
return nil
}
func generateRustLibFile(services []internal.Service, outputDir string, dryRun, verbose bool) error {
code, err := internal.GenerateRustLib(services)
if err != nil {
return fmt.Errorf("generating lib.rs: %w", err)
}
srcDir := filepath.Join(outputDir, "src")
libFile := filepath.Join(srcDir, "lib.rs")
if dryRun {
fmt.Printf("=== %s ===\n%s\n", libFile, code)
return nil
}
if err := os.MkdirAll(srcDir, 0755); err != nil {
return fmt.Errorf("creating rust src directory: %w", err)
}
if err := os.WriteFile(libFile, code, 0600); err != nil {
return fmt.Errorf("writing file: %w", err)
}
if verbose {
fmt.Printf("Generated Rust lib.rs: %s\n", libFile)
}
return nil
}
func generateGoDocFile(services []internal.Service, outputDir, pkgName string, dryRun, verbose bool) error {
code, err := internal.GenerateGoDoc(services, pkgName)
if err != nil {
return fmt.Errorf("generating doc.go: %w", err)
}
formatted, err := format.Source(code)
if err != nil {
return fmt.Errorf("formatting doc.go: %w\nRaw code:\n%s", err, code)
}
docFile := filepath.Join(outputDir, "doc.go")
if dryRun {
fmt.Printf("=== %s ===\n%s\n", docFile, formatted)
return nil
}
if err := os.MkdirAll(outputDir, 0755); err != nil {
return fmt.Errorf("creating output directory: %w", err)
}
if err := os.WriteFile(docFile, formatted, 0600); err != nil {
return fmt.Errorf("writing file: %w", err)
}
if verbose {
fmt.Printf("Generated Go doc.go: %s\n", docFile)
}
return nil
}
func generateGoModFile(outputDir string, dryRun, verbose bool) error {
code, err := internal.GenerateGoMod()
if err != nil {
return fmt.Errorf("generating go.mod: %w", err)
}
parentDir := filepath.Dir(outputDir)
modFile := filepath.Join(parentDir, "go.mod")
if dryRun {
fmt.Printf("=== %s ===\n%s\n", modFile, code)
return nil
}
if err := os.MkdirAll(parentDir, 0755); err != nil {
return fmt.Errorf("creating output directory: %w", err)
}
if err := os.WriteFile(modFile, code, 0600); err != nil {
return fmt.Errorf("writing file: %w", err)
}
if verbose {
fmt.Printf("Generated Go go.mod: %s\n", modFile)
}
return nil
}
func generateSchemas(cfg *config, capabilities []internal.Capability) error {
for _, cap := range capabilities {
if err := generateSchemaFile(cap, cfg.inputDir, cfg.dryRun, cfg.verbose); err != nil {
return fmt.Errorf("generating schema for %s: %w", cap.Name, err)
}
}
return nil
}
func generateSchemaFile(cap internal.Capability, outputDir string, dryRun, verbose bool) error {
schema, err := internal.GenerateSchema(cap)
if err != nil {
return fmt.Errorf("generating schema: %w", err)
}
if err := internal.ValidateXTPSchema(schema); err != nil {
fmt.Fprintf(os.Stderr, "Warning: Schema validation for %s:\n%s\n", cap.Name, err)
}
schemaFile := filepath.Join(outputDir, cap.SourceFile+".yaml")
if dryRun {
fmt.Printf("=== %s ===\n%s\n", schemaFile, schema)
return nil
}
if err := os.WriteFile(schemaFile, schema, 0600); err != nil {
return fmt.Errorf("writing file: %w", err)
}
if verbose {
fmt.Printf("Generated XTP schema: %s\n", schemaFile)
}
return nil
}