Compare commits
3 Commits
feeb31139d
...
b5e39121d2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5e39121d2 | ||
|
|
64d81bd93f | ||
|
|
e8aba45f21 |
94
CLAUDE.md
Normal file
94
CLAUDE.md
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
# `ninja` — BlockNinja developer CLI
|
||||||
|
|
||||||
|
The `ninja` command: build/verify/publish CMS plugins & themes, drive the plugin
|
||||||
|
registry, and capture theme preview screenshots. Its own repo since WO-WZ-023
|
||||||
|
(remote `git.dev.alexdunmow.com/block/cli`); formerly lived inside `cms`.
|
||||||
|
|
||||||
|
Go module `git.dev.alexdunmow.com/block/cli` (Go 1.26). Cobra CLI. Talks to the
|
||||||
|
orchestrator over Connect-RPC; builds `.bnp` artifacts with `block/core` +
|
||||||
|
wazero; screenshots with chromedp.
|
||||||
|
|
||||||
|
## Build / install / test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make build # → ./bin/ninja
|
||||||
|
make install # go install ./cmd/ninja → $GOBIN (yields `ninja` on PATH)
|
||||||
|
make test # go test ./... (compiles guest wasm fixtures: needs GOOS=wasip1 + core proxy)
|
||||||
|
make proto # regenerate the vendored orchestrator client (see below)
|
||||||
|
make safety-check # cd ../check-safety && go run . ../cli
|
||||||
|
```
|
||||||
|
|
||||||
|
> **`make install` matters.** `ninja plugin publish` / `ninja plugin build` use
|
||||||
|
> the binary on your PATH, not the repo. After changing build/pack behaviour you
|
||||||
|
> MUST `make install` before any publish or the old binary ships. The classic
|
||||||
|
> failure: an installed binary that predates `.bnp` preview-packing silently
|
||||||
|
> republishes a theme **without** its `preview.png`, wiping the live preview.
|
||||||
|
> Verify with `ninja theme screenshot --help | grep screenshots-dir` and by
|
||||||
|
> extracting a fresh `.bnp` (`ninja plugin verify <file.bnp>` → `preview=true`).
|
||||||
|
|
||||||
|
## Command surface
|
||||||
|
|
||||||
|
- `ninja plugin build | verify <f.bnp> | publish | init | status | bump [major|minor|patch]`
|
||||||
|
- `ninja plugin list | delete | delete-version | pull | version | abi`
|
||||||
|
- `ninja plugin tags add|rm|set|clear`
|
||||||
|
- `ninja scope create|default|set|list`
|
||||||
|
- `ninja account list|set|show` · `ninja login | whoami | logout`
|
||||||
|
- `ninja theme screenshot` (preview + `--pages`/`--screenshots-dir` multi-page capture)
|
||||||
|
|
||||||
|
## `.bnp` artifacts
|
||||||
|
|
||||||
|
A `.bnp` is a **zstd-compressed tar** (NOT gzip — `tar tzf` won't read it; use
|
||||||
|
`tar tf` or `ninja plugin verify`). `internal/bnp` owns build+verify; wasm
|
||||||
|
plugins and codeless plugins/themes both pack into one. Optionally packs a root
|
||||||
|
`preview.png` and a `screenshots/` dir (validated: png/jpg/jpeg/webp, ≤8MB/file,
|
||||||
|
≤12 files, no subdirs). `internal/shot` renders pages via chromedp for
|
||||||
|
`ninja theme screenshot`.
|
||||||
|
|
||||||
|
**Publish builds the artifact itself.** `ninja plugin publish` builds the `.bnp`
|
||||||
|
and ships it — the artifact is the ONLY publish form (the legacy source-archive
|
||||||
|
path is deleted). `--bnp <file>` ships a prebuilt one. Instances download the
|
||||||
|
artifact and never compile.
|
||||||
|
|
||||||
|
## Orchestrator target (dev vs prod)
|
||||||
|
|
||||||
|
Global `--host` flag: orchestrator base URL. **Default is
|
||||||
|
`https://my.blockninjacms.com` — PRODUCTION.** For local dev always pass
|
||||||
|
`--host https://my.localdev.blockninjacms.com` (or set it as the active host via
|
||||||
|
`ninja login`). Never publish dev/test plugins to the prod registry by omitting
|
||||||
|
`--host`.
|
||||||
|
|
||||||
|
Auth is **device flow** (`ninja login` → `AuthService.StartDevice`/`PollDevice`).
|
||||||
|
Credentials (per-host token + active account) persist to
|
||||||
|
`<os.UserConfigDir>/ninja/credentials.json` (Linux: `~/.config/ninja/…`).
|
||||||
|
`internal/creds` reads/writes it; `internal/orchclient` builds the Connect client.
|
||||||
|
|
||||||
|
## Vendored orchestrator proto (NOT a submodule)
|
||||||
|
|
||||||
|
`proto/orchestrator/v1/plugin_registry.proto` is a **hand-vendored COPY** of the
|
||||||
|
orchestrator's proto, not a git submodule. `make proto` regenerates
|
||||||
|
`internal/api/orchestrator/v1/` from it. `buf.gen.yaml` runs in **managed mode**
|
||||||
|
and overrides `go_package_prefix` → `git.dev.alexdunmow.com/block/cli/internal/api`
|
||||||
|
so the generated Go lands in this module regardless of the proto's declared
|
||||||
|
`go_package`. Needs `buf` + `protoc-gen-go` + `protoc-gen-connect-go` on PATH.
|
||||||
|
|
||||||
|
**To pick up new orchestrator RPCs** (e.g. registry gallery/review methods): copy
|
||||||
|
the updated `plugin_registry.proto` (and any new proto) from the orchestrator's
|
||||||
|
proto into `proto/`, run `make proto`, then add client methods in
|
||||||
|
`internal/orchclient`. There is no automatic sync — the copy is deliberate so the
|
||||||
|
CLI pins a known server surface.
|
||||||
|
|
||||||
|
## Workspace couplings
|
||||||
|
|
||||||
|
- `make safety-check` shells into the sibling `../check-safety` tool — the CLI is
|
||||||
|
scanned like the other repos. Run it before publishing CLI changes.
|
||||||
|
- Depends on `git.dev.alexdunmow.com/block/core` (the plugin SDK) via the Gitea
|
||||||
|
module proxy. **Never** use `replace` directives — tag/push `core`, then bump.
|
||||||
|
- The `.bnp` format + pack/validation rules are shared truth with the CMS host
|
||||||
|
loader and the orchestrator registry ingestion; changing them is a cross-repo
|
||||||
|
contract change.
|
||||||
|
|
||||||
|
## Working in here
|
||||||
|
|
||||||
|
- Standalone repo: commit/push only within `cli/`. Work on `main`.
|
||||||
|
- Ask before publishing anything to a registry — publish is outward-facing and
|
||||||
|
hits a real orchestrator (prod by default).
|
||||||
@ -37,6 +37,7 @@ func newPluginCmd() *cobra.Command {
|
|||||||
newPluginBumpCmd(),
|
newPluginBumpCmd(),
|
||||||
newPluginVersionCmd(),
|
newPluginVersionCmd(),
|
||||||
newPluginTagsCmd(),
|
newPluginTagsCmd(),
|
||||||
|
newPluginGalleryCmd(),
|
||||||
)
|
)
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|||||||
@ -98,6 +98,8 @@ func printBuildSummary(r *bnp.BuildResult) {
|
|||||||
fmt.Printf(" │ job types %d\n", r.JobTypes)
|
fmt.Printf(" │ job types %d\n", r.JobTypes)
|
||||||
fmt.Printf(" │ hooks %s\n", hooks)
|
fmt.Printf(" │ hooks %s\n", hooks)
|
||||||
fmt.Printf(" │ bundled dirs %s\n", dirs)
|
fmt.Printf(" │ bundled dirs %s\n", dirs)
|
||||||
|
fmt.Printf(" │ preview.png %t\n", r.PreviewIncluded)
|
||||||
|
fmt.Printf(" │ screenshots %d\n", r.ScreenshotCount)
|
||||||
fmt.Printf(" │ data_dir grant %t\n", r.DataDir)
|
fmt.Printf(" │ data_dir grant %t\n", r.DataDir)
|
||||||
fmt.Println(" └───────────────────────────────────────────")
|
fmt.Println(" └───────────────────────────────────────────")
|
||||||
}
|
}
|
||||||
@ -121,8 +123,8 @@ func printVerifySummary(path string, r *bnp.VerifyResult) {
|
|||||||
joined = strings.Join(dirs, ", ")
|
joined = strings.Join(dirs, ", ")
|
||||||
}
|
}
|
||||||
fmt.Printf("OK: %s\n", path)
|
fmt.Printf("OK: %s\n", path)
|
||||||
fmt.Printf(" name=%s version=%s abi_version=%d blocks=%d data_dir=%t dirs=[%s]\n",
|
fmt.Printf(" name=%s version=%s abi_version=%d blocks=%d data_dir=%t dirs=[%s] preview=%t screenshots=%d\n",
|
||||||
r.Name, r.Version, r.ABIVersion, r.BlockCount, r.DataDir, joined)
|
r.Name, r.Version, r.ABIVersion, r.BlockCount, r.DataDir, joined, r.HasPreview, r.ScreenshotCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
// humanBytes formats a byte count with a binary unit suffix.
|
// humanBytes formats a byte count with a binary unit suffix.
|
||||||
|
|||||||
339
cmd/ninja/cmd/plugin_gallery.go
Normal file
339
cmd/ninja/cmd/plugin_gallery.go
Normal file
@ -0,0 +1,339 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"connectrpc.com/connect"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
core "git.dev.alexdunmow.com/block/core/plugin"
|
||||||
|
|
||||||
|
v1 "git.dev.alexdunmow.com/block/cli/internal/api/orchestrator/v1"
|
||||||
|
"git.dev.alexdunmow.com/block/cli/internal/creds"
|
||||||
|
"git.dev.alexdunmow.com/block/cli/internal/orchclient"
|
||||||
|
)
|
||||||
|
|
||||||
|
// maxScreenshotBytes mirrors the orchestrator's UploadScreenshot read cap
|
||||||
|
// (connect.WithReadMaxBytes(8<<20)); we reject oversize files client-side so
|
||||||
|
// the failure is a clear message instead of a truncated-stream RPC error.
|
||||||
|
const maxScreenshotBytes = 8 << 20
|
||||||
|
|
||||||
|
// newPluginGalleryCmd builds the `ninja plugin gallery` command group for
|
||||||
|
// curating a plugin/theme's registry screenshot gallery. Reads are public;
|
||||||
|
// writes require scope membership (or owning-account membership for a private
|
||||||
|
// plugin) — the orchestrator enforces this per-handler. The target plugin
|
||||||
|
// defaults to the one described by plugin.mod in the working directory; the
|
||||||
|
// --scope/--name flags override the coordinates so the gallery of any plugin
|
||||||
|
// you can manage can be curated from anywhere.
|
||||||
|
func newPluginGalleryCmd() *cobra.Command {
|
||||||
|
var scopeFlag, nameFlag string
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "gallery",
|
||||||
|
Short: "Manage a plugin's registry screenshot gallery",
|
||||||
|
Long: `Curate the screenshot gallery shown on a plugin or theme's registry
|
||||||
|
detail page.
|
||||||
|
|
||||||
|
The target plugin defaults to the one described by plugin.mod in the current
|
||||||
|
directory. Use --scope/--name to target a different plugin.
|
||||||
|
|
||||||
|
Subcommands:
|
||||||
|
list Show the current gallery (public read)
|
||||||
|
add <image> Upload an image (png/jpg/jpeg/webp, <=8MiB, <=12 total)
|
||||||
|
remove <screenshot-id> Delete a screenshot
|
||||||
|
reorder <id>... Reorder the gallery (must list every current image once)
|
||||||
|
set-featured <id> Mark one screenshot featured (drives the card preview)`,
|
||||||
|
}
|
||||||
|
cmd.PersistentFlags().StringVar(&scopeFlag, "scope", "", "Target scope slug (default: plugin.mod scope)")
|
||||||
|
cmd.PersistentFlags().StringVar(&nameFlag, "name", "", "Target plugin name (default: plugin.mod name)")
|
||||||
|
|
||||||
|
cmd.AddCommand(
|
||||||
|
newGalleryListCmd(),
|
||||||
|
newGalleryAddCmd(),
|
||||||
|
newGalleryRemoveCmd(),
|
||||||
|
newGalleryReorderCmd(),
|
||||||
|
newGallerySetFeaturedCmd(),
|
||||||
|
)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// galleryTarget is the resolved address of a plugin's gallery: a bare scope
|
||||||
|
// slug (no leading @), the plugin name, and — for a private plugin — the
|
||||||
|
// active account it belongs to.
|
||||||
|
type galleryTarget struct {
|
||||||
|
scopeSlug string
|
||||||
|
name string
|
||||||
|
private bool
|
||||||
|
accountID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveGalleryTarget derives the target plugin from plugin.mod (when present
|
||||||
|
// in the working directory) with --scope/--name overrides. A missing plugin.mod
|
||||||
|
// is not fatal as long as the flags supply scope+name.
|
||||||
|
func resolveGalleryTarget(c *cobra.Command, hc creds.HostCreds) (galleryTarget, error) {
|
||||||
|
scopeFlag, _ := c.Flags().GetString("scope")
|
||||||
|
nameFlag, _ := c.Flags().GetString("name")
|
||||||
|
|
||||||
|
var scope, name string
|
||||||
|
var private bool
|
||||||
|
if mod, err := readLocalMod(); err == nil {
|
||||||
|
scope = mod.Plugin.Scope
|
||||||
|
name = mod.Plugin.Name
|
||||||
|
private = mod.Plugin.Private
|
||||||
|
}
|
||||||
|
// Flag overrides. An explicit --scope names a public scope; the @private
|
||||||
|
// namespace is reached only via plugin.mod's private flag or by naming the
|
||||||
|
// private scope directly.
|
||||||
|
if scopeFlag != "" {
|
||||||
|
scope = scopeFlag
|
||||||
|
private = false
|
||||||
|
}
|
||||||
|
if nameFlag != "" {
|
||||||
|
name = nameFlag
|
||||||
|
}
|
||||||
|
|
||||||
|
scopeSlug := strings.TrimPrefix(strings.TrimSpace(scope), "@")
|
||||||
|
if scopeSlug == core.PrivateScopeSlug {
|
||||||
|
private = true
|
||||||
|
}
|
||||||
|
|
||||||
|
t := galleryTarget{scopeSlug: scopeSlug, name: strings.TrimSpace(name), private: private, accountID: hc.ActiveAccountID}
|
||||||
|
if t.name == "" {
|
||||||
|
return t, fmt.Errorf("no plugin name: run inside a plugin dir (with plugin.mod) or pass --name")
|
||||||
|
}
|
||||||
|
if t.private {
|
||||||
|
t.scopeSlug = core.PrivateScopeSlug
|
||||||
|
if t.accountID == "" {
|
||||||
|
return t, fmt.Errorf("private plugin gallery requires an active account; run `ninja account set <slug>`")
|
||||||
|
}
|
||||||
|
} else if t.scopeSlug == "" {
|
||||||
|
return t, fmt.Errorf("no scope: run inside a plugin dir (with plugin.mod) or pass --scope")
|
||||||
|
}
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolvePluginID looks up the target's registry plugin ID, which every write
|
||||||
|
// RPC keys on. Mirrors the publish flow's GetPlugin call (private plugins are
|
||||||
|
// addressed by the @private scope plus the active account).
|
||||||
|
func (t galleryTarget) resolvePluginID(ctx context.Context, cli *orchclient.Client) (string, error) {
|
||||||
|
req := &v1.GetPluginRequest{ScopeSlug: t.scopeSlug, Name: t.name}
|
||||||
|
if t.private {
|
||||||
|
req.ScopeSlug = core.PrivateScopeSlug
|
||||||
|
req.ActiveAccountId = t.accountID
|
||||||
|
}
|
||||||
|
pr, err := cli.Reg.GetPlugin(ctx, connect.NewRequest(req))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("get plugin: %w", err)
|
||||||
|
}
|
||||||
|
return pr.Msg.Plugin.Id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGalleryListCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "Show the current gallery for the target plugin",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(c *cobra.Command, _ []string) error {
|
||||||
|
cli, _, _, hc, err := resolveClient(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t, err := resolveGalleryTarget(c, hc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resp, err := cli.Gallery.ListScreenshots(context.Background(), connect.NewRequest(&v1.ListScreenshotsRequest{
|
||||||
|
ScopeSlug: t.scopeSlug,
|
||||||
|
PluginName: t.name,
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list screenshots: %w", err)
|
||||||
|
}
|
||||||
|
shots := resp.Msg.Screenshots
|
||||||
|
if len(shots) == 0 {
|
||||||
|
fmt.Println("(no screenshots — add one with `ninja plugin gallery add <image>`)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, s := range shots {
|
||||||
|
marker := " "
|
||||||
|
if s.Featured {
|
||||||
|
marker = "*"
|
||||||
|
}
|
||||||
|
fmt.Printf("%2d.%s %s %s %s\n", s.Position, marker, s.Id, s.ContentType, s.Url)
|
||||||
|
}
|
||||||
|
fmt.Println("\n(* = featured — drives the registry card preview image)")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGalleryAddCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "add <image>",
|
||||||
|
Short: "Upload an image to the gallery (png/jpg/jpeg/webp, <=8MiB)",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(c *cobra.Command, args []string) error {
|
||||||
|
cli, _, _, hc, err := resolveClient(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t, err := resolveGalleryTarget(c, hc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
path := args[0]
|
||||||
|
contentType, err := imageContentType(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read image: %w", err)
|
||||||
|
}
|
||||||
|
if len(data) == 0 {
|
||||||
|
return fmt.Errorf("%s is empty", path)
|
||||||
|
}
|
||||||
|
if len(data) > maxScreenshotBytes {
|
||||||
|
return fmt.Errorf("%s is %d bytes; the gallery limit is %d (8MiB)", path, len(data), maxScreenshotBytes)
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
pluginID, err := t.resolvePluginID(ctx, cli)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resp, err := cli.Gallery.UploadScreenshot(ctx, connect.NewRequest(&v1.UploadScreenshotRequest{
|
||||||
|
PluginId: pluginID,
|
||||||
|
Image: data,
|
||||||
|
Filename: filepath.Base(path),
|
||||||
|
ContentType: contentType,
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("upload screenshot: %w", err)
|
||||||
|
}
|
||||||
|
s := resp.Msg.Screenshot
|
||||||
|
fmt.Printf("Uploaded %s (id=%s, position=%d)\n", filepath.Base(path), s.Id, s.Position)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGalleryRemoveCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "remove <screenshot-id>",
|
||||||
|
Short: "Delete a screenshot from the gallery",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(c *cobra.Command, args []string) error {
|
||||||
|
cli, _, _, hc, err := resolveClient(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t, err := resolveGalleryTarget(c, hc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
pluginID, err := t.resolvePluginID(ctx, cli)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := cli.Gallery.DeleteScreenshot(ctx, connect.NewRequest(&v1.DeleteScreenshotRequest{
|
||||||
|
PluginId: pluginID,
|
||||||
|
ScreenshotId: args[0],
|
||||||
|
})); err != nil {
|
||||||
|
return fmt.Errorf("delete screenshot: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Deleted screenshot %s\n", args[0])
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGalleryReorderCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "reorder <screenshot-id>...",
|
||||||
|
Short: "Reorder the gallery (list every current screenshot id exactly once)",
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
RunE: func(c *cobra.Command, args []string) error {
|
||||||
|
cli, _, _, hc, err := resolveClient(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t, err := resolveGalleryTarget(c, hc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
pluginID, err := t.resolvePluginID(ctx, cli)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resp, err := cli.Gallery.ReorderScreenshots(ctx, connect.NewRequest(&v1.ReorderScreenshotsRequest{
|
||||||
|
PluginId: pluginID,
|
||||||
|
ScreenshotIdsInOrder: args,
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reorder screenshots: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Reordered. New order:")
|
||||||
|
for _, s := range resp.Msg.Screenshots {
|
||||||
|
marker := " "
|
||||||
|
if s.Featured {
|
||||||
|
marker = "*"
|
||||||
|
}
|
||||||
|
fmt.Printf("%2d.%s %s\n", s.Position, marker, s.Id)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGallerySetFeaturedCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "set-featured <screenshot-id>",
|
||||||
|
Short: "Mark one screenshot featured (drives the registry card preview)",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(c *cobra.Command, args []string) error {
|
||||||
|
cli, _, _, hc, err := resolveClient(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t, err := resolveGalleryTarget(c, hc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
pluginID, err := t.resolvePluginID(ctx, cli)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := cli.Gallery.SetFeaturedScreenshot(ctx, connect.NewRequest(&v1.SetFeaturedScreenshotRequest{
|
||||||
|
PluginId: pluginID,
|
||||||
|
ScreenshotId: args[0],
|
||||||
|
})); err != nil {
|
||||||
|
return fmt.Errorf("set featured screenshot: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Set screenshot %s as featured\n", args[0])
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// imageContentType maps a file's extension to the content type the gallery
|
||||||
|
// accepts, rejecting anything outside the png/jpg/jpeg/webp allowlist the
|
||||||
|
// orchestrator enforces. Detecting from the extension (rather than sniffing
|
||||||
|
// bytes) keeps the CLI's rejection message aligned with the server's rule.
|
||||||
|
func imageContentType(path string) (string, error) {
|
||||||
|
switch strings.ToLower(filepath.Ext(path)) {
|
||||||
|
case ".png":
|
||||||
|
return "image/png", nil
|
||||||
|
case ".jpg", ".jpeg":
|
||||||
|
return "image/jpeg", nil
|
||||||
|
case ".webp":
|
||||||
|
return "image/webp", nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unsupported image type %q: gallery accepts png, jpg, jpeg, webp", filepath.Ext(path))
|
||||||
|
}
|
||||||
|
}
|
||||||
191
cmd/ninja/cmd/plugin_gallery_test.go
Normal file
191
cmd/ninja/cmd/plugin_gallery_test.go
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
core "git.dev.alexdunmow.com/block/core/plugin"
|
||||||
|
"git.dev.alexdunmow.com/block/cli/internal/creds"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestImageContentType(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
path string
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{path: "preview.png", want: "image/png"},
|
||||||
|
{path: "shot.jpg", want: "image/jpeg"},
|
||||||
|
{path: "shot.jpeg", want: "image/jpeg"},
|
||||||
|
{path: "shot.webp", want: "image/webp"},
|
||||||
|
{path: "SHOT.PNG", want: "image/png"}, // case-insensitive
|
||||||
|
{path: "dir/nested/x.jpg", want: "image/jpeg"},
|
||||||
|
{path: "shot.gif", wantErr: true},
|
||||||
|
{path: "shot.svg", wantErr: true},
|
||||||
|
{path: "noext", wantErr: true},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
got, err := imageContentType(c.path)
|
||||||
|
if c.wantErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("imageContentType(%q) = %q, want error", c.path, got)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("imageContentType(%q) err: %v", c.path, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if got != c.want {
|
||||||
|
t.Errorf("imageContentType(%q) = %q, want %q", c.path, got, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// galleryTestCmd builds a command carrying the --scope/--name flags exactly as
|
||||||
|
// the gallery parent registers them, so resolveGalleryTarget can read them.
|
||||||
|
func galleryTestCmd(scope, name string) *cobra.Command {
|
||||||
|
c := &cobra.Command{Use: "gallery"}
|
||||||
|
c.Flags().String("scope", "", "")
|
||||||
|
c.Flags().String("name", "", "")
|
||||||
|
if scope != "" {
|
||||||
|
_ = c.Flags().Set("scope", scope)
|
||||||
|
}
|
||||||
|
if name != "" {
|
||||||
|
_ = c.Flags().Set("name", name)
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeGalleryMod(t *testing.T, dir, scope, name string, private bool) {
|
||||||
|
t.Helper()
|
||||||
|
m := &core.ModFile{Plugin: core.ModPlugin{
|
||||||
|
Name: name,
|
||||||
|
Scope: scope,
|
||||||
|
Version: "0.1.0",
|
||||||
|
Private: private,
|
||||||
|
}}
|
||||||
|
if err := writeMod(filepath.Join(dir, "plugin.mod"), m); err != nil {
|
||||||
|
t.Fatalf("writeMod: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveGalleryTarget_FromMod(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
t.Chdir(dir)
|
||||||
|
writeGalleryMod(t, dir, "@themes", "gotham", false)
|
||||||
|
|
||||||
|
got, err := resolveGalleryTarget(galleryTestCmd("", ""), creds.HostCreds{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolveGalleryTarget: %v", err)
|
||||||
|
}
|
||||||
|
// Leading @ must be trimmed (ListScreenshots does not trim server-side).
|
||||||
|
if got.scopeSlug != "themes" || got.name != "gotham" || got.private {
|
||||||
|
t.Errorf("got %+v, want scopeSlug=themes name=gotham private=false", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveGalleryTarget_FlagOverride(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
t.Chdir(dir)
|
||||||
|
writeGalleryMod(t, dir, "@themes", "gotham", false)
|
||||||
|
|
||||||
|
got, err := resolveGalleryTarget(galleryTestCmd("acme", "widget"), creds.HostCreds{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolveGalleryTarget: %v", err)
|
||||||
|
}
|
||||||
|
if got.scopeSlug != "acme" || got.name != "widget" {
|
||||||
|
t.Errorf("got %+v, want scopeSlug=acme name=widget", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveGalleryTarget_NoModNeedsFlags(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
t.Chdir(dir) // no plugin.mod written
|
||||||
|
|
||||||
|
// Flags supply both coordinates → resolves without a plugin.mod.
|
||||||
|
got, err := resolveGalleryTarget(galleryTestCmd("acme", "widget"), creds.HostCreds{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolveGalleryTarget with flags: %v", err)
|
||||||
|
}
|
||||||
|
if got.scopeSlug != "acme" || got.name != "widget" {
|
||||||
|
t.Errorf("got %+v, want scopeSlug=acme name=widget", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// No plugin.mod and no flags → an actionable error.
|
||||||
|
if _, err := resolveGalleryTarget(galleryTestCmd("", ""), creds.HostCreds{}); err == nil {
|
||||||
|
t.Error("expected error with no plugin.mod and no flags")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveGalleryTarget_MissingScope(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
t.Chdir(dir)
|
||||||
|
|
||||||
|
// Name but no scope (public) → error naming the missing scope.
|
||||||
|
_, err := resolveGalleryTarget(galleryTestCmd("", "widget"), creds.HostCreds{})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for name without scope")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveGalleryTarget_PrivateFromMod(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
t.Chdir(dir)
|
||||||
|
writeGalleryMod(t, dir, "", "widget", true)
|
||||||
|
|
||||||
|
// Private plugin without an active account → error.
|
||||||
|
if _, err := resolveGalleryTarget(galleryTestCmd("", ""), creds.HostCreds{}); err == nil {
|
||||||
|
t.Error("expected error: private gallery needs an active account")
|
||||||
|
}
|
||||||
|
|
||||||
|
// With an active account, the scope resolves to the @private namespace.
|
||||||
|
got, err := resolveGalleryTarget(galleryTestCmd("", ""), creds.HostCreds{ActiveAccountID: "acct-1"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolveGalleryTarget private: %v", err)
|
||||||
|
}
|
||||||
|
if !got.private || got.scopeSlug != core.PrivateScopeSlug || got.accountID != "acct-1" {
|
||||||
|
t.Errorf("got %+v, want private=true scopeSlug=%s accountID=acct-1", got, core.PrivateScopeSlug)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveGalleryTarget_PrivateScopeNameImpliesPrivate(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
t.Chdir(dir)
|
||||||
|
|
||||||
|
// Naming the private scope directly flips private on (and still needs an account).
|
||||||
|
got, err := resolveGalleryTarget(galleryTestCmd("@"+core.PrivateScopeSlug, "widget"), creds.HostCreds{ActiveAccountID: "acct-1"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolveGalleryTarget: %v", err)
|
||||||
|
}
|
||||||
|
if !got.private || got.scopeSlug != core.PrivateScopeSlug {
|
||||||
|
t.Errorf("got %+v, want private=true scopeSlug=%s", got, core.PrivateScopeSlug)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveGalleryTarget_ReadFileHelper(t *testing.T) {
|
||||||
|
// Guard that a valid image on disk round-trips through the same os.ReadFile
|
||||||
|
// path `add` uses (content-type derived from the extension).
|
||||||
|
dir := t.TempDir()
|
||||||
|
path := filepath.Join(dir, "preview.png")
|
||||||
|
if err := os.WriteFile(path, []byte("\x89PNG\r\n\x1a\nfake"), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ct, err := imageContentType(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("imageContentType: %v", err)
|
||||||
|
}
|
||||||
|
if ct != "image/png" {
|
||||||
|
t.Errorf("content type = %q, want image/png", ct)
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read: %v", err)
|
||||||
|
}
|
||||||
|
if len(data) == 0 || len(data) > maxScreenshotBytes {
|
||||||
|
t.Errorf("unexpected size %d", len(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,6 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@ -21,6 +23,7 @@ func newThemeCmd() *cobra.Command {
|
|||||||
|
|
||||||
func newThemeScreenshotCmd() *cobra.Command {
|
func newThemeScreenshotCmd() *cobra.Command {
|
||||||
var gallery, slug, out, mobileOut, themeName, waitSelector string
|
var gallery, slug, out, mobileOut, themeName, waitSelector string
|
||||||
|
var pages, screenshotsDir string
|
||||||
var mobile bool
|
var mobile bool
|
||||||
var width, height int
|
var width, height int
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@ -92,6 +95,39 @@ the rest of the CLI.`,
|
|||||||
}
|
}
|
||||||
fmt.Printf("wrote %s (%d bytes)\n", mobileOut, len(mpng))
|
fmt.Printf("wrote %s (%d bytes)\n", mobileOut, len(mpng))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Multi-page gallery capture (--pages): one PNG per slug into
|
||||||
|
// --screenshots-dir, named NN-<slug>.png. These are the source
|
||||||
|
// files the .bnp packer ships under screenshots/ and the registry
|
||||||
|
// ingests into the plugin gallery at publish.
|
||||||
|
if pages != "" {
|
||||||
|
if err := os.MkdirAll(screenshotsDir, 0o755); err != nil {
|
||||||
|
return fmt.Errorf("create %s: %w", screenshotsDir, err)
|
||||||
|
}
|
||||||
|
for i, pageSlug := range strings.Split(pages, ",") {
|
||||||
|
pageSlug = strings.TrimSpace(pageSlug)
|
||||||
|
if pageSlug == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pURL := shot.PreviewURL(gallery, pageSlug, themeName)
|
||||||
|
fmt.Fprintf(os.Stderr, "capturing page %s: %s\n", pageSlug, pURL)
|
||||||
|
ppng, err := shot.Capture(ctx, shot.Options{
|
||||||
|
URL: pURL,
|
||||||
|
Width: width,
|
||||||
|
Height: height,
|
||||||
|
WaitSelector: waitSelector,
|
||||||
|
Timeout: 45 * time.Second,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dest := filepath.Join(screenshotsDir, fmt.Sprintf("%02d-%s.png", i+1, slugFileName(pageSlug)))
|
||||||
|
if err := os.WriteFile(dest, ppng, 0o644); err != nil {
|
||||||
|
return fmt.Errorf("write %s: %w", dest, err)
|
||||||
|
}
|
||||||
|
fmt.Printf("wrote %s (%d bytes)\n", dest, len(ppng))
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -104,5 +140,27 @@ the rest of the CLI.`,
|
|||||||
cmd.Flags().BoolVar(&mobile, "mobile", false, "Also capture preview-mobile.png at a phone viewport")
|
cmd.Flags().BoolVar(&mobile, "mobile", false, "Also capture preview-mobile.png at a phone viewport")
|
||||||
cmd.Flags().IntVar(&width, "width", 1440, "Desktop viewport width")
|
cmd.Flags().IntVar(&width, "width", 1440, "Desktop viewport width")
|
||||||
cmd.Flags().IntVar(&height, "height", 900, "Desktop viewport height")
|
cmd.Flags().IntVar(&height, "height", 900, "Desktop viewport height")
|
||||||
|
cmd.Flags().StringVar(&pages, "pages", "", "Comma-separated page slugs to capture into --screenshots-dir (e.g. /,/blog,/about)")
|
||||||
|
cmd.Flags().StringVar(&screenshotsDir, "screenshots-dir", "screenshots", "Directory for --pages captures (packed into the .bnp)")
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// slugFileName turns a page slug into a filename fragment ("/" → "home",
|
||||||
|
// "/blog/post" → "blog-post").
|
||||||
|
func slugFileName(slug string) string {
|
||||||
|
s := strings.Trim(slug, "/")
|
||||||
|
if s == "" {
|
||||||
|
return "home"
|
||||||
|
}
|
||||||
|
s = strings.ReplaceAll(s, "/", "-")
|
||||||
|
var b strings.Builder
|
||||||
|
for _, r := range s {
|
||||||
|
switch {
|
||||||
|
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '-', r == '_':
|
||||||
|
b.WriteRune(r)
|
||||||
|
default:
|
||||||
|
b.WriteRune('-')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|||||||
@ -27,6 +27,10 @@ const (
|
|||||||
PluginRegistryServiceName = "orchestrator.v1.PluginRegistryService"
|
PluginRegistryServiceName = "orchestrator.v1.PluginRegistryService"
|
||||||
// PluginModerationServiceName is the fully-qualified name of the PluginModerationService service.
|
// PluginModerationServiceName is the fully-qualified name of the PluginModerationService service.
|
||||||
PluginModerationServiceName = "orchestrator.v1.PluginModerationService"
|
PluginModerationServiceName = "orchestrator.v1.PluginModerationService"
|
||||||
|
// PluginGalleryServiceName is the fully-qualified name of the PluginGalleryService service.
|
||||||
|
PluginGalleryServiceName = "orchestrator.v1.PluginGalleryService"
|
||||||
|
// PluginReviewServiceName is the fully-qualified name of the PluginReviewService service.
|
||||||
|
PluginReviewServiceName = "orchestrator.v1.PluginReviewService"
|
||||||
// PluginPublishServiceName is the fully-qualified name of the PluginPublishService service.
|
// PluginPublishServiceName is the fully-qualified name of the PluginPublishService service.
|
||||||
PluginPublishServiceName = "orchestrator.v1.PluginPublishService"
|
PluginPublishServiceName = "orchestrator.v1.PluginPublishService"
|
||||||
// PluginAuthServiceName is the fully-qualified name of the PluginAuthService service.
|
// PluginAuthServiceName is the fully-qualified name of the PluginAuthService service.
|
||||||
@ -107,6 +111,51 @@ const (
|
|||||||
// PluginModerationServiceRequestChangesProcedure is the fully-qualified name of the
|
// PluginModerationServiceRequestChangesProcedure is the fully-qualified name of the
|
||||||
// PluginModerationService's RequestChanges RPC.
|
// PluginModerationService's RequestChanges RPC.
|
||||||
PluginModerationServiceRequestChangesProcedure = "/orchestrator.v1.PluginModerationService/RequestChanges"
|
PluginModerationServiceRequestChangesProcedure = "/orchestrator.v1.PluginModerationService/RequestChanges"
|
||||||
|
// PluginModerationServiceListFlaggedReviewsProcedure is the fully-qualified name of the
|
||||||
|
// PluginModerationService's ListFlaggedReviews RPC.
|
||||||
|
PluginModerationServiceListFlaggedReviewsProcedure = "/orchestrator.v1.PluginModerationService/ListFlaggedReviews"
|
||||||
|
// PluginModerationServiceSoftDeleteReviewProcedure is the fully-qualified name of the
|
||||||
|
// PluginModerationService's SoftDeleteReview RPC.
|
||||||
|
PluginModerationServiceSoftDeleteReviewProcedure = "/orchestrator.v1.PluginModerationService/SoftDeleteReview"
|
||||||
|
// PluginModerationServiceSoftDeleteReplyProcedure is the fully-qualified name of the
|
||||||
|
// PluginModerationService's SoftDeleteReply RPC.
|
||||||
|
PluginModerationServiceSoftDeleteReplyProcedure = "/orchestrator.v1.PluginModerationService/SoftDeleteReply"
|
||||||
|
// PluginGalleryServiceListScreenshotsProcedure is the fully-qualified name of the
|
||||||
|
// PluginGalleryService's ListScreenshots RPC.
|
||||||
|
PluginGalleryServiceListScreenshotsProcedure = "/orchestrator.v1.PluginGalleryService/ListScreenshots"
|
||||||
|
// PluginGalleryServiceUploadScreenshotProcedure is the fully-qualified name of the
|
||||||
|
// PluginGalleryService's UploadScreenshot RPC.
|
||||||
|
PluginGalleryServiceUploadScreenshotProcedure = "/orchestrator.v1.PluginGalleryService/UploadScreenshot"
|
||||||
|
// PluginGalleryServiceDeleteScreenshotProcedure is the fully-qualified name of the
|
||||||
|
// PluginGalleryService's DeleteScreenshot RPC.
|
||||||
|
PluginGalleryServiceDeleteScreenshotProcedure = "/orchestrator.v1.PluginGalleryService/DeleteScreenshot"
|
||||||
|
// PluginGalleryServiceReorderScreenshotsProcedure is the fully-qualified name of the
|
||||||
|
// PluginGalleryService's ReorderScreenshots RPC.
|
||||||
|
PluginGalleryServiceReorderScreenshotsProcedure = "/orchestrator.v1.PluginGalleryService/ReorderScreenshots"
|
||||||
|
// PluginGalleryServiceSetFeaturedScreenshotProcedure is the fully-qualified name of the
|
||||||
|
// PluginGalleryService's SetFeaturedScreenshot RPC.
|
||||||
|
PluginGalleryServiceSetFeaturedScreenshotProcedure = "/orchestrator.v1.PluginGalleryService/SetFeaturedScreenshot"
|
||||||
|
// PluginReviewServiceListReviewsProcedure is the fully-qualified name of the PluginReviewService's
|
||||||
|
// ListReviews RPC.
|
||||||
|
PluginReviewServiceListReviewsProcedure = "/orchestrator.v1.PluginReviewService/ListReviews"
|
||||||
|
// PluginReviewServiceGetMyReviewProcedure is the fully-qualified name of the PluginReviewService's
|
||||||
|
// GetMyReview RPC.
|
||||||
|
PluginReviewServiceGetMyReviewProcedure = "/orchestrator.v1.PluginReviewService/GetMyReview"
|
||||||
|
// PluginReviewServiceUpsertReviewProcedure is the fully-qualified name of the PluginReviewService's
|
||||||
|
// UpsertReview RPC.
|
||||||
|
PluginReviewServiceUpsertReviewProcedure = "/orchestrator.v1.PluginReviewService/UpsertReview"
|
||||||
|
// PluginReviewServiceDeleteReviewProcedure is the fully-qualified name of the PluginReviewService's
|
||||||
|
// DeleteReview RPC.
|
||||||
|
PluginReviewServiceDeleteReviewProcedure = "/orchestrator.v1.PluginReviewService/DeleteReview"
|
||||||
|
// PluginReviewServiceReplyToReviewProcedure is the fully-qualified name of the
|
||||||
|
// PluginReviewService's ReplyToReview RPC.
|
||||||
|
PluginReviewServiceReplyToReviewProcedure = "/orchestrator.v1.PluginReviewService/ReplyToReview"
|
||||||
|
// PluginReviewServiceDeleteReplyProcedure is the fully-qualified name of the PluginReviewService's
|
||||||
|
// DeleteReply RPC.
|
||||||
|
PluginReviewServiceDeleteReplyProcedure = "/orchestrator.v1.PluginReviewService/DeleteReply"
|
||||||
|
// PluginReviewServiceFlagReviewProcedure is the fully-qualified name of the PluginReviewService's
|
||||||
|
// FlagReview RPC.
|
||||||
|
PluginReviewServiceFlagReviewProcedure = "/orchestrator.v1.PluginReviewService/FlagReview"
|
||||||
// PluginPublishServicePublishVersionProcedure is the fully-qualified name of the
|
// PluginPublishServicePublishVersionProcedure is the fully-qualified name of the
|
||||||
// PluginPublishService's PublishVersion RPC.
|
// PluginPublishService's PublishVersion RPC.
|
||||||
PluginPublishServicePublishVersionProcedure = "/orchestrator.v1.PluginPublishService/PublishVersion"
|
PluginPublishServicePublishVersionProcedure = "/orchestrator.v1.PluginPublishService/PublishVersion"
|
||||||
@ -714,6 +763,11 @@ type PluginModerationServiceClient interface {
|
|||||||
ApproveSubmission(context.Context, *connect.Request[v1.ApproveSubmissionRequest]) (*connect.Response[v1.ApproveSubmissionResponse], error)
|
ApproveSubmission(context.Context, *connect.Request[v1.ApproveSubmissionRequest]) (*connect.Response[v1.ApproveSubmissionResponse], error)
|
||||||
RejectSubmission(context.Context, *connect.Request[v1.RejectSubmissionRequest]) (*connect.Response[v1.RejectSubmissionResponse], error)
|
RejectSubmission(context.Context, *connect.Request[v1.RejectSubmissionRequest]) (*connect.Response[v1.RejectSubmissionResponse], error)
|
||||||
RequestChanges(context.Context, *connect.Request[v1.RequestChangesRequest]) (*connect.Response[v1.RequestChangesResponse], error)
|
RequestChanges(context.Context, *connect.Request[v1.RequestChangesRequest]) (*connect.Response[v1.RequestChangesResponse], error)
|
||||||
|
// Star-review moderation (ADR 0021). Unrelated to the submission queue
|
||||||
|
// above: these act on user reviews (PluginReviewService), not plugins.
|
||||||
|
ListFlaggedReviews(context.Context, *connect.Request[v1.ListFlaggedReviewsRequest]) (*connect.Response[v1.ListFlaggedReviewsResponse], error)
|
||||||
|
SoftDeleteReview(context.Context, *connect.Request[v1.SoftDeleteReviewRequest]) (*connect.Response[v1.SoftDeleteReviewResponse], error)
|
||||||
|
SoftDeleteReply(context.Context, *connect.Request[v1.SoftDeleteReplyRequest]) (*connect.Response[v1.SoftDeleteReplyResponse], error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPluginModerationServiceClient constructs a client for the
|
// NewPluginModerationServiceClient constructs a client for the
|
||||||
@ -751,6 +805,24 @@ func NewPluginModerationServiceClient(httpClient connect.HTTPClient, baseURL str
|
|||||||
connect.WithSchema(pluginModerationServiceMethods.ByName("RequestChanges")),
|
connect.WithSchema(pluginModerationServiceMethods.ByName("RequestChanges")),
|
||||||
connect.WithClientOptions(opts...),
|
connect.WithClientOptions(opts...),
|
||||||
),
|
),
|
||||||
|
listFlaggedReviews: connect.NewClient[v1.ListFlaggedReviewsRequest, v1.ListFlaggedReviewsResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+PluginModerationServiceListFlaggedReviewsProcedure,
|
||||||
|
connect.WithSchema(pluginModerationServiceMethods.ByName("ListFlaggedReviews")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
softDeleteReview: connect.NewClient[v1.SoftDeleteReviewRequest, v1.SoftDeleteReviewResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+PluginModerationServiceSoftDeleteReviewProcedure,
|
||||||
|
connect.WithSchema(pluginModerationServiceMethods.ByName("SoftDeleteReview")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
softDeleteReply: connect.NewClient[v1.SoftDeleteReplyRequest, v1.SoftDeleteReplyResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+PluginModerationServiceSoftDeleteReplyProcedure,
|
||||||
|
connect.WithSchema(pluginModerationServiceMethods.ByName("SoftDeleteReply")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -760,6 +832,9 @@ type pluginModerationServiceClient struct {
|
|||||||
approveSubmission *connect.Client[v1.ApproveSubmissionRequest, v1.ApproveSubmissionResponse]
|
approveSubmission *connect.Client[v1.ApproveSubmissionRequest, v1.ApproveSubmissionResponse]
|
||||||
rejectSubmission *connect.Client[v1.RejectSubmissionRequest, v1.RejectSubmissionResponse]
|
rejectSubmission *connect.Client[v1.RejectSubmissionRequest, v1.RejectSubmissionResponse]
|
||||||
requestChanges *connect.Client[v1.RequestChangesRequest, v1.RequestChangesResponse]
|
requestChanges *connect.Client[v1.RequestChangesRequest, v1.RequestChangesResponse]
|
||||||
|
listFlaggedReviews *connect.Client[v1.ListFlaggedReviewsRequest, v1.ListFlaggedReviewsResponse]
|
||||||
|
softDeleteReview *connect.Client[v1.SoftDeleteReviewRequest, v1.SoftDeleteReviewResponse]
|
||||||
|
softDeleteReply *connect.Client[v1.SoftDeleteReplyRequest, v1.SoftDeleteReplyResponse]
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListPendingReviews calls orchestrator.v1.PluginModerationService.ListPendingReviews.
|
// ListPendingReviews calls orchestrator.v1.PluginModerationService.ListPendingReviews.
|
||||||
@ -782,6 +857,21 @@ func (c *pluginModerationServiceClient) RequestChanges(ctx context.Context, req
|
|||||||
return c.requestChanges.CallUnary(ctx, req)
|
return c.requestChanges.CallUnary(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListFlaggedReviews calls orchestrator.v1.PluginModerationService.ListFlaggedReviews.
|
||||||
|
func (c *pluginModerationServiceClient) ListFlaggedReviews(ctx context.Context, req *connect.Request[v1.ListFlaggedReviewsRequest]) (*connect.Response[v1.ListFlaggedReviewsResponse], error) {
|
||||||
|
return c.listFlaggedReviews.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoftDeleteReview calls orchestrator.v1.PluginModerationService.SoftDeleteReview.
|
||||||
|
func (c *pluginModerationServiceClient) SoftDeleteReview(ctx context.Context, req *connect.Request[v1.SoftDeleteReviewRequest]) (*connect.Response[v1.SoftDeleteReviewResponse], error) {
|
||||||
|
return c.softDeleteReview.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoftDeleteReply calls orchestrator.v1.PluginModerationService.SoftDeleteReply.
|
||||||
|
func (c *pluginModerationServiceClient) SoftDeleteReply(ctx context.Context, req *connect.Request[v1.SoftDeleteReplyRequest]) (*connect.Response[v1.SoftDeleteReplyResponse], error) {
|
||||||
|
return c.softDeleteReply.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
// PluginModerationServiceHandler is an implementation of the
|
// PluginModerationServiceHandler is an implementation of the
|
||||||
// orchestrator.v1.PluginModerationService service.
|
// orchestrator.v1.PluginModerationService service.
|
||||||
type PluginModerationServiceHandler interface {
|
type PluginModerationServiceHandler interface {
|
||||||
@ -789,6 +879,11 @@ type PluginModerationServiceHandler interface {
|
|||||||
ApproveSubmission(context.Context, *connect.Request[v1.ApproveSubmissionRequest]) (*connect.Response[v1.ApproveSubmissionResponse], error)
|
ApproveSubmission(context.Context, *connect.Request[v1.ApproveSubmissionRequest]) (*connect.Response[v1.ApproveSubmissionResponse], error)
|
||||||
RejectSubmission(context.Context, *connect.Request[v1.RejectSubmissionRequest]) (*connect.Response[v1.RejectSubmissionResponse], error)
|
RejectSubmission(context.Context, *connect.Request[v1.RejectSubmissionRequest]) (*connect.Response[v1.RejectSubmissionResponse], error)
|
||||||
RequestChanges(context.Context, *connect.Request[v1.RequestChangesRequest]) (*connect.Response[v1.RequestChangesResponse], error)
|
RequestChanges(context.Context, *connect.Request[v1.RequestChangesRequest]) (*connect.Response[v1.RequestChangesResponse], error)
|
||||||
|
// Star-review moderation (ADR 0021). Unrelated to the submission queue
|
||||||
|
// above: these act on user reviews (PluginReviewService), not plugins.
|
||||||
|
ListFlaggedReviews(context.Context, *connect.Request[v1.ListFlaggedReviewsRequest]) (*connect.Response[v1.ListFlaggedReviewsResponse], error)
|
||||||
|
SoftDeleteReview(context.Context, *connect.Request[v1.SoftDeleteReviewRequest]) (*connect.Response[v1.SoftDeleteReviewResponse], error)
|
||||||
|
SoftDeleteReply(context.Context, *connect.Request[v1.SoftDeleteReplyRequest]) (*connect.Response[v1.SoftDeleteReplyResponse], error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPluginModerationServiceHandler builds an HTTP handler from the service implementation. It
|
// NewPluginModerationServiceHandler builds an HTTP handler from the service implementation. It
|
||||||
@ -822,6 +917,24 @@ func NewPluginModerationServiceHandler(svc PluginModerationServiceHandler, opts
|
|||||||
connect.WithSchema(pluginModerationServiceMethods.ByName("RequestChanges")),
|
connect.WithSchema(pluginModerationServiceMethods.ByName("RequestChanges")),
|
||||||
connect.WithHandlerOptions(opts...),
|
connect.WithHandlerOptions(opts...),
|
||||||
)
|
)
|
||||||
|
pluginModerationServiceListFlaggedReviewsHandler := connect.NewUnaryHandler(
|
||||||
|
PluginModerationServiceListFlaggedReviewsProcedure,
|
||||||
|
svc.ListFlaggedReviews,
|
||||||
|
connect.WithSchema(pluginModerationServiceMethods.ByName("ListFlaggedReviews")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
pluginModerationServiceSoftDeleteReviewHandler := connect.NewUnaryHandler(
|
||||||
|
PluginModerationServiceSoftDeleteReviewProcedure,
|
||||||
|
svc.SoftDeleteReview,
|
||||||
|
connect.WithSchema(pluginModerationServiceMethods.ByName("SoftDeleteReview")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
pluginModerationServiceSoftDeleteReplyHandler := connect.NewUnaryHandler(
|
||||||
|
PluginModerationServiceSoftDeleteReplyProcedure,
|
||||||
|
svc.SoftDeleteReply,
|
||||||
|
connect.WithSchema(pluginModerationServiceMethods.ByName("SoftDeleteReply")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
return "/orchestrator.v1.PluginModerationService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return "/orchestrator.v1.PluginModerationService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.URL.Path {
|
switch r.URL.Path {
|
||||||
case PluginModerationServiceListPendingReviewsProcedure:
|
case PluginModerationServiceListPendingReviewsProcedure:
|
||||||
@ -832,6 +945,12 @@ func NewPluginModerationServiceHandler(svc PluginModerationServiceHandler, opts
|
|||||||
pluginModerationServiceRejectSubmissionHandler.ServeHTTP(w, r)
|
pluginModerationServiceRejectSubmissionHandler.ServeHTTP(w, r)
|
||||||
case PluginModerationServiceRequestChangesProcedure:
|
case PluginModerationServiceRequestChangesProcedure:
|
||||||
pluginModerationServiceRequestChangesHandler.ServeHTTP(w, r)
|
pluginModerationServiceRequestChangesHandler.ServeHTTP(w, r)
|
||||||
|
case PluginModerationServiceListFlaggedReviewsProcedure:
|
||||||
|
pluginModerationServiceListFlaggedReviewsHandler.ServeHTTP(w, r)
|
||||||
|
case PluginModerationServiceSoftDeleteReviewProcedure:
|
||||||
|
pluginModerationServiceSoftDeleteReviewHandler.ServeHTTP(w, r)
|
||||||
|
case PluginModerationServiceSoftDeleteReplyProcedure:
|
||||||
|
pluginModerationServiceSoftDeleteReplyHandler.ServeHTTP(w, r)
|
||||||
default:
|
default:
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
}
|
}
|
||||||
@ -857,6 +976,420 @@ func (UnimplementedPluginModerationServiceHandler) RequestChanges(context.Contex
|
|||||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginModerationService.RequestChanges is not implemented"))
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginModerationService.RequestChanges is not implemented"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (UnimplementedPluginModerationServiceHandler) ListFlaggedReviews(context.Context, *connect.Request[v1.ListFlaggedReviewsRequest]) (*connect.Response[v1.ListFlaggedReviewsResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginModerationService.ListFlaggedReviews is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedPluginModerationServiceHandler) SoftDeleteReview(context.Context, *connect.Request[v1.SoftDeleteReviewRequest]) (*connect.Response[v1.SoftDeleteReviewResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginModerationService.SoftDeleteReview is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedPluginModerationServiceHandler) SoftDeleteReply(context.Context, *connect.Request[v1.SoftDeleteReplyRequest]) (*connect.Response[v1.SoftDeleteReplyResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginModerationService.SoftDeleteReply is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginGalleryServiceClient is a client for the orchestrator.v1.PluginGalleryService service.
|
||||||
|
type PluginGalleryServiceClient interface {
|
||||||
|
ListScreenshots(context.Context, *connect.Request[v1.ListScreenshotsRequest]) (*connect.Response[v1.ListScreenshotsResponse], error)
|
||||||
|
UploadScreenshot(context.Context, *connect.Request[v1.UploadScreenshotRequest]) (*connect.Response[v1.UploadScreenshotResponse], error)
|
||||||
|
DeleteScreenshot(context.Context, *connect.Request[v1.DeleteScreenshotRequest]) (*connect.Response[v1.DeleteScreenshotResponse], error)
|
||||||
|
ReorderScreenshots(context.Context, *connect.Request[v1.ReorderScreenshotsRequest]) (*connect.Response[v1.ReorderScreenshotsResponse], error)
|
||||||
|
SetFeaturedScreenshot(context.Context, *connect.Request[v1.SetFeaturedScreenshotRequest]) (*connect.Response[v1.SetFeaturedScreenshotResponse], error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPluginGalleryServiceClient constructs a client for the orchestrator.v1.PluginGalleryService
|
||||||
|
// service. By default, it uses the Connect protocol with the binary Protobuf Codec, asks for
|
||||||
|
// gzipped responses, and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply
|
||||||
|
// the connect.WithGRPC() or connect.WithGRPCWeb() options.
|
||||||
|
//
|
||||||
|
// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
|
||||||
|
// http://api.acme.com or https://acme.com/grpc).
|
||||||
|
func NewPluginGalleryServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) PluginGalleryServiceClient {
|
||||||
|
baseURL = strings.TrimRight(baseURL, "/")
|
||||||
|
pluginGalleryServiceMethods := v1.File_orchestrator_v1_plugin_registry_proto.Services().ByName("PluginGalleryService").Methods()
|
||||||
|
return &pluginGalleryServiceClient{
|
||||||
|
listScreenshots: connect.NewClient[v1.ListScreenshotsRequest, v1.ListScreenshotsResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+PluginGalleryServiceListScreenshotsProcedure,
|
||||||
|
connect.WithSchema(pluginGalleryServiceMethods.ByName("ListScreenshots")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
uploadScreenshot: connect.NewClient[v1.UploadScreenshotRequest, v1.UploadScreenshotResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+PluginGalleryServiceUploadScreenshotProcedure,
|
||||||
|
connect.WithSchema(pluginGalleryServiceMethods.ByName("UploadScreenshot")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
deleteScreenshot: connect.NewClient[v1.DeleteScreenshotRequest, v1.DeleteScreenshotResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+PluginGalleryServiceDeleteScreenshotProcedure,
|
||||||
|
connect.WithSchema(pluginGalleryServiceMethods.ByName("DeleteScreenshot")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
reorderScreenshots: connect.NewClient[v1.ReorderScreenshotsRequest, v1.ReorderScreenshotsResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+PluginGalleryServiceReorderScreenshotsProcedure,
|
||||||
|
connect.WithSchema(pluginGalleryServiceMethods.ByName("ReorderScreenshots")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
setFeaturedScreenshot: connect.NewClient[v1.SetFeaturedScreenshotRequest, v1.SetFeaturedScreenshotResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+PluginGalleryServiceSetFeaturedScreenshotProcedure,
|
||||||
|
connect.WithSchema(pluginGalleryServiceMethods.ByName("SetFeaturedScreenshot")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pluginGalleryServiceClient implements PluginGalleryServiceClient.
|
||||||
|
type pluginGalleryServiceClient struct {
|
||||||
|
listScreenshots *connect.Client[v1.ListScreenshotsRequest, v1.ListScreenshotsResponse]
|
||||||
|
uploadScreenshot *connect.Client[v1.UploadScreenshotRequest, v1.UploadScreenshotResponse]
|
||||||
|
deleteScreenshot *connect.Client[v1.DeleteScreenshotRequest, v1.DeleteScreenshotResponse]
|
||||||
|
reorderScreenshots *connect.Client[v1.ReorderScreenshotsRequest, v1.ReorderScreenshotsResponse]
|
||||||
|
setFeaturedScreenshot *connect.Client[v1.SetFeaturedScreenshotRequest, v1.SetFeaturedScreenshotResponse]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListScreenshots calls orchestrator.v1.PluginGalleryService.ListScreenshots.
|
||||||
|
func (c *pluginGalleryServiceClient) ListScreenshots(ctx context.Context, req *connect.Request[v1.ListScreenshotsRequest]) (*connect.Response[v1.ListScreenshotsResponse], error) {
|
||||||
|
return c.listScreenshots.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadScreenshot calls orchestrator.v1.PluginGalleryService.UploadScreenshot.
|
||||||
|
func (c *pluginGalleryServiceClient) UploadScreenshot(ctx context.Context, req *connect.Request[v1.UploadScreenshotRequest]) (*connect.Response[v1.UploadScreenshotResponse], error) {
|
||||||
|
return c.uploadScreenshot.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteScreenshot calls orchestrator.v1.PluginGalleryService.DeleteScreenshot.
|
||||||
|
func (c *pluginGalleryServiceClient) DeleteScreenshot(ctx context.Context, req *connect.Request[v1.DeleteScreenshotRequest]) (*connect.Response[v1.DeleteScreenshotResponse], error) {
|
||||||
|
return c.deleteScreenshot.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReorderScreenshots calls orchestrator.v1.PluginGalleryService.ReorderScreenshots.
|
||||||
|
func (c *pluginGalleryServiceClient) ReorderScreenshots(ctx context.Context, req *connect.Request[v1.ReorderScreenshotsRequest]) (*connect.Response[v1.ReorderScreenshotsResponse], error) {
|
||||||
|
return c.reorderScreenshots.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFeaturedScreenshot calls orchestrator.v1.PluginGalleryService.SetFeaturedScreenshot.
|
||||||
|
func (c *pluginGalleryServiceClient) SetFeaturedScreenshot(ctx context.Context, req *connect.Request[v1.SetFeaturedScreenshotRequest]) (*connect.Response[v1.SetFeaturedScreenshotResponse], error) {
|
||||||
|
return c.setFeaturedScreenshot.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginGalleryServiceHandler is an implementation of the orchestrator.v1.PluginGalleryService
|
||||||
|
// service.
|
||||||
|
type PluginGalleryServiceHandler interface {
|
||||||
|
ListScreenshots(context.Context, *connect.Request[v1.ListScreenshotsRequest]) (*connect.Response[v1.ListScreenshotsResponse], error)
|
||||||
|
UploadScreenshot(context.Context, *connect.Request[v1.UploadScreenshotRequest]) (*connect.Response[v1.UploadScreenshotResponse], error)
|
||||||
|
DeleteScreenshot(context.Context, *connect.Request[v1.DeleteScreenshotRequest]) (*connect.Response[v1.DeleteScreenshotResponse], error)
|
||||||
|
ReorderScreenshots(context.Context, *connect.Request[v1.ReorderScreenshotsRequest]) (*connect.Response[v1.ReorderScreenshotsResponse], error)
|
||||||
|
SetFeaturedScreenshot(context.Context, *connect.Request[v1.SetFeaturedScreenshotRequest]) (*connect.Response[v1.SetFeaturedScreenshotResponse], error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPluginGalleryServiceHandler builds an HTTP handler from the service implementation. It returns
|
||||||
|
// the path on which to mount the handler and the handler itself.
|
||||||
|
//
|
||||||
|
// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
|
||||||
|
// and JSON codecs. They also support gzip compression.
|
||||||
|
func NewPluginGalleryServiceHandler(svc PluginGalleryServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
|
||||||
|
pluginGalleryServiceMethods := v1.File_orchestrator_v1_plugin_registry_proto.Services().ByName("PluginGalleryService").Methods()
|
||||||
|
pluginGalleryServiceListScreenshotsHandler := connect.NewUnaryHandler(
|
||||||
|
PluginGalleryServiceListScreenshotsProcedure,
|
||||||
|
svc.ListScreenshots,
|
||||||
|
connect.WithSchema(pluginGalleryServiceMethods.ByName("ListScreenshots")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
pluginGalleryServiceUploadScreenshotHandler := connect.NewUnaryHandler(
|
||||||
|
PluginGalleryServiceUploadScreenshotProcedure,
|
||||||
|
svc.UploadScreenshot,
|
||||||
|
connect.WithSchema(pluginGalleryServiceMethods.ByName("UploadScreenshot")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
pluginGalleryServiceDeleteScreenshotHandler := connect.NewUnaryHandler(
|
||||||
|
PluginGalleryServiceDeleteScreenshotProcedure,
|
||||||
|
svc.DeleteScreenshot,
|
||||||
|
connect.WithSchema(pluginGalleryServiceMethods.ByName("DeleteScreenshot")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
pluginGalleryServiceReorderScreenshotsHandler := connect.NewUnaryHandler(
|
||||||
|
PluginGalleryServiceReorderScreenshotsProcedure,
|
||||||
|
svc.ReorderScreenshots,
|
||||||
|
connect.WithSchema(pluginGalleryServiceMethods.ByName("ReorderScreenshots")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
pluginGalleryServiceSetFeaturedScreenshotHandler := connect.NewUnaryHandler(
|
||||||
|
PluginGalleryServiceSetFeaturedScreenshotProcedure,
|
||||||
|
svc.SetFeaturedScreenshot,
|
||||||
|
connect.WithSchema(pluginGalleryServiceMethods.ByName("SetFeaturedScreenshot")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
return "/orchestrator.v1.PluginGalleryService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case PluginGalleryServiceListScreenshotsProcedure:
|
||||||
|
pluginGalleryServiceListScreenshotsHandler.ServeHTTP(w, r)
|
||||||
|
case PluginGalleryServiceUploadScreenshotProcedure:
|
||||||
|
pluginGalleryServiceUploadScreenshotHandler.ServeHTTP(w, r)
|
||||||
|
case PluginGalleryServiceDeleteScreenshotProcedure:
|
||||||
|
pluginGalleryServiceDeleteScreenshotHandler.ServeHTTP(w, r)
|
||||||
|
case PluginGalleryServiceReorderScreenshotsProcedure:
|
||||||
|
pluginGalleryServiceReorderScreenshotsHandler.ServeHTTP(w, r)
|
||||||
|
case PluginGalleryServiceSetFeaturedScreenshotProcedure:
|
||||||
|
pluginGalleryServiceSetFeaturedScreenshotHandler.ServeHTTP(w, r)
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedPluginGalleryServiceHandler returns CodeUnimplemented from all methods.
|
||||||
|
type UnimplementedPluginGalleryServiceHandler struct{}
|
||||||
|
|
||||||
|
func (UnimplementedPluginGalleryServiceHandler) ListScreenshots(context.Context, *connect.Request[v1.ListScreenshotsRequest]) (*connect.Response[v1.ListScreenshotsResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginGalleryService.ListScreenshots is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedPluginGalleryServiceHandler) UploadScreenshot(context.Context, *connect.Request[v1.UploadScreenshotRequest]) (*connect.Response[v1.UploadScreenshotResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginGalleryService.UploadScreenshot is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedPluginGalleryServiceHandler) DeleteScreenshot(context.Context, *connect.Request[v1.DeleteScreenshotRequest]) (*connect.Response[v1.DeleteScreenshotResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginGalleryService.DeleteScreenshot is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedPluginGalleryServiceHandler) ReorderScreenshots(context.Context, *connect.Request[v1.ReorderScreenshotsRequest]) (*connect.Response[v1.ReorderScreenshotsResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginGalleryService.ReorderScreenshots is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedPluginGalleryServiceHandler) SetFeaturedScreenshot(context.Context, *connect.Request[v1.SetFeaturedScreenshotRequest]) (*connect.Response[v1.SetFeaturedScreenshotResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginGalleryService.SetFeaturedScreenshot is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginReviewServiceClient is a client for the orchestrator.v1.PluginReviewService service.
|
||||||
|
type PluginReviewServiceClient interface {
|
||||||
|
ListReviews(context.Context, *connect.Request[v1.ListReviewsRequest]) (*connect.Response[v1.ListReviewsResponse], error)
|
||||||
|
GetMyReview(context.Context, *connect.Request[v1.GetMyReviewRequest]) (*connect.Response[v1.GetMyReviewResponse], error)
|
||||||
|
UpsertReview(context.Context, *connect.Request[v1.UpsertReviewRequest]) (*connect.Response[v1.UpsertReviewResponse], error)
|
||||||
|
DeleteReview(context.Context, *connect.Request[v1.DeleteReviewRequest]) (*connect.Response[v1.DeleteReviewResponse], error)
|
||||||
|
ReplyToReview(context.Context, *connect.Request[v1.ReplyToReviewRequest]) (*connect.Response[v1.ReplyToReviewResponse], error)
|
||||||
|
DeleteReply(context.Context, *connect.Request[v1.DeleteReplyRequest]) (*connect.Response[v1.DeleteReplyResponse], error)
|
||||||
|
FlagReview(context.Context, *connect.Request[v1.FlagReviewRequest]) (*connect.Response[v1.FlagReviewResponse], error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPluginReviewServiceClient constructs a client for the orchestrator.v1.PluginReviewService
|
||||||
|
// service. By default, it uses the Connect protocol with the binary Protobuf Codec, asks for
|
||||||
|
// gzipped responses, and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply
|
||||||
|
// the connect.WithGRPC() or connect.WithGRPCWeb() options.
|
||||||
|
//
|
||||||
|
// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
|
||||||
|
// http://api.acme.com or https://acme.com/grpc).
|
||||||
|
func NewPluginReviewServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) PluginReviewServiceClient {
|
||||||
|
baseURL = strings.TrimRight(baseURL, "/")
|
||||||
|
pluginReviewServiceMethods := v1.File_orchestrator_v1_plugin_registry_proto.Services().ByName("PluginReviewService").Methods()
|
||||||
|
return &pluginReviewServiceClient{
|
||||||
|
listReviews: connect.NewClient[v1.ListReviewsRequest, v1.ListReviewsResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+PluginReviewServiceListReviewsProcedure,
|
||||||
|
connect.WithSchema(pluginReviewServiceMethods.ByName("ListReviews")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
getMyReview: connect.NewClient[v1.GetMyReviewRequest, v1.GetMyReviewResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+PluginReviewServiceGetMyReviewProcedure,
|
||||||
|
connect.WithSchema(pluginReviewServiceMethods.ByName("GetMyReview")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
upsertReview: connect.NewClient[v1.UpsertReviewRequest, v1.UpsertReviewResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+PluginReviewServiceUpsertReviewProcedure,
|
||||||
|
connect.WithSchema(pluginReviewServiceMethods.ByName("UpsertReview")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
deleteReview: connect.NewClient[v1.DeleteReviewRequest, v1.DeleteReviewResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+PluginReviewServiceDeleteReviewProcedure,
|
||||||
|
connect.WithSchema(pluginReviewServiceMethods.ByName("DeleteReview")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
replyToReview: connect.NewClient[v1.ReplyToReviewRequest, v1.ReplyToReviewResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+PluginReviewServiceReplyToReviewProcedure,
|
||||||
|
connect.WithSchema(pluginReviewServiceMethods.ByName("ReplyToReview")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
deleteReply: connect.NewClient[v1.DeleteReplyRequest, v1.DeleteReplyResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+PluginReviewServiceDeleteReplyProcedure,
|
||||||
|
connect.WithSchema(pluginReviewServiceMethods.ByName("DeleteReply")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
flagReview: connect.NewClient[v1.FlagReviewRequest, v1.FlagReviewResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+PluginReviewServiceFlagReviewProcedure,
|
||||||
|
connect.WithSchema(pluginReviewServiceMethods.ByName("FlagReview")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pluginReviewServiceClient implements PluginReviewServiceClient.
|
||||||
|
type pluginReviewServiceClient struct {
|
||||||
|
listReviews *connect.Client[v1.ListReviewsRequest, v1.ListReviewsResponse]
|
||||||
|
getMyReview *connect.Client[v1.GetMyReviewRequest, v1.GetMyReviewResponse]
|
||||||
|
upsertReview *connect.Client[v1.UpsertReviewRequest, v1.UpsertReviewResponse]
|
||||||
|
deleteReview *connect.Client[v1.DeleteReviewRequest, v1.DeleteReviewResponse]
|
||||||
|
replyToReview *connect.Client[v1.ReplyToReviewRequest, v1.ReplyToReviewResponse]
|
||||||
|
deleteReply *connect.Client[v1.DeleteReplyRequest, v1.DeleteReplyResponse]
|
||||||
|
flagReview *connect.Client[v1.FlagReviewRequest, v1.FlagReviewResponse]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListReviews calls orchestrator.v1.PluginReviewService.ListReviews.
|
||||||
|
func (c *pluginReviewServiceClient) ListReviews(ctx context.Context, req *connect.Request[v1.ListReviewsRequest]) (*connect.Response[v1.ListReviewsResponse], error) {
|
||||||
|
return c.listReviews.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMyReview calls orchestrator.v1.PluginReviewService.GetMyReview.
|
||||||
|
func (c *pluginReviewServiceClient) GetMyReview(ctx context.Context, req *connect.Request[v1.GetMyReviewRequest]) (*connect.Response[v1.GetMyReviewResponse], error) {
|
||||||
|
return c.getMyReview.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpsertReview calls orchestrator.v1.PluginReviewService.UpsertReview.
|
||||||
|
func (c *pluginReviewServiceClient) UpsertReview(ctx context.Context, req *connect.Request[v1.UpsertReviewRequest]) (*connect.Response[v1.UpsertReviewResponse], error) {
|
||||||
|
return c.upsertReview.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteReview calls orchestrator.v1.PluginReviewService.DeleteReview.
|
||||||
|
func (c *pluginReviewServiceClient) DeleteReview(ctx context.Context, req *connect.Request[v1.DeleteReviewRequest]) (*connect.Response[v1.DeleteReviewResponse], error) {
|
||||||
|
return c.deleteReview.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplyToReview calls orchestrator.v1.PluginReviewService.ReplyToReview.
|
||||||
|
func (c *pluginReviewServiceClient) ReplyToReview(ctx context.Context, req *connect.Request[v1.ReplyToReviewRequest]) (*connect.Response[v1.ReplyToReviewResponse], error) {
|
||||||
|
return c.replyToReview.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteReply calls orchestrator.v1.PluginReviewService.DeleteReply.
|
||||||
|
func (c *pluginReviewServiceClient) DeleteReply(ctx context.Context, req *connect.Request[v1.DeleteReplyRequest]) (*connect.Response[v1.DeleteReplyResponse], error) {
|
||||||
|
return c.deleteReply.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlagReview calls orchestrator.v1.PluginReviewService.FlagReview.
|
||||||
|
func (c *pluginReviewServiceClient) FlagReview(ctx context.Context, req *connect.Request[v1.FlagReviewRequest]) (*connect.Response[v1.FlagReviewResponse], error) {
|
||||||
|
return c.flagReview.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginReviewServiceHandler is an implementation of the orchestrator.v1.PluginReviewService
|
||||||
|
// service.
|
||||||
|
type PluginReviewServiceHandler interface {
|
||||||
|
ListReviews(context.Context, *connect.Request[v1.ListReviewsRequest]) (*connect.Response[v1.ListReviewsResponse], error)
|
||||||
|
GetMyReview(context.Context, *connect.Request[v1.GetMyReviewRequest]) (*connect.Response[v1.GetMyReviewResponse], error)
|
||||||
|
UpsertReview(context.Context, *connect.Request[v1.UpsertReviewRequest]) (*connect.Response[v1.UpsertReviewResponse], error)
|
||||||
|
DeleteReview(context.Context, *connect.Request[v1.DeleteReviewRequest]) (*connect.Response[v1.DeleteReviewResponse], error)
|
||||||
|
ReplyToReview(context.Context, *connect.Request[v1.ReplyToReviewRequest]) (*connect.Response[v1.ReplyToReviewResponse], error)
|
||||||
|
DeleteReply(context.Context, *connect.Request[v1.DeleteReplyRequest]) (*connect.Response[v1.DeleteReplyResponse], error)
|
||||||
|
FlagReview(context.Context, *connect.Request[v1.FlagReviewRequest]) (*connect.Response[v1.FlagReviewResponse], error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPluginReviewServiceHandler builds an HTTP handler from the service implementation. It returns
|
||||||
|
// the path on which to mount the handler and the handler itself.
|
||||||
|
//
|
||||||
|
// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
|
||||||
|
// and JSON codecs. They also support gzip compression.
|
||||||
|
func NewPluginReviewServiceHandler(svc PluginReviewServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
|
||||||
|
pluginReviewServiceMethods := v1.File_orchestrator_v1_plugin_registry_proto.Services().ByName("PluginReviewService").Methods()
|
||||||
|
pluginReviewServiceListReviewsHandler := connect.NewUnaryHandler(
|
||||||
|
PluginReviewServiceListReviewsProcedure,
|
||||||
|
svc.ListReviews,
|
||||||
|
connect.WithSchema(pluginReviewServiceMethods.ByName("ListReviews")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
pluginReviewServiceGetMyReviewHandler := connect.NewUnaryHandler(
|
||||||
|
PluginReviewServiceGetMyReviewProcedure,
|
||||||
|
svc.GetMyReview,
|
||||||
|
connect.WithSchema(pluginReviewServiceMethods.ByName("GetMyReview")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
pluginReviewServiceUpsertReviewHandler := connect.NewUnaryHandler(
|
||||||
|
PluginReviewServiceUpsertReviewProcedure,
|
||||||
|
svc.UpsertReview,
|
||||||
|
connect.WithSchema(pluginReviewServiceMethods.ByName("UpsertReview")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
pluginReviewServiceDeleteReviewHandler := connect.NewUnaryHandler(
|
||||||
|
PluginReviewServiceDeleteReviewProcedure,
|
||||||
|
svc.DeleteReview,
|
||||||
|
connect.WithSchema(pluginReviewServiceMethods.ByName("DeleteReview")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
pluginReviewServiceReplyToReviewHandler := connect.NewUnaryHandler(
|
||||||
|
PluginReviewServiceReplyToReviewProcedure,
|
||||||
|
svc.ReplyToReview,
|
||||||
|
connect.WithSchema(pluginReviewServiceMethods.ByName("ReplyToReview")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
pluginReviewServiceDeleteReplyHandler := connect.NewUnaryHandler(
|
||||||
|
PluginReviewServiceDeleteReplyProcedure,
|
||||||
|
svc.DeleteReply,
|
||||||
|
connect.WithSchema(pluginReviewServiceMethods.ByName("DeleteReply")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
pluginReviewServiceFlagReviewHandler := connect.NewUnaryHandler(
|
||||||
|
PluginReviewServiceFlagReviewProcedure,
|
||||||
|
svc.FlagReview,
|
||||||
|
connect.WithSchema(pluginReviewServiceMethods.ByName("FlagReview")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
return "/orchestrator.v1.PluginReviewService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case PluginReviewServiceListReviewsProcedure:
|
||||||
|
pluginReviewServiceListReviewsHandler.ServeHTTP(w, r)
|
||||||
|
case PluginReviewServiceGetMyReviewProcedure:
|
||||||
|
pluginReviewServiceGetMyReviewHandler.ServeHTTP(w, r)
|
||||||
|
case PluginReviewServiceUpsertReviewProcedure:
|
||||||
|
pluginReviewServiceUpsertReviewHandler.ServeHTTP(w, r)
|
||||||
|
case PluginReviewServiceDeleteReviewProcedure:
|
||||||
|
pluginReviewServiceDeleteReviewHandler.ServeHTTP(w, r)
|
||||||
|
case PluginReviewServiceReplyToReviewProcedure:
|
||||||
|
pluginReviewServiceReplyToReviewHandler.ServeHTTP(w, r)
|
||||||
|
case PluginReviewServiceDeleteReplyProcedure:
|
||||||
|
pluginReviewServiceDeleteReplyHandler.ServeHTTP(w, r)
|
||||||
|
case PluginReviewServiceFlagReviewProcedure:
|
||||||
|
pluginReviewServiceFlagReviewHandler.ServeHTTP(w, r)
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedPluginReviewServiceHandler returns CodeUnimplemented from all methods.
|
||||||
|
type UnimplementedPluginReviewServiceHandler struct{}
|
||||||
|
|
||||||
|
func (UnimplementedPluginReviewServiceHandler) ListReviews(context.Context, *connect.Request[v1.ListReviewsRequest]) (*connect.Response[v1.ListReviewsResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginReviewService.ListReviews is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedPluginReviewServiceHandler) GetMyReview(context.Context, *connect.Request[v1.GetMyReviewRequest]) (*connect.Response[v1.GetMyReviewResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginReviewService.GetMyReview is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedPluginReviewServiceHandler) UpsertReview(context.Context, *connect.Request[v1.UpsertReviewRequest]) (*connect.Response[v1.UpsertReviewResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginReviewService.UpsertReview is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedPluginReviewServiceHandler) DeleteReview(context.Context, *connect.Request[v1.DeleteReviewRequest]) (*connect.Response[v1.DeleteReviewResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginReviewService.DeleteReview is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedPluginReviewServiceHandler) ReplyToReview(context.Context, *connect.Request[v1.ReplyToReviewRequest]) (*connect.Response[v1.ReplyToReviewResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginReviewService.ReplyToReview is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedPluginReviewServiceHandler) DeleteReply(context.Context, *connect.Request[v1.DeleteReplyRequest]) (*connect.Response[v1.DeleteReplyResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginReviewService.DeleteReply is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedPluginReviewServiceHandler) FlagReview(context.Context, *connect.Request[v1.FlagReviewRequest]) (*connect.Response[v1.FlagReviewResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginReviewService.FlagReview is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
// PluginPublishServiceClient is a client for the orchestrator.v1.PluginPublishService service.
|
// PluginPublishServiceClient is a client for the orchestrator.v1.PluginPublishService service.
|
||||||
type PluginPublishServiceClient interface {
|
type PluginPublishServiceClient interface {
|
||||||
PublishVersion(context.Context, *connect.Request[v1.PublishVersionRequest]) (*connect.Response[v1.PublishVersionResponse], error)
|
PublishVersion(context.Context, *connect.Request[v1.PublishVersionRequest]) (*connect.Response[v1.PublishVersionResponse], error)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -41,9 +41,9 @@ type BuildOptions struct {
|
|||||||
|
|
||||||
// BuildResult summarizes a produced artifact for the CLI summary table.
|
// BuildResult summarizes a produced artifact for the CLI summary table.
|
||||||
type BuildResult struct {
|
type BuildResult struct {
|
||||||
OutputPath string
|
OutputPath string
|
||||||
Name string
|
Name string
|
||||||
Version string
|
Version string
|
||||||
// Codeless marks a declarative artifact (no plugin.wasm, WO-WZ-020).
|
// Codeless marks a declarative artifact (no plugin.wasm, WO-WZ-020).
|
||||||
Codeless bool
|
Codeless bool
|
||||||
WasmBytes int64
|
WasmBytes int64
|
||||||
@ -57,6 +57,11 @@ type BuildResult struct {
|
|||||||
Hooks []string
|
Hooks []string
|
||||||
DataDir bool
|
DataDir bool
|
||||||
IncludedDirs []string
|
IncludedDirs []string
|
||||||
|
// PreviewIncluded/ScreenshotCount report the packed presentation members
|
||||||
|
// (root preview.png + screenshots/); the registry ingests these into the
|
||||||
|
// plugin's gallery at publish.
|
||||||
|
PreviewIncluded bool
|
||||||
|
ScreenshotCount int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build compiles the plugin in opts.Dir to wasm, extracts its manifest, and
|
// Build compiles the plugin in opts.Dir to wasm, extracts its manifest, and
|
||||||
@ -182,6 +187,14 @@ func Build(ctx context.Context, opts BuildOptions) (*BuildResult, error) {
|
|||||||
includedDirs = append(includedDirs, o.artifact)
|
includedDirs = append(includedDirs, o.artifact)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
presEntries, previewIncluded, screenshotCount, err := presentationEntries(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
entries = append(entries, presEntries...)
|
||||||
|
if screenshotCount > 0 {
|
||||||
|
includedDirs = append(includedDirs, dirScreenshots)
|
||||||
|
}
|
||||||
|
|
||||||
// 6. Pack.
|
// 6. Pack.
|
||||||
outPath := opts.Output
|
outPath := opts.Output
|
||||||
@ -200,20 +213,22 @@ func Build(ctx context.Context, opts BuildOptions) (*BuildResult, error) {
|
|||||||
wasmInfo, _ := os.Stat(wasmPath)
|
wasmInfo, _ := os.Stat(wasmPath)
|
||||||
|
|
||||||
return &BuildResult{
|
return &BuildResult{
|
||||||
OutputPath: outPath,
|
OutputPath: outPath,
|
||||||
Name: manifest.GetName(),
|
Name: manifest.GetName(),
|
||||||
Version: manifest.GetVersion(),
|
Version: manifest.GetVersion(),
|
||||||
WasmBytes: wasmInfo.Size(),
|
WasmBytes: wasmInfo.Size(),
|
||||||
ManifestBytes: int64(len(manifestBytes)),
|
ManifestBytes: int64(len(manifestBytes)),
|
||||||
ArtifactBytes: artInfo.Size(),
|
ArtifactBytes: artInfo.Size(),
|
||||||
UncompBytes: uncomp,
|
UncompBytes: uncomp,
|
||||||
BlockCount: len(manifest.GetBlocks()),
|
BlockCount: len(manifest.GetBlocks()),
|
||||||
TemplateCount: len(manifest.GetTemplateKeys()),
|
TemplateCount: len(manifest.GetTemplateKeys()),
|
||||||
AdminPages: len(manifest.GetAdminPages()),
|
AdminPages: len(manifest.GetAdminPages()),
|
||||||
JobTypes: len(manifest.GetJobTypes()),
|
JobTypes: len(manifest.GetJobTypes()),
|
||||||
Hooks: hooksPresent(manifest),
|
Hooks: hooksPresent(manifest),
|
||||||
DataDir: manifest.GetDataDir(),
|
DataDir: manifest.GetDataDir(),
|
||||||
IncludedDirs: includedDirs,
|
IncludedDirs: includedDirs,
|
||||||
|
PreviewIncluded: previewIncluded,
|
||||||
|
ScreenshotCount: screenshotCount,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -34,10 +34,10 @@ const manifestYAMLName = "manifest.yaml"
|
|||||||
// manifestYAML mirrors the declarative PluginManifest fields a codeless
|
// manifestYAML mirrors the declarative PluginManifest fields a codeless
|
||||||
// plugin can set. File-valued keys are paths relative to the repo root.
|
// plugin can set. File-valued keys are paths relative to the repo root.
|
||||||
type manifestYAML struct {
|
type manifestYAML struct {
|
||||||
ThemePresets string `yaml:"theme_presets"` // JSON file
|
ThemePresets string `yaml:"theme_presets"` // JSON file
|
||||||
BundledFonts string `yaml:"bundled_fonts"` // JSON file
|
BundledFonts string `yaml:"bundled_fonts"` // JSON file
|
||||||
SettingsSchema string `yaml:"settings_schema"` // JSON file
|
SettingsSchema string `yaml:"settings_schema"` // JSON file
|
||||||
MasterPages string `yaml:"master_pages"` // JSON file ([]masterPageJSON)
|
MasterPages string `yaml:"master_pages"` // JSON file ([]masterPageJSON)
|
||||||
RequiredIconPacks []string `yaml:"required_icon_packs"`
|
RequiredIconPacks []string `yaml:"required_icon_packs"`
|
||||||
SystemTemplates []struct {
|
SystemTemplates []struct {
|
||||||
Key string `yaml:"key"`
|
Key string `yaml:"key"`
|
||||||
@ -240,6 +240,14 @@ func BuildCodeless(_ context.Context, opts BuildOptions) (*BuildResult, error) {
|
|||||||
includedDirs = append(includedDirs, o.artifact)
|
includedDirs = append(includedDirs, o.artifact)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
presEntries, previewIncluded, screenshotCount, err := presentationEntries(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
entries = append(entries, presEntries...)
|
||||||
|
if screenshotCount > 0 {
|
||||||
|
includedDirs = append(includedDirs, dirScreenshots)
|
||||||
|
}
|
||||||
|
|
||||||
outPath := opts.Output
|
outPath := opts.Output
|
||||||
if outPath == "" {
|
if outPath == "" {
|
||||||
@ -255,15 +263,17 @@ func BuildCodeless(_ context.Context, opts BuildOptions) (*BuildResult, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &BuildResult{
|
return &BuildResult{
|
||||||
OutputPath: outPath,
|
OutputPath: outPath,
|
||||||
Name: manifest.GetName(),
|
Name: manifest.GetName(),
|
||||||
Version: manifest.GetVersion(),
|
Version: manifest.GetVersion(),
|
||||||
Codeless: true,
|
Codeless: true,
|
||||||
ManifestBytes: int64(len(manifestBytes)),
|
ManifestBytes: int64(len(manifestBytes)),
|
||||||
ArtifactBytes: artInfo.Size(),
|
ArtifactBytes: artInfo.Size(),
|
||||||
UncompBytes: uncomp,
|
UncompBytes: uncomp,
|
||||||
BlockCount: blockCount,
|
BlockCount: blockCount,
|
||||||
IncludedDirs: includedDirs,
|
IncludedDirs: includedDirs,
|
||||||
|
PreviewIncluded: previewIncluded,
|
||||||
|
ScreenshotCount: screenshotCount,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
121
internal/bnp/screenshots.go
Normal file
121
internal/bnp/screenshots.go
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
package bnp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Presentation members: a root preview.png plus an optional screenshots/ dir.
|
||||||
|
// Neither ships behavior — the registry ingests them at publish into the
|
||||||
|
// plugin's managed gallery (spec 2026-07-05-registry-presentation-reviews).
|
||||||
|
// The .so-era source archives carried preview.png implicitly; the .bnp
|
||||||
|
// packers dropped it, which silently wiped previews on republish — these
|
||||||
|
// entries restore that and add the multi-screenshot surface.
|
||||||
|
const (
|
||||||
|
filePreview = "preview.png"
|
||||||
|
dirScreenshots = "screenshots"
|
||||||
|
// maxScreenshotBytes caps one presentation image (registry gallery cap).
|
||||||
|
maxScreenshotBytes = 8 << 20
|
||||||
|
// maxScreenshotFiles caps the screenshots/ dir (registry gallery cap).
|
||||||
|
maxScreenshotFiles = 12
|
||||||
|
)
|
||||||
|
|
||||||
|
// screenshotExtOK reports whether a filename carries an accepted gallery
|
||||||
|
// image extension.
|
||||||
|
func screenshotExtOK(name string) bool {
|
||||||
|
switch strings.ToLower(filepath.Ext(name)) {
|
||||||
|
case ".png", ".jpg", ".jpeg", ".webp":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateScreenshotsDir checks the optional screenshots/ dir: accepted
|
||||||
|
// extensions only, ≤ maxScreenshotBytes per file, ≤ maxScreenshotFiles total,
|
||||||
|
// no subdirectories or irregular files. Returns the file count (0 when the
|
||||||
|
// dir is absent).
|
||||||
|
func validateScreenshotsDir(dir string) (int, error) {
|
||||||
|
root := filepath.Join(dir, dirScreenshots)
|
||||||
|
info, err := os.Stat(root)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("stat %s/: %w", dirScreenshots, err)
|
||||||
|
}
|
||||||
|
if !info.IsDir() {
|
||||||
|
return 0, fmt.Errorf("%s exists but is not a directory", dirScreenshots)
|
||||||
|
}
|
||||||
|
entries, err := os.ReadDir(root)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("read %s/: %w", dirScreenshots, err)
|
||||||
|
}
|
||||||
|
count := 0
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.IsDir() {
|
||||||
|
return 0, fmt.Errorf("%s/%s: subdirectories are not allowed in screenshots/", dirScreenshots, e.Name())
|
||||||
|
}
|
||||||
|
fi, err := e.Info()
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("%s/%s: %w", dirScreenshots, e.Name(), err)
|
||||||
|
}
|
||||||
|
if !fi.Mode().IsRegular() {
|
||||||
|
return 0, fmt.Errorf("%s/%s: not a regular file", dirScreenshots, e.Name())
|
||||||
|
}
|
||||||
|
if !screenshotExtOK(e.Name()) {
|
||||||
|
return 0, fmt.Errorf("%s/%s: unsupported image type (allowed: .png .jpg .jpeg .webp)", dirScreenshots, e.Name())
|
||||||
|
}
|
||||||
|
if fi.Size() > maxScreenshotBytes {
|
||||||
|
return 0, fmt.Errorf("%s/%s: %d bytes exceeds the %d MB per-image cap", dirScreenshots, e.Name(), fi.Size(), maxScreenshotBytes>>20)
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
if count > maxScreenshotFiles {
|
||||||
|
return 0, fmt.Errorf("%s/ holds %d images; the gallery cap is %d", dirScreenshots, count, maxScreenshotFiles)
|
||||||
|
}
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// presentationEntries validates and returns pack entries for the optional
|
||||||
|
// root preview.png and screenshots/ dir. Both are optional; absence is not an
|
||||||
|
// error.
|
||||||
|
func presentationEntries(dir string) (entries []packEntry, previewIncluded bool, screenshotCount int, err error) {
|
||||||
|
previewPath := filepath.Join(dir, filePreview)
|
||||||
|
if fi, statErr := os.Stat(previewPath); statErr == nil {
|
||||||
|
if !fi.Mode().IsRegular() {
|
||||||
|
return nil, false, 0, fmt.Errorf("%s is not a regular file", filePreview)
|
||||||
|
}
|
||||||
|
if fi.Size() > maxScreenshotBytes {
|
||||||
|
return nil, false, 0, fmt.Errorf("%s: %d bytes exceeds the %d MB per-image cap", filePreview, fi.Size(), maxScreenshotBytes>>20)
|
||||||
|
}
|
||||||
|
entries = append(entries, packEntry{ArtifactPath: filePreview, Source: previewPath})
|
||||||
|
previewIncluded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
screenshotCount, err = validateScreenshotsDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, 0, err
|
||||||
|
}
|
||||||
|
if screenshotCount > 0 {
|
||||||
|
root := filepath.Join(dir, dirScreenshots)
|
||||||
|
files, readErr := os.ReadDir(root)
|
||||||
|
if readErr != nil {
|
||||||
|
return nil, false, 0, fmt.Errorf("read %s/: %w", dirScreenshots, readErr)
|
||||||
|
}
|
||||||
|
names := make([]string, 0, len(files))
|
||||||
|
for _, f := range files {
|
||||||
|
names = append(names, f.Name())
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
for _, name := range names {
|
||||||
|
entries = append(entries, packEntry{
|
||||||
|
ArtifactPath: dirScreenshots + "/" + name,
|
||||||
|
Source: filepath.Join(root, name),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entries, previewIncluded, screenshotCount, nil
|
||||||
|
}
|
||||||
152
internal/bnp/screenshots_test.go
Normal file
152
internal/bnp/screenshots_test.go
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
package bnp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// presentationFixture extends the codeless fixture with a root preview.png
|
||||||
|
// and a screenshots/ dir.
|
||||||
|
func presentationFixture(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
dir := codelessFixture(t)
|
||||||
|
writeFixture(t, dir, map[string]string{
|
||||||
|
"preview.png": "not-really-png-but-bytes",
|
||||||
|
"screenshots/01-home.png": "home-bytes",
|
||||||
|
"screenshots/02-blog.jpg": "blog-bytes",
|
||||||
|
"screenshots/03-page.webp": "page-bytes",
|
||||||
|
})
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCodelessBuildPacksPresentationMembers(t *testing.T) {
|
||||||
|
dir := presentationFixture(t)
|
||||||
|
|
||||||
|
out := filepath.Join(t.TempDir(), "fixture-theme-0.1.0.bnp")
|
||||||
|
res, err := BuildCodeless(context.Background(), BuildOptions{Dir: dir, Output: out})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BuildCodeless: %v", err)
|
||||||
|
}
|
||||||
|
if !res.PreviewIncluded || res.ScreenshotCount != 3 {
|
||||||
|
t.Errorf("result preview=%t screenshots=%d; want true/3", res.PreviewIncluded, res.ScreenshotCount)
|
||||||
|
}
|
||||||
|
found := false
|
||||||
|
for _, d := range res.IncludedDirs {
|
||||||
|
if d == dirScreenshots {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("IncludedDirs %v missing %q", res.IncludedDirs, dirScreenshots)
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := Verify(out)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Verify: %v", err)
|
||||||
|
}
|
||||||
|
if !v.HasPreview || v.ScreenshotCount != 3 {
|
||||||
|
t.Errorf("verify preview=%t screenshots=%d; want true/3", v.HasPreview, v.ScreenshotCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCodelessBuildWithoutPresentationMembers(t *testing.T) {
|
||||||
|
dir := codelessFixture(t)
|
||||||
|
out := filepath.Join(t.TempDir(), "fixture-theme-0.1.0.bnp")
|
||||||
|
res, err := BuildCodeless(context.Background(), BuildOptions{Dir: dir, Output: out})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BuildCodeless: %v", err)
|
||||||
|
}
|
||||||
|
if res.PreviewIncluded || res.ScreenshotCount != 0 {
|
||||||
|
t.Errorf("result preview=%t screenshots=%d; want false/0", res.PreviewIncluded, res.ScreenshotCount)
|
||||||
|
}
|
||||||
|
v, err := Verify(out)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Verify: %v", err)
|
||||||
|
}
|
||||||
|
if v.HasPreview || v.ScreenshotCount != 0 {
|
||||||
|
t.Errorf("verify preview=%t screenshots=%d; want false/0", v.HasPreview, v.ScreenshotCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateScreenshotsDirRejections(t *testing.T) {
|
||||||
|
build := func(t *testing.T, mutate func(dir string)) error {
|
||||||
|
t.Helper()
|
||||||
|
dir := codelessFixture(t)
|
||||||
|
mutate(dir)
|
||||||
|
out := filepath.Join(t.TempDir(), "out.bnp")
|
||||||
|
_, err := BuildCodeless(context.Background(), BuildOptions{Dir: dir, Output: out})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("bad extension", func(t *testing.T) {
|
||||||
|
err := build(t, func(dir string) {
|
||||||
|
writeFixture(t, dir, map[string]string{"screenshots/readme.txt": "nope"})
|
||||||
|
})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "unsupported image type") {
|
||||||
|
t.Fatalf("err = %v; want unsupported image type", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("subdirectory", func(t *testing.T) {
|
||||||
|
err := build(t, func(dir string) {
|
||||||
|
writeFixture(t, dir, map[string]string{"screenshots/sub/x.png": "nope"})
|
||||||
|
})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "subdirectories are not allowed") {
|
||||||
|
t.Fatalf("err = %v; want subdirectory rejection", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("too many", func(t *testing.T) {
|
||||||
|
err := build(t, func(dir string) {
|
||||||
|
files := map[string]string{}
|
||||||
|
for i := range maxScreenshotFiles + 1 {
|
||||||
|
files[fmt.Sprintf("screenshots/%02d.png", i)] = "x"
|
||||||
|
}
|
||||||
|
writeFixture(t, dir, files)
|
||||||
|
})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "gallery cap") {
|
||||||
|
t.Fatalf("err = %v; want gallery cap rejection", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("oversized image", func(t *testing.T) {
|
||||||
|
err := build(t, func(dir string) {
|
||||||
|
big := filepath.Join(dir, dirScreenshots, "big.png")
|
||||||
|
if mkErr := os.MkdirAll(filepath.Dir(big), 0o755); mkErr != nil {
|
||||||
|
t.Fatal(mkErr)
|
||||||
|
}
|
||||||
|
f, cErr := os.Create(big)
|
||||||
|
if cErr != nil {
|
||||||
|
t.Fatal(cErr)
|
||||||
|
}
|
||||||
|
if tErr := f.Truncate(maxScreenshotBytes + 1); tErr != nil {
|
||||||
|
t.Fatal(tErr)
|
||||||
|
}
|
||||||
|
_ = f.Close()
|
||||||
|
})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "per-image cap") {
|
||||||
|
t.Fatalf("err = %v; want per-image cap rejection", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("oversized preview", func(t *testing.T) {
|
||||||
|
err := build(t, func(dir string) {
|
||||||
|
big := filepath.Join(dir, filePreview)
|
||||||
|
f, cErr := os.Create(big)
|
||||||
|
if cErr != nil {
|
||||||
|
t.Fatal(cErr)
|
||||||
|
}
|
||||||
|
if tErr := f.Truncate(maxScreenshotBytes + 1); tErr != nil {
|
||||||
|
t.Fatal(tErr)
|
||||||
|
}
|
||||||
|
_ = f.Close()
|
||||||
|
})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "per-image cap") {
|
||||||
|
t.Fatalf("err = %v; want per-image cap rejection", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -47,6 +47,10 @@ type VerifyResult struct {
|
|||||||
HasBlocks bool
|
HasBlocks bool
|
||||||
HasTemplates bool
|
HasTemplates bool
|
||||||
HasSeed bool
|
HasSeed bool
|
||||||
|
// HasPreview/ScreenshotCount report the presentation members the registry
|
||||||
|
// ingests into the plugin gallery at publish.
|
||||||
|
HasPreview bool
|
||||||
|
ScreenshotCount int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify extracts bnpPath into a temp dir and applies the reader's checks:
|
// Verify extracts bnpPath into a temp dir and applies the reader's checks:
|
||||||
@ -119,22 +123,40 @@ func Verify(bnpPath string) (*VerifyResult, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &VerifyResult{
|
return &VerifyResult{
|
||||||
Name: name,
|
Name: name,
|
||||||
Version: manifest.GetVersion(),
|
Version: manifest.GetVersion(),
|
||||||
ABIVersion: manifest.GetAbiVersion(),
|
ABIVersion: manifest.GetAbiVersion(),
|
||||||
Codeless: manifest.GetCodeless(),
|
Codeless: manifest.GetCodeless(),
|
||||||
DataDir: manifest.GetDataDir(),
|
DataDir: manifest.GetDataDir(),
|
||||||
BlockCount: len(manifest.GetBlocks()),
|
BlockCount: len(manifest.GetBlocks()),
|
||||||
HasMigrations: dirExists(filepath.Join(destDir, "migrations")),
|
HasMigrations: dirExists(filepath.Join(destDir, "migrations")),
|
||||||
HasSchemas: dirExists(filepath.Join(destDir, "schemas")),
|
HasSchemas: dirExists(filepath.Join(destDir, "schemas")),
|
||||||
HasAssets: dirExists(filepath.Join(destDir, "assets")),
|
HasAssets: dirExists(filepath.Join(destDir, "assets")),
|
||||||
HasWeb: dirExists(filepath.Join(destDir, "web")),
|
HasWeb: dirExists(filepath.Join(destDir, "web")),
|
||||||
HasBlocks: dirExists(filepath.Join(destDir, dirBlocks)),
|
HasBlocks: dirExists(filepath.Join(destDir, dirBlocks)),
|
||||||
HasTemplates: dirExists(filepath.Join(destDir, dirTemplates)),
|
HasTemplates: dirExists(filepath.Join(destDir, dirTemplates)),
|
||||||
HasSeed: dirExists(filepath.Join(destDir, dirSeed)),
|
HasSeed: dirExists(filepath.Join(destDir, dirSeed)),
|
||||||
|
HasPreview: fileRegular(filepath.Join(destDir, filePreview)),
|
||||||
|
ScreenshotCount: countScreenshots(filepath.Join(destDir, dirScreenshots)),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// countScreenshots counts accepted image files directly under an extracted
|
||||||
|
// screenshots/ dir (0 when absent).
|
||||||
|
func countScreenshots(root string) int {
|
||||||
|
entries, err := os.ReadDir(root)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
n := 0
|
||||||
|
for _, e := range entries {
|
||||||
|
if !e.IsDir() && screenshotExtOK(e.Name()) {
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
// CodelessHookViolation returns a named error when a codeless manifest
|
// CodelessHookViolation returns a named error when a codeless manifest
|
||||||
// declares any capability that requires guest code. Mirrored by the CMS
|
// declares any capability that requires guest code. Mirrored by the CMS
|
||||||
// reader (lockstep duplication, same as the rest of this file).
|
// reader (lockstep duplication, same as the rest of this file).
|
||||||
|
|||||||
@ -11,10 +11,11 @@ import (
|
|||||||
type Client struct {
|
type Client struct {
|
||||||
Host string
|
Host string
|
||||||
Token string
|
Token string
|
||||||
Auth orchestratorv1connect.PluginAuthServiceClient
|
Auth orchestratorv1connect.PluginAuthServiceClient
|
||||||
Scope orchestratorv1connect.PluginScopeServiceClient
|
Scope orchestratorv1connect.PluginScopeServiceClient
|
||||||
Reg orchestratorv1connect.PluginRegistryServiceClient
|
Reg orchestratorv1connect.PluginRegistryServiceClient
|
||||||
Pub orchestratorv1connect.PluginPublishServiceClient
|
Pub orchestratorv1connect.PluginPublishServiceClient
|
||||||
|
Gallery orchestratorv1connect.PluginGalleryServiceClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(host, token string) *Client {
|
func New(host, token string) *Client {
|
||||||
@ -27,6 +28,7 @@ func New(host, token string) *Client {
|
|||||||
c.Scope = orchestratorv1connect.NewPluginScopeServiceClient(httpClient, host, opts...)
|
c.Scope = orchestratorv1connect.NewPluginScopeServiceClient(httpClient, host, opts...)
|
||||||
c.Reg = orchestratorv1connect.NewPluginRegistryServiceClient(httpClient, host, opts...)
|
c.Reg = orchestratorv1connect.NewPluginRegistryServiceClient(httpClient, host, opts...)
|
||||||
c.Pub = orchestratorv1connect.NewPluginPublishServiceClient(httpClient, host, opts...)
|
c.Pub = orchestratorv1connect.NewPluginPublishServiceClient(httpClient, host, opts...)
|
||||||
|
c.Gallery = orchestratorv1connect.NewPluginGalleryServiceClient(httpClient, host, opts...)
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -49,6 +49,36 @@ service PluginModerationService {
|
|||||||
rpc ApproveSubmission(ApproveSubmissionRequest) returns (ApproveSubmissionResponse);
|
rpc ApproveSubmission(ApproveSubmissionRequest) returns (ApproveSubmissionResponse);
|
||||||
rpc RejectSubmission(RejectSubmissionRequest) returns (RejectSubmissionResponse);
|
rpc RejectSubmission(RejectSubmissionRequest) returns (RejectSubmissionResponse);
|
||||||
rpc RequestChanges(RequestChangesRequest) returns (RequestChangesResponse);
|
rpc RequestChanges(RequestChangesRequest) returns (RequestChangesResponse);
|
||||||
|
|
||||||
|
// Star-review moderation (ADR 0021). Unrelated to the submission queue
|
||||||
|
// above: these act on user reviews (PluginReviewService), not plugins.
|
||||||
|
rpc ListFlaggedReviews(ListFlaggedReviewsRequest) returns (ListFlaggedReviewsResponse);
|
||||||
|
rpc SoftDeleteReview(SoftDeleteReviewRequest) returns (SoftDeleteReviewResponse);
|
||||||
|
rpc SoftDeleteReply(SoftDeleteReplyRequest) returns (SoftDeleteReplyResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginGalleryService manages a plugin's screenshot gallery. Reads are
|
||||||
|
// public; writes require scope (or owning-account) membership, enforced
|
||||||
|
// in-handler.
|
||||||
|
service PluginGalleryService {
|
||||||
|
rpc ListScreenshots(ListScreenshotsRequest) returns (ListScreenshotsResponse);
|
||||||
|
rpc UploadScreenshot(UploadScreenshotRequest) returns (UploadScreenshotResponse);
|
||||||
|
rpc DeleteScreenshot(DeleteScreenshotRequest) returns (DeleteScreenshotResponse);
|
||||||
|
rpc ReorderScreenshots(ReorderScreenshotsRequest) returns (ReorderScreenshotsResponse);
|
||||||
|
rpc SetFeaturedScreenshot(SetFeaturedScreenshotRequest) returns (SetFeaturedScreenshotResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginReviewService is the star-review surface for PUBLIC plugins
|
||||||
|
// (ADR 0021): verified-install gating, one review per user, author replies,
|
||||||
|
// user flags. Reads are public; writes require a logged-in orchestrator user.
|
||||||
|
service PluginReviewService {
|
||||||
|
rpc ListReviews(ListReviewsRequest) returns (ListReviewsResponse);
|
||||||
|
rpc GetMyReview(GetMyReviewRequest) returns (GetMyReviewResponse);
|
||||||
|
rpc UpsertReview(UpsertReviewRequest) returns (UpsertReviewResponse);
|
||||||
|
rpc DeleteReview(DeleteReviewRequest) returns (DeleteReviewResponse);
|
||||||
|
rpc ReplyToReview(ReplyToReviewRequest) returns (ReplyToReviewResponse);
|
||||||
|
rpc DeleteReply(DeleteReplyRequest) returns (DeleteReplyResponse);
|
||||||
|
rpc FlagReview(FlagReviewRequest) returns (FlagReviewResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
// PluginPublishService is called by the ninja CLI to publish a version.
|
// PluginPublishService is called by the ninja CLI to publish a version.
|
||||||
@ -117,10 +147,32 @@ message Plugin {
|
|||||||
string latest_version = 13;
|
string latest_version = 13;
|
||||||
repeated string tags = 14;
|
repeated string tags = 14;
|
||||||
// preview_image_url is the PUBLIC, unsigned, long-lived image URL for this
|
// preview_image_url is the PUBLIC, unsigned, long-lived image URL for this
|
||||||
// plugin's latest non-yanked version preview (a PNG screenshot), usable
|
// plugin's card image, usable directly as an <img src>: the gallery's
|
||||||
// directly as an <img src>. Empty when no preview has been published yet —
|
// Featured screenshot when one exists, else the latest non-yanked version's
|
||||||
// the wizard card then degrades to display_name + tags (spec §5.2).
|
// preview.png. Empty when neither exists — the wizard card then degrades to
|
||||||
|
// display_name + tags (spec §5.2).
|
||||||
string preview_image_url = 15;
|
string preview_image_url = 15;
|
||||||
|
// rating_avg/rating_count are the denormalised star-review aggregate over
|
||||||
|
// non-deleted reviews (0/0 when unreviewed).
|
||||||
|
float rating_avg = 16;
|
||||||
|
int32 rating_count = 17;
|
||||||
|
// install_count is the number of distinct CMS instances currently recorded
|
||||||
|
// as installers of this plugin (registry_install_events grain).
|
||||||
|
int32 install_count = 18;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginScreenshot is one image in a plugin's managed gallery. The gallery is
|
||||||
|
// per-plugin (not per-version); images arrive either from a published
|
||||||
|
// artifact's screenshots/ dir or from author uploads, and the author orders
|
||||||
|
// them and marks one Featured (the card image).
|
||||||
|
message PluginScreenshot {
|
||||||
|
string id = 1;
|
||||||
|
// url is the public unsigned image URL (only public plugins serve gallery
|
||||||
|
// images in v1).
|
||||||
|
string url = 2;
|
||||||
|
bool featured = 3;
|
||||||
|
int32 position = 4;
|
||||||
|
string content_type = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Category {
|
message Category {
|
||||||
@ -199,6 +251,8 @@ message GetPluginResponse {
|
|||||||
Plugin plugin = 1;
|
Plugin plugin = 1;
|
||||||
repeated Version versions = 2;
|
repeated Version versions = 2;
|
||||||
map<string,string> channels = 3;
|
map<string,string> channels = 3;
|
||||||
|
// screenshots is the plugin's gallery in display order (Featured included).
|
||||||
|
repeated PluginScreenshot screenshots = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ListPluginsRequest {
|
message ListPluginsRequest {
|
||||||
@ -323,8 +377,10 @@ message ResolveInstallResponse {
|
|||||||
// --- Instance-authenticated request shapes ---
|
// --- Instance-authenticated request shapes ---
|
||||||
// The instance proves its identity with the per-account X-CMS-Secret header;
|
// The instance proves its identity with the per-account X-CMS-Secret header;
|
||||||
// instance_id selects the instance whose owning account scopes the call. No
|
// instance_id selects the instance whose owning account scopes the call. No
|
||||||
// active_account_id / user JWT is needed. These cover only private plugins —
|
// active_account_id / user JWT is needed. Listing covers only private plugins
|
||||||
// public plugins use the anonymous ListPlugins / ResolveInstall.
|
// (public plugins use the anonymous ListPlugins); resolve accepts BOTH — the
|
||||||
|
// instance identity is what lets the registry record the install event, which
|
||||||
|
// backs public install counts and verified-install review gating (ADR 0021).
|
||||||
|
|
||||||
message ListPrivatePluginsForInstanceRequest {
|
message ListPrivatePluginsForInstanceRequest {
|
||||||
string instance_id = 1;
|
string instance_id = 1;
|
||||||
@ -334,6 +390,10 @@ message ResolveInstallForInstanceRequest {
|
|||||||
string instance_id = 1;
|
string instance_id = 1;
|
||||||
string plugin_name = 2;
|
string plugin_name = 2;
|
||||||
string version_or_channel = 3;
|
string version_or_channel = 3;
|
||||||
|
// scope_slug selects the plugin's scope. Empty or "private" resolves the
|
||||||
|
// calling account's private plugin (the original behavior); any other value
|
||||||
|
// resolves a PUBLIC plugin in that scope (ADR 0021).
|
||||||
|
string scope_slug = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Review / moderation ---
|
// --- Review / moderation ---
|
||||||
@ -385,6 +445,180 @@ message RequestChangesRequest {
|
|||||||
}
|
}
|
||||||
message RequestChangesResponse {}
|
message RequestChangesResponse {}
|
||||||
|
|
||||||
|
// --- Gallery service ---
|
||||||
|
|
||||||
|
message ListScreenshotsRequest {
|
||||||
|
string scope_slug = 1;
|
||||||
|
string plugin_name = 2;
|
||||||
|
}
|
||||||
|
message ListScreenshotsResponse {
|
||||||
|
repeated PluginScreenshot screenshots = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UploadScreenshotRequest {
|
||||||
|
string plugin_id = 1;
|
||||||
|
// image is the raw file bytes (≤ 8 MiB; png/jpg/jpeg/webp).
|
||||||
|
bytes image = 2;
|
||||||
|
string filename = 3;
|
||||||
|
string content_type = 4;
|
||||||
|
}
|
||||||
|
message UploadScreenshotResponse {
|
||||||
|
PluginScreenshot screenshot = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeleteScreenshotRequest {
|
||||||
|
string plugin_id = 1;
|
||||||
|
string screenshot_id = 2;
|
||||||
|
}
|
||||||
|
message DeleteScreenshotResponse {}
|
||||||
|
|
||||||
|
message ReorderScreenshotsRequest {
|
||||||
|
string plugin_id = 1;
|
||||||
|
// screenshot_ids_in_order must name every current gallery image exactly once.
|
||||||
|
repeated string screenshot_ids_in_order = 2;
|
||||||
|
}
|
||||||
|
message ReorderScreenshotsResponse {
|
||||||
|
repeated PluginScreenshot screenshots = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SetFeaturedScreenshotRequest {
|
||||||
|
string plugin_id = 1;
|
||||||
|
string screenshot_id = 2;
|
||||||
|
}
|
||||||
|
message SetFeaturedScreenshotResponse {}
|
||||||
|
|
||||||
|
// --- Star reviews (ADR 0021) ---
|
||||||
|
|
||||||
|
message PluginReview {
|
||||||
|
string id = 1;
|
||||||
|
string plugin_id = 2;
|
||||||
|
string user_id = 3;
|
||||||
|
string author_display_name = 4;
|
||||||
|
int32 rating = 5; // 1..5
|
||||||
|
string title = 6;
|
||||||
|
string body = 7;
|
||||||
|
// version_at_review is the plugin version the reviewer's account had
|
||||||
|
// installed when the review was written (best effort; may be empty).
|
||||||
|
string version_at_review = 8;
|
||||||
|
bool edited = 9;
|
||||||
|
google.protobuf.Timestamp created_at = 10;
|
||||||
|
google.protobuf.Timestamp updated_at = 11;
|
||||||
|
PluginReviewReply reply = 12; // unset when no (non-deleted) reply exists
|
||||||
|
// deleted/deleted_reason are populated only in moderation views; public
|
||||||
|
// listings exclude soft-deleted reviews entirely.
|
||||||
|
bool deleted = 13;
|
||||||
|
string deleted_reason = 14;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PluginReviewReply {
|
||||||
|
string id = 1;
|
||||||
|
string author_display_name = 2;
|
||||||
|
string body = 3;
|
||||||
|
bool edited = 4;
|
||||||
|
google.protobuf.Timestamp created_at = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ReviewAggregate {
|
||||||
|
float rating_avg = 1;
|
||||||
|
int32 rating_count = 2;
|
||||||
|
// count1..count5 are the per-star histogram buckets.
|
||||||
|
int32 count1 = 3;
|
||||||
|
int32 count2 = 4;
|
||||||
|
int32 count3 = 5;
|
||||||
|
int32 count4 = 6;
|
||||||
|
int32 count5 = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListReviewsRequest {
|
||||||
|
string scope_slug = 1;
|
||||||
|
string plugin_name = 2;
|
||||||
|
int32 limit = 3; // 0 → server default
|
||||||
|
int32 offset = 4;
|
||||||
|
}
|
||||||
|
message ListReviewsResponse {
|
||||||
|
repeated PluginReview reviews = 1;
|
||||||
|
ReviewAggregate aggregate = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetMyReviewRequest {
|
||||||
|
string plugin_id = 1;
|
||||||
|
}
|
||||||
|
message GetMyReviewResponse {
|
||||||
|
PluginReview review = 1; // unset when the caller has no review
|
||||||
|
// eligible reports whether the caller may write a review (verified install
|
||||||
|
// on their account, not a scope member). The form UI keys off this.
|
||||||
|
bool eligible = 2;
|
||||||
|
// ineligible_reason is a human-readable explanation when eligible = false
|
||||||
|
// ("no install on your account", "authors cannot review their own plugin").
|
||||||
|
string ineligible_reason = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpsertReviewRequest {
|
||||||
|
string plugin_id = 1;
|
||||||
|
int32 rating = 2; // 1..5, required
|
||||||
|
string title = 3;
|
||||||
|
string body = 4;
|
||||||
|
}
|
||||||
|
message UpsertReviewResponse {
|
||||||
|
PluginReview review = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeleteReviewRequest {
|
||||||
|
string plugin_id = 1;
|
||||||
|
}
|
||||||
|
message DeleteReviewResponse {}
|
||||||
|
|
||||||
|
message ReplyToReviewRequest {
|
||||||
|
string review_id = 1;
|
||||||
|
string body = 2;
|
||||||
|
}
|
||||||
|
message ReplyToReviewResponse {
|
||||||
|
PluginReviewReply reply = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeleteReplyRequest {
|
||||||
|
string review_id = 1;
|
||||||
|
}
|
||||||
|
message DeleteReplyResponse {}
|
||||||
|
|
||||||
|
message FlagReviewRequest {
|
||||||
|
string review_id = 1;
|
||||||
|
string reason = 2; // required
|
||||||
|
}
|
||||||
|
message FlagReviewResponse {}
|
||||||
|
|
||||||
|
// --- Review moderation (superadmin) ---
|
||||||
|
|
||||||
|
message FlaggedReview {
|
||||||
|
PluginReview review = 1;
|
||||||
|
string scope_slug = 2;
|
||||||
|
string plugin_name = 3;
|
||||||
|
int32 flag_count = 4;
|
||||||
|
// flag_reasons carries the open flags' reasons, newest first.
|
||||||
|
repeated string flag_reasons = 5;
|
||||||
|
google.protobuf.Timestamp first_flagged_at = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListFlaggedReviewsRequest {
|
||||||
|
int32 limit = 1;
|
||||||
|
int32 offset = 2;
|
||||||
|
}
|
||||||
|
message ListFlaggedReviewsResponse {
|
||||||
|
repeated FlaggedReview reviews = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SoftDeleteReviewRequest {
|
||||||
|
string review_id = 1;
|
||||||
|
string reason = 2; // required — audit trail
|
||||||
|
}
|
||||||
|
message SoftDeleteReviewResponse {}
|
||||||
|
|
||||||
|
message SoftDeleteReplyRequest {
|
||||||
|
string review_id = 1;
|
||||||
|
string reason = 2; // required — audit trail
|
||||||
|
}
|
||||||
|
message SoftDeleteReplyResponse {}
|
||||||
|
|
||||||
// --- Publish service ---
|
// --- Publish service ---
|
||||||
|
|
||||||
message PublishVersionRequest {
|
message PublishVersionRequest {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user