feat(ninja): wire ListTags into popular-tag prompt and list helpers

This commit is contained in:
Alex Dunmow 2026-06-07 15:53:03 +08:00
parent bb3ddfe1bd
commit 26b262ce73
5 changed files with 532 additions and 278 deletions

View File

@ -606,17 +606,23 @@ func promptCategoriesWithDefault(ctx context.Context, cli *orchclient.Client, sc
}
// fetchPopularTagsForPrompt returns a comma-joined "tag (count)" string of the
// top tags for the given kind, or "" if the orchestrator lookup fails. The
// current implementation always returns "" because core's copy of the
// orchestrator proto bindings does not yet expose ListTags; once the bindings
// are regenerated this body becomes a best-effort PluginRegistryService.ListTags
// call. Callers must already tolerate an empty return.
// top tags for the given kind, or "" if the orchestrator lookup fails (e.g.
// offline, unauthenticated, RPC error). Callers must tolerate an empty return
// and surface their own user-facing fallback message.
func fetchPopularTagsForPrompt(ctx context.Context, cli *orchclient.Client, kind string) string {
_ = ctx
_ = cli
_ = kind
resp, err := cli.Reg.ListTags(ctx, connect.NewRequest(&v1.ListTagsRequest{Kind: kind, Limit: 20}))
if err != nil {
return ""
}
if len(resp.Msg.Tags) == 0 {
return ""
}
parts := make([]string, 0, len(resp.Msg.Tags))
for _, t := range resp.Msg.Tags {
parts = append(parts, fmt.Sprintf("%s (%d)", t.Tag, t.Count))
}
return strings.Join(parts, ", ")
}
// promptTagsWithDefault prompts the user for free-form tags. Best-effort fetches
// the top-20 most-used tags via ListTags(kind) to surface popular suggestions;

View File

@ -1,17 +1,20 @@
package cmd
import (
"context"
"fmt"
"os"
"slices"
"sort"
"strings"
"connectrpc.com/connect"
"github.com/spf13/cobra"
core "git.dev.alexdunmow.com/block/core/plugin"
"git.dev.alexdunmow.com/block/core/cmd/ninja/internal/creds"
"git.dev.alexdunmow.com/block/core/cmd/ninja/internal/orchclient"
v1 "git.dev.alexdunmow.com/block/core/internal/api/orchestrator/v1"
)
func newPluginTagsCmd() *cobra.Command {
@ -152,14 +155,21 @@ func writeLocalModTags(mod *core.ModFile, tags []string) error {
}
// fetchPopularTagsForList returns a single user-facing line listing the most-used
// tags for the given kind. The current body returns an unavailable-notice
// because core's copy of the orchestrator proto bindings does not yet expose
// ListTags; once bindings are regenerated this body becomes a real
// PluginRegistryService.ListTags call rendering "Popular: tag (count), ..." or
// "Popular tags: (none yet)".
// tags for the given kind. Renders "Popular: tag (count), ...", "Popular tags:
// (none yet)" when no tags exist on public plugins yet, or an "(unreachable)"
// notice if the RPC fails.
func fetchPopularTagsForList(cli *orchclient.Client, kind string) string {
_ = cli
_ = kind
return "(popular tags unavailable — orchestrator bindings not yet regenerated)"
resp, err := cli.Reg.ListTags(context.Background(), connect.NewRequest(&v1.ListTagsRequest{Kind: kind, Limit: 20}))
if err != nil {
return "(could not fetch popular tags — orchestrator unreachable)"
}
if len(resp.Msg.Tags) == 0 {
return "Popular tags: (none yet)"
}
parts := make([]string, len(resp.Msg.Tags))
for i, t := range resp.Msg.Tags {
parts[i] = fmt.Sprintf("%s (%d)", t.Tag, t.Count)
}
return "Popular: " + strings.Join(parts, ", ")
}

View File

@ -71,6 +71,9 @@ const (
// PluginRegistryServiceListCategoriesProcedure is the fully-qualified name of the
// PluginRegistryService's ListCategories RPC.
PluginRegistryServiceListCategoriesProcedure = "/orchestrator.v1.PluginRegistryService/ListCategories"
// PluginRegistryServiceListTagsProcedure is the fully-qualified name of the PluginRegistryService's
// ListTags RPC.
PluginRegistryServiceListTagsProcedure = "/orchestrator.v1.PluginRegistryService/ListTags"
// PluginRegistryServiceSubmitForReviewProcedure is the fully-qualified name of the
// PluginRegistryService's SubmitForReview RPC.
PluginRegistryServiceSubmitForReviewProcedure = "/orchestrator.v1.PluginRegistryService/SubmitForReview"
@ -280,6 +283,7 @@ type PluginRegistryServiceClient interface {
GetVersion(context.Context, *connect.Request[v1.GetVersionRequest]) (*connect.Response[v1.GetVersionResponse], error)
ResolveInstall(context.Context, *connect.Request[v1.ResolveInstallRequest]) (*connect.Response[v1.ResolveInstallResponse], error)
ListCategories(context.Context, *connect.Request[v1.ListCategoriesRequest]) (*connect.Response[v1.ListCategoriesResponse], error)
ListTags(context.Context, *connect.Request[v1.ListTagsRequest]) (*connect.Response[v1.ListTagsResponse], error)
SubmitForReview(context.Context, *connect.Request[v1.SubmitForReviewRequest]) (*connect.Response[v1.SubmitForReviewResponse], error)
// Private-plugin RPCs. All require the caller to be a member of the target
// account; the server resolves account membership from the bearer token.
@ -336,6 +340,12 @@ func NewPluginRegistryServiceClient(httpClient connect.HTTPClient, baseURL strin
connect.WithSchema(pluginRegistryServiceMethods.ByName("ListCategories")),
connect.WithClientOptions(opts...),
),
listTags: connect.NewClient[v1.ListTagsRequest, v1.ListTagsResponse](
httpClient,
baseURL+PluginRegistryServiceListTagsProcedure,
connect.WithSchema(pluginRegistryServiceMethods.ByName("ListTags")),
connect.WithClientOptions(opts...),
),
submitForReview: connect.NewClient[v1.SubmitForReviewRequest, v1.SubmitForReviewResponse](
httpClient,
baseURL+PluginRegistryServiceSubmitForReviewProcedure,
@ -377,6 +387,7 @@ type pluginRegistryServiceClient struct {
getVersion *connect.Client[v1.GetVersionRequest, v1.GetVersionResponse]
resolveInstall *connect.Client[v1.ResolveInstallRequest, v1.ResolveInstallResponse]
listCategories *connect.Client[v1.ListCategoriesRequest, v1.ListCategoriesResponse]
listTags *connect.Client[v1.ListTagsRequest, v1.ListTagsResponse]
submitForReview *connect.Client[v1.SubmitForReviewRequest, v1.SubmitForReviewResponse]
listPrivatePlugins *connect.Client[v1.ListPrivatePluginsRequest, v1.ListPrivatePluginsResponse]
deletePrivatePlugin *connect.Client[v1.DeletePrivatePluginRequest, v1.DeletePrivatePluginResponse]
@ -414,6 +425,11 @@ func (c *pluginRegistryServiceClient) ListCategories(ctx context.Context, req *c
return c.listCategories.CallUnary(ctx, req)
}
// ListTags calls orchestrator.v1.PluginRegistryService.ListTags.
func (c *pluginRegistryServiceClient) ListTags(ctx context.Context, req *connect.Request[v1.ListTagsRequest]) (*connect.Response[v1.ListTagsResponse], error) {
return c.listTags.CallUnary(ctx, req)
}
// SubmitForReview calls orchestrator.v1.PluginRegistryService.SubmitForReview.
func (c *pluginRegistryServiceClient) SubmitForReview(ctx context.Context, req *connect.Request[v1.SubmitForReviewRequest]) (*connect.Response[v1.SubmitForReviewResponse], error) {
return c.submitForReview.CallUnary(ctx, req)
@ -450,6 +466,7 @@ type PluginRegistryServiceHandler interface {
GetVersion(context.Context, *connect.Request[v1.GetVersionRequest]) (*connect.Response[v1.GetVersionResponse], error)
ResolveInstall(context.Context, *connect.Request[v1.ResolveInstallRequest]) (*connect.Response[v1.ResolveInstallResponse], error)
ListCategories(context.Context, *connect.Request[v1.ListCategoriesRequest]) (*connect.Response[v1.ListCategoriesResponse], error)
ListTags(context.Context, *connect.Request[v1.ListTagsRequest]) (*connect.Response[v1.ListTagsResponse], error)
SubmitForReview(context.Context, *connect.Request[v1.SubmitForReviewRequest]) (*connect.Response[v1.SubmitForReviewResponse], error)
// Private-plugin RPCs. All require the caller to be a member of the target
// account; the server resolves account membership from the bearer token.
@ -502,6 +519,12 @@ func NewPluginRegistryServiceHandler(svc PluginRegistryServiceHandler, opts ...c
connect.WithSchema(pluginRegistryServiceMethods.ByName("ListCategories")),
connect.WithHandlerOptions(opts...),
)
pluginRegistryServiceListTagsHandler := connect.NewUnaryHandler(
PluginRegistryServiceListTagsProcedure,
svc.ListTags,
connect.WithSchema(pluginRegistryServiceMethods.ByName("ListTags")),
connect.WithHandlerOptions(opts...),
)
pluginRegistryServiceSubmitForReviewHandler := connect.NewUnaryHandler(
PluginRegistryServiceSubmitForReviewProcedure,
svc.SubmitForReview,
@ -546,6 +569,8 @@ func NewPluginRegistryServiceHandler(svc PluginRegistryServiceHandler, opts ...c
pluginRegistryServiceResolveInstallHandler.ServeHTTP(w, r)
case PluginRegistryServiceListCategoriesProcedure:
pluginRegistryServiceListCategoriesHandler.ServeHTTP(w, r)
case PluginRegistryServiceListTagsProcedure:
pluginRegistryServiceListTagsHandler.ServeHTTP(w, r)
case PluginRegistryServiceSubmitForReviewProcedure:
pluginRegistryServiceSubmitForReviewHandler.ServeHTTP(w, r)
case PluginRegistryServiceListPrivatePluginsProcedure:
@ -589,6 +614,10 @@ func (UnimplementedPluginRegistryServiceHandler) ListCategories(context.Context,
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginRegistryService.ListCategories is not implemented"))
}
func (UnimplementedPluginRegistryServiceHandler) ListTags(context.Context, *connect.Request[v1.ListTagsRequest]) (*connect.Response[v1.ListTagsResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginRegistryService.ListTags is not implemented"))
}
func (UnimplementedPluginRegistryServiceHandler) SubmitForReview(context.Context, *connect.Request[v1.SubmitForReviewRequest]) (*connect.Response[v1.SubmitForReviewResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginRegistryService.SubmitForReview is not implemented"))
}

File diff suppressed because it is too large Load Diff

2
proto

@ -1 +1 @@
Subproject commit 6d6445f74ec8152c0458257adf1f406d8ce8e68a
Subproject commit cdb50a77f284f2ae124ddd777c688d352e63ed0c