diff --git a/cmd/ninja/cmd/plugin.go b/cmd/ninja/cmd/plugin.go index 063911e..9598784 100644 --- a/cmd/ninja/cmd/plugin.go +++ b/cmd/ninja/cmd/plugin.go @@ -360,6 +360,10 @@ func newPluginInitCmd() *cobra.Command { return err } } + tags, err := promptTagsWithDefault(ctx, cli, scanner, kind, existing.Plugin.Tags) + if err != nil { + return err + } createReq := &v1.CreatePluginRequest{ ScopeSlug: scopeAPISlug(scope), @@ -385,7 +389,7 @@ func newPluginInitCmd() *cobra.Command { if private { modScope = "" } - if err := upsertPluginMod(modScope, name, displayName, description, kind, cats, nil, private); err != nil { + if err := upsertPluginMod(modScope, name, displayName, description, kind, cats, tags, private); err != nil { return err } fmt.Println("plugin.mod updated") @@ -600,6 +604,57 @@ func promptCategoriesWithDefault(ctx context.Context, cli *orchclient.Client, sc return out, nil } +// promptTagsWithDefault prompts the user for free-form tags. Best-effort fetches +// the top-20 most-used tags via ListTags(kind) to surface popular suggestions; +// if that call fails (offline, etc.) it falls back to a plain prompt with a +// one-line warning. Empty input keeps `current`. Validates with NormalizeTags; +// on error, prints the issue and reprompts (one re-try, then bails). +func promptTagsWithDefault(ctx context.Context, cli *orchclient.Client, scanner *bufio.Scanner, kind string, current []string) ([]string, error) { + // Best-effort popular-tags lookup. + var popular string + // TODO(tags): re-enable once core regenerates proto bindings to expose v1.ListTagsRequest (plan Task 12). + // if resp, err := cli.Reg.ListTags(ctx, connect.NewRequest(&v1.ListTagsRequest{Kind: kind, Limit: 20})); err == nil { + // 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)) + // } + // popular = strings.Join(parts, ", ") + // } + _ = ctx + _ = cli + + for attempt := 0; attempt < 2; attempt++ { + if len(current) > 0 { + fmt.Printf("Tags (current: %s)\n", strings.Join(current, ", ")) + } else { + fmt.Println("Tags (current: none)") + } + if popular != "" { + fmt.Printf("Popular: %s\n", popular) + } else if attempt == 0 { + fmt.Println("(could not fetch popular tags — offline or unauthenticated)") + } + fmt.Print("Enter comma-separated tags (blank to keep current): ") + if !scanner.Scan() { + return nil, fmt.Errorf("cancelled") + } + raw := strings.TrimSpace(scanner.Text()) + if raw == "" { + return current, nil + } + parts := strings.Split(raw, ",") + out, err := core.NormalizeTags(parts) + if err == nil { + return out, nil + } + fmt.Printf(" %s\n", err.Error()) + if attempt == 0 { + fmt.Println(" Try again:") + } + } + return nil, fmt.Errorf("tag input failed validation after 2 attempts") +} + // pickedSet returns a set of the given slugs for O(1) membership checks. func pickedSet(slugs []string) map[string]struct{} { s := make(map[string]struct{}, len(slugs))