2026-05-02 09:46:47 +00:00
|
|
|
package generate
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"go/format"
|
|
|
|
|
"go/token"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"sort"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
|
2026-05-05 10:23:14 +00:00
|
|
|
"forge.lclr.dev/AI/mcp-framework/manifest"
|
2026-05-02 09:46:47 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-02 09:57:44 +00:00
|
|
|
manifestFile, err := manifest.Load(normalized.ManifestPath)
|
|
|
|
|
if err != nil {
|
2026-05-02 09:46:47 +00:00
|
|
|
return Result{}, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
manifestContent, err := os.ReadFile(normalized.ManifestPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return Result{}, fmt.Errorf("read manifest %s: %w", normalized.ManifestPath, err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-02 09:57:44 +00:00
|
|
|
manifestLoader, err := renderManifestLoader(normalized.PackageName, string(manifestContent))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return Result{}, err
|
|
|
|
|
}
|
|
|
|
|
metadata, err := renderMetadata(normalized.PackageName, manifestFile)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return Result{}, err
|
|
|
|
|
}
|
|
|
|
|
update, err := renderUpdate(normalized.PackageName)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return Result{}, err
|
|
|
|
|
}
|
|
|
|
|
secretstore, err := renderSecretStore(normalized.PackageName)
|
2026-05-02 09:46:47 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return Result{}, err
|
|
|
|
|
}
|
2026-05-02 10:02:23 +00:00
|
|
|
config, err := renderConfig(normalized.PackageName, manifestFile.Config.Fields)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return Result{}, err
|
|
|
|
|
}
|
2026-05-02 09:46:47 +00:00
|
|
|
|
|
|
|
|
files := []generatedFile{
|
|
|
|
|
{
|
|
|
|
|
Path: filepath.Join(normalized.PackageDir, "manifest.go"),
|
2026-05-02 09:57:44 +00:00
|
|
|
Content: manifestLoader,
|
|
|
|
|
Mode: 0o644,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Path: filepath.Join(normalized.PackageDir, "metadata.go"),
|
|
|
|
|
Content: metadata,
|
|
|
|
|
Mode: 0o644,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Path: filepath.Join(normalized.PackageDir, "update.go"),
|
|
|
|
|
Content: update,
|
|
|
|
|
Mode: 0o644,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Path: filepath.Join(normalized.PackageDir, "secretstore.go"),
|
|
|
|
|
Content: secretstore,
|
2026-05-02 09:46:47 +00:00
|
|
|
Mode: 0o644,
|
|
|
|
|
},
|
|
|
|
|
}
|
2026-05-02 10:02:23 +00:00
|
|
|
if strings.TrimSpace(config) != "" {
|
|
|
|
|
files = append(files, generatedFile{
|
|
|
|
|
Path: filepath.Join(normalized.PackageDir, "config.go"),
|
|
|
|
|
Content: config,
|
|
|
|
|
Mode: 0o644,
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-05-02 09:46:47 +00:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2026-05-05 10:23:14 +00:00
|
|
|
import fwmanifest "forge.lclr.dev/AI/mcp-framework/manifest"
|
2026-05-02 09:46:47 +00:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-02 09:57:44 +00:00
|
|
|
func renderMetadata(packageName string, manifestFile manifest.File) (string, error) {
|
|
|
|
|
bootstrapInfo := manifestFile.BootstrapInfo()
|
|
|
|
|
|
|
|
|
|
source := fmt.Sprintf(`// Code generated by mcp-framework generate. DO NOT EDIT.
|
|
|
|
|
|
|
|
|
|
package %s
|
|
|
|
|
|
2026-05-05 10:23:14 +00:00
|
|
|
import fwmanifest "forge.lclr.dev/AI/mcp-framework/manifest"
|
2026-05-02 09:57:44 +00:00
|
|
|
|
|
|
|
|
const BinaryName = %s
|
|
|
|
|
const DefaultDescription = %s
|
|
|
|
|
const DocsURL = %s
|
|
|
|
|
|
|
|
|
|
func BootstrapInfo(startDir string) (fwmanifest.BootstrapMetadata, string, error) {
|
|
|
|
|
manifestFile, source, err := LoadManifest(startDir)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fwmanifest.BootstrapMetadata{}, "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return manifestFile.BootstrapInfo(), source, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func ScaffoldInfo(startDir string) (fwmanifest.ScaffoldMetadata, string, error) {
|
|
|
|
|
manifestFile, source, err := LoadManifest(startDir)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fwmanifest.ScaffoldMetadata{}, "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return manifestFile.ScaffoldInfo(), source, nil
|
|
|
|
|
}
|
|
|
|
|
`, packageName, strconv.Quote(manifestFile.BinaryName), strconv.Quote(bootstrapInfo.Description), strconv.Quote(manifestFile.DocsURL))
|
|
|
|
|
|
|
|
|
|
return formatGenerated("metadata", source)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func renderUpdate(packageName string) (string, error) {
|
|
|
|
|
source := fmt.Sprintf(`// Code generated by mcp-framework generate. DO NOT EDIT.
|
|
|
|
|
|
|
|
|
|
package %s
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"flag"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"strings"
|
|
|
|
|
|
2026-05-05 10:23:14 +00:00
|
|
|
fwupdate "forge.lclr.dev/AI/mcp-framework/update"
|
2026-05-02 09:57:44 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func UpdateOptions(version string, stdout io.Writer) (fwupdate.Options, error) {
|
|
|
|
|
return UpdateOptionsFrom(".", version, stdout)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func UpdateOptionsFrom(startDir string, version string, stdout io.Writer) (fwupdate.Options, error) {
|
|
|
|
|
manifestFile, _, err := LoadManifest(startDir)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fwupdate.Options{}, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
binaryName := strings.TrimSpace(manifestFile.BinaryName)
|
|
|
|
|
if binaryName == "" {
|
|
|
|
|
binaryName = BinaryName
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return fwupdate.Options{
|
|
|
|
|
CurrentVersion: version,
|
|
|
|
|
Stdout: stdout,
|
|
|
|
|
BinaryName: binaryName,
|
|
|
|
|
ReleaseSource: manifestFile.Update.ReleaseSource(),
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func RunUpdate(ctx context.Context, args []string, version string, stdout io.Writer) error {
|
|
|
|
|
return RunUpdateFrom(ctx, args, ".", version, stdout)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func RunUpdateFrom(ctx context.Context, args []string, startDir string, version string, stdout io.Writer) error {
|
|
|
|
|
fs := flag.NewFlagSet("update", flag.ContinueOnError)
|
|
|
|
|
fs.SetOutput(io.Discard)
|
|
|
|
|
|
|
|
|
|
if err := fs.Parse(args); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if fs.NArg() != 0 {
|
|
|
|
|
return fmt.Errorf("update does not accept positional arguments: %%s", strings.Join(fs.Args(), ", "))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
options, err := UpdateOptionsFrom(startDir, version, stdout)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return fwupdate.Run(ctx, options)
|
|
|
|
|
}
|
|
|
|
|
`, packageName)
|
|
|
|
|
|
|
|
|
|
return formatGenerated("update", source)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func renderSecretStore(packageName string) (string, error) {
|
|
|
|
|
source := fmt.Sprintf(`// Code generated by mcp-framework generate. DO NOT EDIT.
|
|
|
|
|
|
|
|
|
|
package %s
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strings"
|
|
|
|
|
|
2026-05-05 10:23:14 +00:00
|
|
|
fwsecretstore "forge.lclr.dev/AI/mcp-framework/secretstore"
|
2026-05-02 09:57:44 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type SecretStoreOptions struct {
|
2026-05-02 13:04:29 +00:00
|
|
|
ServiceName string
|
|
|
|
|
LookupEnv func(string) (string, bool)
|
|
|
|
|
KWalletAppID string
|
|
|
|
|
KWalletFolder string
|
|
|
|
|
BitwardenCommand string
|
|
|
|
|
BitwardenDebug bool
|
|
|
|
|
DisableBitwardenCache bool
|
|
|
|
|
Shell string
|
|
|
|
|
ExecutableResolver fwsecretstore.ExecutableResolver
|
2026-05-02 09:57:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func OpenSecretStore(options SecretStoreOptions) (fwsecretstore.Store, error) {
|
|
|
|
|
return fwsecretstore.OpenFromManifest(secretStoreOpenOptions(options))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func DescribeSecretRuntime(options SecretStoreOptions) (fwsecretstore.RuntimeDescription, error) {
|
|
|
|
|
return fwsecretstore.DescribeRuntime(secretStoreDescribeOptions(options))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func PreflightSecretStore(options SecretStoreOptions) (fwsecretstore.PreflightReport, error) {
|
|
|
|
|
return fwsecretstore.PreflightFromManifest(secretStoreDescribeOptions(options))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func secretStoreOpenOptions(options SecretStoreOptions) fwsecretstore.OpenFromManifestOptions {
|
|
|
|
|
return fwsecretstore.OpenFromManifestOptions{
|
|
|
|
|
ServiceName: secretStoreServiceName(options),
|
|
|
|
|
LookupEnv: options.LookupEnv,
|
|
|
|
|
KWalletAppID: options.KWalletAppID,
|
|
|
|
|
KWalletFolder: options.KWalletFolder,
|
|
|
|
|
BitwardenCommand: options.BitwardenCommand,
|
|
|
|
|
BitwardenDebug: options.BitwardenDebug,
|
2026-05-02 13:04:29 +00:00
|
|
|
DisableBitwardenCache: options.DisableBitwardenCache,
|
2026-05-02 09:57:44 +00:00
|
|
|
Shell: options.Shell,
|
|
|
|
|
ManifestLoader: LoadManifest,
|
|
|
|
|
ExecutableResolver: options.ExecutableResolver,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func secretStoreDescribeOptions(options SecretStoreOptions) fwsecretstore.DescribeRuntimeOptions {
|
|
|
|
|
return fwsecretstore.DescribeRuntimeOptions{
|
|
|
|
|
ServiceName: secretStoreServiceName(options),
|
|
|
|
|
LookupEnv: options.LookupEnv,
|
|
|
|
|
KWalletAppID: options.KWalletAppID,
|
|
|
|
|
KWalletFolder: options.KWalletFolder,
|
|
|
|
|
BitwardenCommand: options.BitwardenCommand,
|
|
|
|
|
BitwardenDebug: options.BitwardenDebug,
|
2026-05-02 13:04:29 +00:00
|
|
|
DisableBitwardenCache: options.DisableBitwardenCache,
|
2026-05-02 09:57:44 +00:00
|
|
|
Shell: options.Shell,
|
|
|
|
|
ManifestLoader: LoadManifest,
|
|
|
|
|
ExecutableResolver: options.ExecutableResolver,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func secretStoreServiceName(options SecretStoreOptions) string {
|
|
|
|
|
serviceName := strings.TrimSpace(options.ServiceName)
|
|
|
|
|
if serviceName != "" {
|
|
|
|
|
return serviceName
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
startDir := "."
|
|
|
|
|
executableResolver := options.ExecutableResolver
|
|
|
|
|
if executableResolver == nil {
|
|
|
|
|
executableResolver = os.Executable
|
|
|
|
|
}
|
|
|
|
|
if executablePath, err := executableResolver(); err == nil {
|
|
|
|
|
if dir := strings.TrimSpace(filepath.Dir(strings.TrimSpace(executablePath))); dir != "" {
|
|
|
|
|
startDir = dir
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if manifestFile, _, err := LoadManifest(startDir); err == nil {
|
|
|
|
|
if binaryName := strings.TrimSpace(manifestFile.BinaryName); binaryName != "" {
|
|
|
|
|
return binaryName
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return BinaryName
|
|
|
|
|
}
|
|
|
|
|
`, packageName)
|
|
|
|
|
|
|
|
|
|
return formatGenerated("secretstore", source)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-02 10:02:23 +00:00
|
|
|
func renderConfig(packageName string, fields []manifest.ConfigField) (string, error) {
|
|
|
|
|
if len(fields) == 0 {
|
|
|
|
|
return "", nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var flagsBuilder strings.Builder
|
|
|
|
|
var specsBuilder strings.Builder
|
|
|
|
|
var setupBuilder strings.Builder
|
|
|
|
|
for _, field := range fields {
|
|
|
|
|
name := strings.TrimSpace(field.Name)
|
|
|
|
|
if name == "" {
|
|
|
|
|
return "", fmt.Errorf("generate config field: name must not be empty")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
flagName := strings.TrimSpace(field.Flag)
|
|
|
|
|
if flagName != "" {
|
|
|
|
|
fmt.Fprintf(
|
|
|
|
|
&flagsBuilder,
|
|
|
|
|
"\tflags.values[%s] = fs.String(%s, \"\", %s)\n",
|
|
|
|
|
strconv.Quote(name),
|
|
|
|
|
strconv.Quote(flagName),
|
|
|
|
|
strconv.Quote(configFieldLabel(field)),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Fprintf(
|
|
|
|
|
&specsBuilder,
|
|
|
|
|
"\t\t{Name: %s, Required: %t, DefaultValue: %s, Sources: []fwcli.ValueSource{%s}, FlagKey: %s, EnvKey: %s, ConfigKey: %s, SecretKey: replaceProfile(%s, profile)},\n",
|
|
|
|
|
strconv.Quote(name),
|
|
|
|
|
field.Required,
|
|
|
|
|
strconv.Quote(field.Default),
|
|
|
|
|
configSourceList(field.Sources),
|
|
|
|
|
strconv.Quote(flagName),
|
|
|
|
|
strconv.Quote(field.Env),
|
|
|
|
|
strconv.Quote(field.ConfigKey),
|
|
|
|
|
strconv.Quote(field.SecretKeyTemplate),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
fmt.Fprintf(
|
|
|
|
|
&setupBuilder,
|
|
|
|
|
"\t\t{Name: %s, Label: %s, Type: %s, Required: %t, Default: %s, ExistingSecret: existing[%s]},\n",
|
|
|
|
|
strconv.Quote(name),
|
|
|
|
|
strconv.Quote(configFieldLabel(field)),
|
|
|
|
|
configSetupFieldType(field.Type),
|
|
|
|
|
field.Required,
|
|
|
|
|
strconv.Quote(field.Default),
|
|
|
|
|
strconv.Quote(name),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
source := fmt.Sprintf(`// Code generated by mcp-framework generate. DO NOT EDIT.
|
|
|
|
|
|
|
|
|
|
package %s
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"flag"
|
|
|
|
|
"strings"
|
|
|
|
|
|
2026-05-05 10:23:14 +00:00
|
|
|
fwcli "forge.lclr.dev/AI/mcp-framework/cli"
|
2026-05-02 10:02:23 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type ConfigFlags struct {
|
|
|
|
|
values map[string]*string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func AddConfigFlags(fs *flag.FlagSet) ConfigFlags {
|
|
|
|
|
if fs == nil {
|
|
|
|
|
fs = flag.CommandLine
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
flags := ConfigFlags{
|
|
|
|
|
values: make(map[string]*string),
|
|
|
|
|
}
|
|
|
|
|
%s
|
|
|
|
|
return flags
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func ConfigFlagValues(flags ConfigFlags) map[string]string {
|
|
|
|
|
values := make(map[string]string)
|
|
|
|
|
for name, value := range flags.values {
|
|
|
|
|
if value == nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if trimmed := strings.TrimSpace(*value); trimmed != "" {
|
|
|
|
|
values[name] = trimmed
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return values
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func ResolveFieldSpecs(profile string) []fwcli.FieldSpec {
|
|
|
|
|
return []fwcli.FieldSpec{
|
|
|
|
|
%s
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func SetupFields(existing map[string]string) []fwcli.SetupField {
|
|
|
|
|
if existing == nil {
|
|
|
|
|
existing = map[string]string{}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return []fwcli.SetupField{
|
|
|
|
|
%s
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func replaceProfile(value, profile string) string {
|
|
|
|
|
return strings.ReplaceAll(value, "{profile}", strings.TrimSpace(profile))
|
|
|
|
|
}
|
|
|
|
|
`, packageName, flagsBuilder.String(), specsBuilder.String(), setupBuilder.String())
|
|
|
|
|
|
|
|
|
|
return formatGenerated("config", source)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func configFieldLabel(field manifest.ConfigField) string {
|
|
|
|
|
if label := strings.TrimSpace(field.Label); label != "" {
|
|
|
|
|
return label
|
|
|
|
|
}
|
|
|
|
|
return strings.TrimSpace(field.Name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func configSourceList(sources []string) string {
|
|
|
|
|
if len(sources) == 0 {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
parts := make([]string, 0, len(sources))
|
|
|
|
|
for _, source := range sources {
|
|
|
|
|
switch strings.TrimSpace(source) {
|
|
|
|
|
case "flag":
|
|
|
|
|
parts = append(parts, "fwcli.SourceFlag")
|
|
|
|
|
case "env":
|
|
|
|
|
parts = append(parts, "fwcli.SourceEnv")
|
|
|
|
|
case "config":
|
|
|
|
|
parts = append(parts, "fwcli.SourceConfig")
|
|
|
|
|
case "secret":
|
|
|
|
|
parts = append(parts, "fwcli.SourceSecret")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return strings.Join(parts, ", ")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func configSetupFieldType(fieldType string) string {
|
|
|
|
|
switch strings.TrimSpace(fieldType) {
|
|
|
|
|
case "url":
|
|
|
|
|
return "fwcli.SetupFieldURL"
|
|
|
|
|
case "secret":
|
|
|
|
|
return "fwcli.SetupFieldSecret"
|
|
|
|
|
case "bool":
|
|
|
|
|
return "fwcli.SetupFieldBool"
|
|
|
|
|
case "list":
|
|
|
|
|
return "fwcli.SetupFieldList"
|
|
|
|
|
default:
|
|
|
|
|
return "fwcli.SetupFieldString"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-02 09:57:44 +00:00
|
|
|
func formatGenerated(name, source string) (string, error) {
|
|
|
|
|
formatted, err := format.Source([]byte(source))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", fmt.Errorf("format generated %s: %w", name, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return string(formatted), nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-02 09:46:47 +00:00
|
|
|
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
|
|
|
|
|
}
|