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 // fetchPopularTagsForPrompt returns a comma-joined "tag (count)" string of the
// top tags for the given kind, or "" if the orchestrator lookup fails. The // top tags for the given kind, or "" if the orchestrator lookup fails (e.g.
// current implementation always returns "" because core's copy of the // offline, unauthenticated, RPC error). Callers must tolerate an empty return
// orchestrator proto bindings does not yet expose ListTags; once the bindings // and surface their own user-facing fallback message.
// are regenerated this body becomes a best-effort PluginRegistryService.ListTags
// call. Callers must already tolerate an empty return.
func fetchPopularTagsForPrompt(ctx context.Context, cli *orchclient.Client, kind string) string { func fetchPopularTagsForPrompt(ctx context.Context, cli *orchclient.Client, kind string) string {
_ = ctx resp, err := cli.Reg.ListTags(ctx, connect.NewRequest(&v1.ListTagsRequest{Kind: kind, Limit: 20}))
_ = cli if err != nil {
_ = kind
return "" 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 // promptTagsWithDefault prompts the user for free-form tags. Best-effort fetches
// the top-20 most-used tags via ListTags(kind) to surface popular suggestions; // the top-20 most-used tags via ListTags(kind) to surface popular suggestions;

View File

@ -1,17 +1,20 @@
package cmd package cmd
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"slices" "slices"
"sort" "sort"
"strings" "strings"
"connectrpc.com/connect"
"github.com/spf13/cobra" "github.com/spf13/cobra"
core "git.dev.alexdunmow.com/block/core/plugin" 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/creds"
"git.dev.alexdunmow.com/block/core/cmd/ninja/internal/orchclient" "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 { 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 // fetchPopularTagsForList returns a single user-facing line listing the most-used
// tags for the given kind. The current body returns an unavailable-notice // tags for the given kind. Renders "Popular: tag (count), ...", "Popular tags:
// because core's copy of the orchestrator proto bindings does not yet expose // (none yet)" when no tags exist on public plugins yet, or an "(unreachable)"
// ListTags; once bindings are regenerated this body becomes a real // notice if the RPC fails.
// PluginRegistryService.ListTags call rendering "Popular: tag (count), ..." or
// "Popular tags: (none yet)".
func fetchPopularTagsForList(cli *orchclient.Client, kind string) string { func fetchPopularTagsForList(cli *orchclient.Client, kind string) string {
_ = cli resp, err := cli.Reg.ListTags(context.Background(), connect.NewRequest(&v1.ListTagsRequest{Kind: kind, Limit: 20}))
_ = kind if err != nil {
return "(popular tags unavailable — orchestrator bindings not yet regenerated)" 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 // PluginRegistryServiceListCategoriesProcedure is the fully-qualified name of the
// PluginRegistryService's ListCategories RPC. // PluginRegistryService's ListCategories RPC.
PluginRegistryServiceListCategoriesProcedure = "/orchestrator.v1.PluginRegistryService/ListCategories" 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 // PluginRegistryServiceSubmitForReviewProcedure is the fully-qualified name of the
// PluginRegistryService's SubmitForReview RPC. // PluginRegistryService's SubmitForReview RPC.
PluginRegistryServiceSubmitForReviewProcedure = "/orchestrator.v1.PluginRegistryService/SubmitForReview" PluginRegistryServiceSubmitForReviewProcedure = "/orchestrator.v1.PluginRegistryService/SubmitForReview"
@ -280,6 +283,7 @@ type PluginRegistryServiceClient interface {
GetVersion(context.Context, *connect.Request[v1.GetVersionRequest]) (*connect.Response[v1.GetVersionResponse], error) GetVersion(context.Context, *connect.Request[v1.GetVersionRequest]) (*connect.Response[v1.GetVersionResponse], error)
ResolveInstall(context.Context, *connect.Request[v1.ResolveInstallRequest]) (*connect.Response[v1.ResolveInstallResponse], 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) 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) 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 // Private-plugin RPCs. All require the caller to be a member of the target
// account; the server resolves account membership from the bearer token. // 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.WithSchema(pluginRegistryServiceMethods.ByName("ListCategories")),
connect.WithClientOptions(opts...), 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]( submitForReview: connect.NewClient[v1.SubmitForReviewRequest, v1.SubmitForReviewResponse](
httpClient, httpClient,
baseURL+PluginRegistryServiceSubmitForReviewProcedure, baseURL+PluginRegistryServiceSubmitForReviewProcedure,
@ -377,6 +387,7 @@ type pluginRegistryServiceClient struct {
getVersion *connect.Client[v1.GetVersionRequest, v1.GetVersionResponse] getVersion *connect.Client[v1.GetVersionRequest, v1.GetVersionResponse]
resolveInstall *connect.Client[v1.ResolveInstallRequest, v1.ResolveInstallResponse] resolveInstall *connect.Client[v1.ResolveInstallRequest, v1.ResolveInstallResponse]
listCategories *connect.Client[v1.ListCategoriesRequest, v1.ListCategoriesResponse] listCategories *connect.Client[v1.ListCategoriesRequest, v1.ListCategoriesResponse]
listTags *connect.Client[v1.ListTagsRequest, v1.ListTagsResponse]
submitForReview *connect.Client[v1.SubmitForReviewRequest, v1.SubmitForReviewResponse] submitForReview *connect.Client[v1.SubmitForReviewRequest, v1.SubmitForReviewResponse]
listPrivatePlugins *connect.Client[v1.ListPrivatePluginsRequest, v1.ListPrivatePluginsResponse] listPrivatePlugins *connect.Client[v1.ListPrivatePluginsRequest, v1.ListPrivatePluginsResponse]
deletePrivatePlugin *connect.Client[v1.DeletePrivatePluginRequest, v1.DeletePrivatePluginResponse] 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) 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. // SubmitForReview calls orchestrator.v1.PluginRegistryService.SubmitForReview.
func (c *pluginRegistryServiceClient) SubmitForReview(ctx context.Context, req *connect.Request[v1.SubmitForReviewRequest]) (*connect.Response[v1.SubmitForReviewResponse], error) { func (c *pluginRegistryServiceClient) SubmitForReview(ctx context.Context, req *connect.Request[v1.SubmitForReviewRequest]) (*connect.Response[v1.SubmitForReviewResponse], error) {
return c.submitForReview.CallUnary(ctx, req) 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) GetVersion(context.Context, *connect.Request[v1.GetVersionRequest]) (*connect.Response[v1.GetVersionResponse], error)
ResolveInstall(context.Context, *connect.Request[v1.ResolveInstallRequest]) (*connect.Response[v1.ResolveInstallResponse], 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) 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) 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 // Private-plugin RPCs. All require the caller to be a member of the target
// account; the server resolves account membership from the bearer token. // 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.WithSchema(pluginRegistryServiceMethods.ByName("ListCategories")),
connect.WithHandlerOptions(opts...), connect.WithHandlerOptions(opts...),
) )
pluginRegistryServiceListTagsHandler := connect.NewUnaryHandler(
PluginRegistryServiceListTagsProcedure,
svc.ListTags,
connect.WithSchema(pluginRegistryServiceMethods.ByName("ListTags")),
connect.WithHandlerOptions(opts...),
)
pluginRegistryServiceSubmitForReviewHandler := connect.NewUnaryHandler( pluginRegistryServiceSubmitForReviewHandler := connect.NewUnaryHandler(
PluginRegistryServiceSubmitForReviewProcedure, PluginRegistryServiceSubmitForReviewProcedure,
svc.SubmitForReview, svc.SubmitForReview,
@ -546,6 +569,8 @@ func NewPluginRegistryServiceHandler(svc PluginRegistryServiceHandler, opts ...c
pluginRegistryServiceResolveInstallHandler.ServeHTTP(w, r) pluginRegistryServiceResolveInstallHandler.ServeHTTP(w, r)
case PluginRegistryServiceListCategoriesProcedure: case PluginRegistryServiceListCategoriesProcedure:
pluginRegistryServiceListCategoriesHandler.ServeHTTP(w, r) pluginRegistryServiceListCategoriesHandler.ServeHTTP(w, r)
case PluginRegistryServiceListTagsProcedure:
pluginRegistryServiceListTagsHandler.ServeHTTP(w, r)
case PluginRegistryServiceSubmitForReviewProcedure: case PluginRegistryServiceSubmitForReviewProcedure:
pluginRegistryServiceSubmitForReviewHandler.ServeHTTP(w, r) pluginRegistryServiceSubmitForReviewHandler.ServeHTTP(w, r)
case PluginRegistryServiceListPrivatePluginsProcedure: 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")) 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) { 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")) 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