feat: complete AX pass — cmd/ rewrite, string concat, proxy, bridge

- 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 <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-26 08:50:29 +00:00
parent de63217168
commit bca6e2c4cb
7 changed files with 86 additions and 105 deletions

View file

@ -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,

View file

@ -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),
})
}

View file

@ -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}
}
}

View file

@ -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}
}
}

View file

@ -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))
}

View file

@ -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
}

View file

@ -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{