The .bnp packers never carried preview.png (the .so-era source archives did implicitly), so any republish silently wiped a theme's registry preview. Both builders now pack an optional root preview.png and a screenshots/ dir (png/jpg/jpeg/webp, <=8MB each, <=12 files, validated), reported in the build/verify summaries. ninja theme screenshot gains --pages/--screenshots-dir to capture NN-<slug>.png per gallery page for the registry gallery. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
143 lines
4.9 KiB
Go
143 lines
4.9 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"git.dev.alexdunmow.com/block/cli/internal/bnp"
|
|
)
|
|
|
|
func newPluginBuildCmd() *cobra.Command {
|
|
var dir, output string
|
|
var codeless bool
|
|
cmd := &cobra.Command{
|
|
Use: "build",
|
|
Short: "Compile a plugin to wasm and pack a .bnp artifact",
|
|
Long: `Compile the plugin in --dir to reactor-mode wasip1 wasm, extract its
|
|
static manifest by instantiating the module once (HOOK_DESCRIBE), and pack a
|
|
.bnp (tar.zst of plugin.wasm, plugin.mod, manifest.pb, plus migrations/,
|
|
schemas/, assets/, web/dist when present).
|
|
|
|
No Docker or podman: the whole pipeline is the local Go toolchain (>= 1.24)
|
|
plus wazero. This replaces the in-container .so compile.`,
|
|
RunE: func(c *cobra.Command, _ []string) error {
|
|
// Classify by repo shape (WO-WZ-020): no Go source → codeless
|
|
// (declarative, no wasm); Go source → wasm. --codeless asserts
|
|
// the expectation and fails loudly on a mismatch.
|
|
isCodeless, err := bnp.IsCodelessRepo(dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if codeless && !isCodeless {
|
|
return fmt.Errorf("--codeless asserted but %s contains Go source — a codeless plugin has no code (delete the Go or drop the flag)", dir)
|
|
}
|
|
var res *bnp.BuildResult
|
|
if isCodeless {
|
|
res, err = bnp.BuildCodeless(context.Background(), bnp.BuildOptions{Dir: dir, Output: output})
|
|
} else {
|
|
res, err = bnp.Build(context.Background(), bnp.BuildOptions{Dir: dir, Output: output})
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
printBuildSummary(res)
|
|
return nil
|
|
},
|
|
}
|
|
cmd.Flags().StringVar(&dir, "dir", ".", "Plugin repo directory")
|
|
cmd.Flags().StringVarP(&output, "output", "o", "", "Output .bnp path (default <name>-<version>.bnp)")
|
|
cmd.Flags().BoolVar(&codeless, "codeless", false, "Assert the repo builds a codeless (no-wasm) artifact; fail if it contains Go source")
|
|
return cmd
|
|
}
|
|
|
|
func newPluginVerifyCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "verify <file.bnp>",
|
|
Short: "Validate a .bnp against the loader's layout/name/abi/path/size checks",
|
|
Long: `Re-run the CMS reader's checks on a .bnp standalone so CI and the registry
|
|
can gate uploads: required members present, path-safety and size caps on
|
|
extraction, manifest decodes, abi_version supported, and manifest name matches
|
|
plugin.mod. Prints a named reason for each malformed class.`,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(c *cobra.Command, args []string) error {
|
|
res, err := bnp.Verify(args[0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
printVerifySummary(args[0], res)
|
|
return nil
|
|
},
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
func printBuildSummary(r *bnp.BuildResult) {
|
|
hooks := "(none)"
|
|
if len(r.Hooks) > 0 {
|
|
hooks = strings.Join(r.Hooks, ", ")
|
|
}
|
|
dirs := "(none)"
|
|
if len(r.IncludedDirs) > 0 {
|
|
dirs = strings.Join(r.IncludedDirs, ", ")
|
|
}
|
|
kind := "wasm"
|
|
if r.Codeless {
|
|
kind = "codeless"
|
|
}
|
|
fmt.Printf("Built %s@%s (%s) → %s\n", r.Name, r.Version, kind, r.OutputPath)
|
|
fmt.Println(" ┌───────────────────────────────────────────")
|
|
fmt.Printf(" │ artifact size %s (%s uncompressed)\n", humanBytes(r.ArtifactBytes), humanBytes(r.UncompBytes))
|
|
fmt.Printf(" │ plugin.wasm %s\n", humanBytes(r.WasmBytes))
|
|
fmt.Printf(" │ manifest.pb %s\n", humanBytes(r.ManifestBytes))
|
|
fmt.Printf(" │ blocks %d\n", r.BlockCount)
|
|
fmt.Printf(" │ templates %d\n", r.TemplateCount)
|
|
fmt.Printf(" │ admin pages %d\n", r.AdminPages)
|
|
fmt.Printf(" │ job types %d\n", r.JobTypes)
|
|
fmt.Printf(" │ hooks %s\n", hooks)
|
|
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.Println(" └───────────────────────────────────────────")
|
|
}
|
|
|
|
func printVerifySummary(path string, r *bnp.VerifyResult) {
|
|
var dirs []string
|
|
if r.HasMigrations {
|
|
dirs = append(dirs, "migrations")
|
|
}
|
|
if r.HasSchemas {
|
|
dirs = append(dirs, "schemas")
|
|
}
|
|
if r.HasAssets {
|
|
dirs = append(dirs, "assets")
|
|
}
|
|
if r.HasWeb {
|
|
dirs = append(dirs, "web")
|
|
}
|
|
joined := "(none)"
|
|
if len(dirs) > 0 {
|
|
joined = strings.Join(dirs, ", ")
|
|
}
|
|
fmt.Printf("OK: %s\n", path)
|
|
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.HasPreview, r.ScreenshotCount)
|
|
}
|
|
|
|
// humanBytes formats a byte count with a binary unit suffix.
|
|
func humanBytes(n int64) string {
|
|
const unit = 1024
|
|
if n < unit {
|
|
return fmt.Sprintf("%d B", n)
|
|
}
|
|
div, exp := int64(unit), 0
|
|
for x := n / unit; x >= unit; x /= unit {
|
|
div *= unit
|
|
exp++
|
|
}
|
|
return fmt.Sprintf("%.1f %ciB", float64(n)/float64(div), "KMGTPE"[exp])
|
|
}
|