From bca6e2c4cb203bd3a4762ebb217db7c74c49b1ab Mon Sep 17 00:00:00 2001 From: Snider Date: Thu, 26 Mar 2026 08:50:29 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20complete=20AX=20pass=20=E2=80=94=20cmd/?= =?UTF-8?q?=20rewrite,=20string=20concat,=20proxy,=20bridge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cmd/api: rewrite from Cobra CLI → Core command tree (c.Command) - cmd/api/cmd_spec.go: uses SpecBuilder.Build → core.Result - cmd/api/cmd_sdk.go: uses c.Process() for openapi-generator, core.Fs - bridge.go: string concat → core.Concat - graphql.go: string concat → core.Concat - openapi.go: string concat → core.Concat - proxy.go: strings → core.TrimPrefix/TrimSuffix, panic concat → core.Concat - Zero disallowed imports, zero string concat, zero old paths Co-Authored-By: Virgil --- bridge.go | 5 ++- cmd/api/cmd.go | 25 +++++++------ cmd/api/cmd_sdk.go | 87 ++++++++++++++++++------------------------- cmd/api/cmd_spec.go | 59 +++++++++++++---------------- graphql.go | 3 +- openapi.go | 2 +- pkg/provider/proxy.go | 10 ++--- 7 files changed, 86 insertions(+), 105 deletions(-) diff --git a/bridge.go b/bridge.go index 79e2e78..6072a42 100644 --- a/bridge.go +++ b/bridge.go @@ -5,6 +5,7 @@ package api import ( "iter" + core "dappco.re/go/core" "github.com/gin-gonic/gin" ) @@ -66,7 +67,7 @@ func (b *ToolBridge) Describe() []RouteDescription { } descs = append(descs, RouteDescription{ Method: "POST", - Path: "/" + t.descriptor.Name, + Path: core.Concat("/", t.descriptor.Name), Summary: t.descriptor.Description, Description: t.descriptor.Description, Tags: tags, @@ -87,7 +88,7 @@ func (b *ToolBridge) DescribeIter() iter.Seq[RouteDescription] { } rd := RouteDescription{ Method: "POST", - Path: "/" + t.descriptor.Name, + Path: core.Concat("/", t.descriptor.Name), Summary: t.descriptor.Description, Description: t.descriptor.Description, Tags: tags, diff --git a/cmd/api/cmd.go b/cmd/api/cmd.go index e0fb419..f1f8950 100644 --- a/cmd/api/cmd.go +++ b/cmd/api/cmd.go @@ -2,17 +2,18 @@ package api -import "forge.lthn.ai/core/cli/pkg/cli" +import core "dappco.re/go/core" -func init() { - cli.RegisterCommands(AddAPICommands) -} - -// AddAPICommands registers the 'api' command group. -func AddAPICommands(root *cli.Command) { - apiCmd := cli.NewGroup("api", "API specification and SDK generation", "") - root.AddCommand(apiCmd) - - addSpecCommand(apiCmd) - addSDKCommand(apiCmd) +// RegisterCommands adds API commands to Core's command tree. +// +// api.RegisterCommands(c) +func RegisterCommands(c *core.Core) { + c.Command("api/spec", core.Command{ + Description: "Generate OpenAPI specification", + Action: cmdSpec(c), + }) + c.Command("api/sdk", core.Command{ + Description: "Generate client SDKs from OpenAPI spec", + Action: cmdSDK(c), + }) } diff --git a/cmd/api/cmd_sdk.go b/cmd/api/cmd_sdk.go index be5c9ee..2bdb852 100644 --- a/cmd/api/cmd_sdk.go +++ b/cmd/api/cmd_sdk.go @@ -4,54 +4,46 @@ package api import ( "context" - "fmt" - "os" - "strings" - - "forge.lthn.ai/core/cli/pkg/cli" - - coreio "dappco.re/go/core/io" - coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" goapi "dappco.re/go/core/api" ) -func addSDKCommand(parent *cli.Command) { - var ( - lang string - output string - specFile string - packageName string - ) - - cmd := cli.NewCommand("sdk", "Generate client SDKs from OpenAPI spec", "", func(cmd *cli.Command, args []string) error { +func cmdSDK(c *core.Core) core.CommandAction { + return func(opts core.Options) core.Result { + lang := opts.String("lang") if lang == "" { - return coreerr.E("sdk.Generate", "--lang is required. Supported: "+strings.Join(goapi.SupportedLanguages(), ", "), nil) + core.Print(nil, "usage: api sdk --lang=go,python [--output=./sdk] [--spec=openapi.json]") + core.Print(nil, "supported: %s", core.Join(", ", goapi.SupportedLanguages()...)) + return core.Result{OK: false} } - // If no spec file provided, generate one to a temp file. + output := opts.String("output") + if output == "" { + output = "./sdk" + } + specFile := opts.String("spec") + packageName := opts.String("package") + if packageName == "" { + packageName = "lethean" + } + + // If no spec file, generate one to temp if specFile == "" { builder := &goapi.SpecBuilder{ Title: "Lethean Core API", Description: "Lethean Core API", Version: "1.0.0", } - bridge := goapi.NewToolBridge("/tools") groups := []goapi.RouteGroup{bridge} - tmpFile, err := os.CreateTemp("", "openapi-*.json") - if err != nil { - return coreerr.E("sdk.Generate", "create temp spec file", err) + tmpPath := core.JoinPath(c.Fs().TempDir("openapi"), "spec.json") + r := goapi.ExportSpec(tmpPath, "json", builder, groups) + if !r.OK { + return r } - defer coreio.Local.Delete(tmpFile.Name()) - - if err := goapi.ExportSpec(tmpFile, "json", builder, groups); err != nil { - tmpFile.Close() - return coreerr.E("sdk.Generate", "generate spec", err) - } - tmpFile.Close() - specFile = tmpFile.Name() + specFile = tmpPath } gen := &goapi.SDKGenerator{ @@ -61,32 +53,25 @@ func addSDKCommand(parent *cli.Command) { } if !gen.Available() { - fmt.Fprintln(os.Stderr, "openapi-generator-cli not found. Install with:") - fmt.Fprintln(os.Stderr, " brew install openapi-generator (macOS)") - fmt.Fprintln(os.Stderr, " npm install @openapitools/openapi-generator-cli -g") - return coreerr.E("sdk.Generate", "openapi-generator-cli not installed", nil) + core.Print(nil, "openapi-generator-cli not found. Install with:") + core.Print(nil, " brew install openapi-generator (macOS)") + core.Print(nil, " npm install @openapitools/openapi-generator-cli -g") + return core.Result{OK: false} } - // Generate for each language. - for l := range strings.SplitSeq(lang, ",") { - l = strings.TrimSpace(l) + for _, l := range core.Split(lang, ",") { + l = core.Trim(l) if l == "" { continue } - fmt.Fprintf(os.Stderr, "Generating %s SDK...\n", l) - if err := gen.Generate(context.Background(), l); err != nil { - return coreerr.E("sdk.Generate", "generate "+l, err) + core.Print(nil, "generating %s SDK...", l) + r := gen.Generate(context.Background(), c, l) + if !r.OK { + return r } - fmt.Fprintf(os.Stderr, " Done: %s/%s/\n", output, l) + core.Print(nil, " done: %s/%s/", output, l) } - return nil - }) - - cli.StringFlag(cmd, &lang, "lang", "l", "", "Target language(s), comma-separated (e.g. go,python,typescript-fetch)") - cli.StringFlag(cmd, &output, "output", "o", "./sdk", "Output directory for generated SDKs") - cli.StringFlag(cmd, &specFile, "spec", "s", "", "Path to existing OpenAPI spec (generates from MCP tools if not provided)") - cli.StringFlag(cmd, &packageName, "package", "p", "lethean", "Package name for generated SDK") - - parent.AddCommand(cmd) + return core.Result{OK: true} + } } diff --git a/cmd/api/cmd_spec.go b/cmd/api/cmd_spec.go index 7ad145e..674dbb3 100644 --- a/cmd/api/cmd_spec.go +++ b/cmd/api/cmd_spec.go @@ -3,52 +3,45 @@ package api import ( - "fmt" - "os" - - "forge.lthn.ai/core/cli/pkg/cli" - + core "dappco.re/go/core" goapi "dappco.re/go/core/api" ) -func addSpecCommand(parent *cli.Command) { - var ( - output string - format string - title string - version string - ) +func cmdSpec(c *core.Core) core.CommandAction { + return func(opts core.Options) core.Result { + format := opts.String("format") + if format == "" { + format = "json" + } + output := opts.String("output") + title := opts.String("title") + if title == "" { + title = "Lethean Core API" + } + ver := opts.String("version") + if ver == "" { + ver = "1.0.0" + } - cmd := cli.NewCommand("spec", "Generate OpenAPI specification", "", func(cmd *cli.Command, args []string) error { - // Build spec from registered route groups. - // Additional groups can be added here as the platform grows. builder := &goapi.SpecBuilder{ Title: title, Description: "Lethean Core API", - Version: version, + Version: ver, } - // Start with the default tool bridge — future versions will - // auto-populate from the MCP tool registry once the bridge - // integration lands in the local go-ai module. bridge := goapi.NewToolBridge("/tools") groups := []goapi.RouteGroup{bridge} if output != "" { - if err := goapi.ExportSpecToFile(output, format, builder, groups); err != nil { - return err - } - fmt.Fprintf(os.Stderr, "Spec written to %s\n", output) - return nil + return goapi.ExportSpec(output, format, builder, groups) } - return goapi.ExportSpec(os.Stdout, format, builder, groups) - }) - - cli.StringFlag(cmd, &output, "output", "o", "", "Write spec to file instead of stdout") - cli.StringFlag(cmd, &format, "format", "f", "json", "Output format: json or yaml") - cli.StringFlag(cmd, &title, "title", "t", "Lethean Core API", "API title in spec") - cli.StringFlag(cmd, &version, "version", "V", "1.0.0", "API version in spec") - - parent.AddCommand(cmd) + // No output file — build and print to stdout + r := builder.Build(groups) + if !r.OK { + return r + } + core.Println(string(r.Value.([]byte))) + return core.Result{OK: true} + } } diff --git a/graphql.go b/graphql.go index c878ee3..dfe8ba2 100644 --- a/graphql.go +++ b/graphql.go @@ -5,6 +5,7 @@ package api import ( "net/http" + core "dappco.re/go/core" "github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql/handler" "github.com/99designs/gqlgen/graphql/playground" @@ -49,7 +50,7 @@ func mountGraphQL(r *gin.Engine, cfg *graphqlConfig) { r.Any(cfg.path, graphqlHandler) if cfg.playground { - playgroundPath := cfg.path + "/playground" + playgroundPath := core.Concat(cfg.path, "/playground") playgroundHandler := playground.Handler("GraphQL", cfg.path) r.GET(playgroundPath, wrapHTTPHandler(playgroundHandler)) } diff --git a/openapi.go b/openapi.go index b29cf7a..eb196ed 100644 --- a/openapi.go +++ b/openapi.go @@ -150,7 +150,7 @@ func (sb *SpecBuilder) buildTags(groups []RouteGroup) []map[string]any { if !seen[name] { tags = append(tags, map[string]any{ "name": name, - "description": name + " endpoints", + "description": core.Concat(name, " endpoints"), }) seen[name] = true } diff --git a/pkg/provider/proxy.go b/pkg/provider/proxy.go index 9eb2b4a..cc95bc8 100644 --- a/pkg/provider/proxy.go +++ b/pkg/provider/proxy.go @@ -6,8 +6,8 @@ import ( "net/http" "net/http/httputil" "net/url" - "strings" + core "dappco.re/go/core" "github.com/gin-gonic/gin" ) @@ -46,7 +46,7 @@ type ProxyProvider struct { func NewProxy(cfg ProxyConfig) *ProxyProvider { target, err := url.Parse(cfg.Upstream) if err != nil { - panic("provider.NewProxy: invalid upstream URL: " + err.Error()) + panic(core.Concat("provider.NewProxy: invalid upstream URL: ", err.Error())) } proxy := httputil.NewSingleHostReverseProxy(target) @@ -54,16 +54,16 @@ func NewProxy(cfg ProxyConfig) *ProxyProvider { // Preserve the original Director but strip the base path so the // upstream receives clean paths (e.g. /items instead of /api/v1/cool-widget/items). defaultDirector := proxy.Director - basePath := strings.TrimSuffix(cfg.BasePath, "/") + basePath := core.TrimSuffix(cfg.BasePath, "/") proxy.Director = func(req *http.Request) { defaultDirector(req) // Strip the base path prefix from the request path. - req.URL.Path = strings.TrimPrefix(req.URL.Path, basePath) + req.URL.Path = core.TrimPrefix(req.URL.Path, basePath) if req.URL.Path == "" { req.URL.Path = "/" } - req.URL.RawPath = strings.TrimPrefix(req.URL.RawPath, basePath) + req.URL.RawPath = core.TrimPrefix(req.URL.RawPath, basePath) } return &ProxyProvider{