package generate import ( "bytes" "errors" "fmt" "go/format" "go/token" "os" "path/filepath" "sort" "strconv" "strings" "gitea.lclr.dev/AI/mcp-framework/manifest" ) var ErrGeneratedFilesOutdated = errors.New("generated files are not up to date") type Options struct { ProjectDir string ManifestPath string PackageDir string PackageName string Check bool } type Result struct { Root string Files []string } func Generate(options Options) (Result, error) { normalized, err := normalizeOptions(options) if err != nil { return Result{}, err } if _, err := manifest.Load(normalized.ManifestPath); err != nil { return Result{}, err } manifestContent, err := os.ReadFile(normalized.ManifestPath) if err != nil { return Result{}, fmt.Errorf("read manifest %s: %w", normalized.ManifestPath, err) } content, err := renderManifestLoader(normalized.PackageName, string(manifestContent)) if err != nil { return Result{}, err } files := []generatedFile{ { Path: filepath.Join(normalized.PackageDir, "manifest.go"), Content: content, Mode: 0o644, }, } written := make([]string, 0, len(files)) for _, file := range files { target := filepath.Join(normalized.ProjectDir, file.Path) if normalized.Check { current, err := os.ReadFile(target) if err != nil { if errors.Is(err, os.ErrNotExist) { return Result{}, fmt.Errorf("%w: %s", ErrGeneratedFilesOutdated, file.Path) } return Result{}, fmt.Errorf("read generated file %q: %w", target, err) } if !bytes.Equal(current, []byte(file.Content)) { return Result{}, fmt.Errorf("%w: %s", ErrGeneratedFilesOutdated, file.Path) } written = append(written, file.Path) continue } if err := writeGeneratedFile(target, file.Content, file.Mode); err != nil { return Result{}, err } written = append(written, file.Path) } sort.Strings(written) return Result{ Root: normalized.ProjectDir, Files: written, }, nil } type normalizedOptions struct { ProjectDir string ManifestPath string PackageDir string PackageName string Check bool } type generatedFile struct { Path string Content string Mode os.FileMode } func normalizeOptions(options Options) (normalizedOptions, error) { manifestPath := strings.TrimSpace(options.ManifestPath) projectDir := strings.TrimSpace(options.ProjectDir) if manifestPath == "" { baseDir := projectDir if baseDir == "" { wd, err := os.Getwd() if err != nil { return normalizedOptions{}, fmt.Errorf("resolve working directory: %w", err) } baseDir = wd } manifestPath = filepath.Join(baseDir, manifest.DefaultFile) } else if !filepath.IsAbs(manifestPath) { baseDir := projectDir if baseDir == "" { wd, err := os.Getwd() if err != nil { return normalizedOptions{}, fmt.Errorf("resolve working directory: %w", err) } baseDir = wd } manifestPath = filepath.Join(baseDir, manifestPath) } resolvedManifest, err := filepath.Abs(manifestPath) if err != nil { return normalizedOptions{}, fmt.Errorf("resolve manifest path %q: %w", manifestPath, err) } if projectDir == "" { projectDir = filepath.Dir(resolvedManifest) } resolvedProjectDir, err := filepath.Abs(projectDir) if err != nil { return normalizedOptions{}, fmt.Errorf("resolve project dir %q: %w", projectDir, err) } packageDir := filepath.Clean(strings.TrimSpace(options.PackageDir)) if packageDir == "." || packageDir == "" { packageDir = "mcpgen" } if filepath.IsAbs(packageDir) || packageDir == ".." || strings.HasPrefix(packageDir, ".."+string(filepath.Separator)) { return normalizedOptions{}, fmt.Errorf("package dir %q must be relative to the project", options.PackageDir) } packageName := strings.TrimSpace(options.PackageName) if packageName == "" { packageName = filepath.Base(packageDir) } if !token.IsIdentifier(packageName) { return normalizedOptions{}, fmt.Errorf("package name %q is not a valid Go identifier", packageName) } return normalizedOptions{ ProjectDir: resolvedProjectDir, ManifestPath: resolvedManifest, PackageDir: packageDir, PackageName: packageName, Check: options.Check, }, nil } func renderManifestLoader(packageName, manifestContent string) (string, error) { source := fmt.Sprintf(`// Code generated by mcp-framework generate. DO NOT EDIT. package %s import fwmanifest "gitea.lclr.dev/AI/mcp-framework/manifest" const embeddedManifest = %s func LoadManifest(startDir string) (fwmanifest.File, string, error) { return fwmanifest.LoadDefaultOrEmbedded(startDir, embeddedManifest) } `, packageName, strconv.Quote(manifestContent)) formatted, err := format.Source([]byte(source)) if err != nil { return "", fmt.Errorf("format generated manifest loader: %w", err) } return string(formatted), nil } func writeGeneratedFile(path, content string, mode os.FileMode) error { current, err := os.ReadFile(path) if err == nil && bytes.Equal(current, []byte(content)) { return nil } if err != nil && !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("read generated file %q: %w", path, err) } dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0o755); err != nil { return fmt.Errorf("create generated directory %q: %w", dir, err) } if mode == 0 { mode = 0o644 } if err := os.WriteFile(path, []byte(content), mode); err != nil { return fmt.Errorf("write generated file %q: %w", path, err) } return nil }