From 00c460c18f71ce1e47588e14c547713c73730059 Mon Sep 17 00:00:00 2001 From: Quigley Malcolm Date: Mon, 15 Jun 2026 16:43:29 -0500 Subject: [PATCH 1/3] feat(cli): embed plugin registry Adds cli/internal/registry/ with the plugin registry embedded into the binary via //go:embed. The registry is read at runtime with no network call required. Initial registry contains placeholder 0.1.0 entries for the five known platforms (dbt, gooddata, polaris, salesforce, snowflake). Checksums are marked sha256:placeholder pending real plugin packaging. Entries will be updated with real tags and checksums when converters are packaged as distributable OSSIE plugins. Wiring into cmd/plugin/list.go and cmd/plugin/install.go is deferred to P1 and P2 respectively. --- cli/internal/registry/plugins-registry.yaml | 45 ++++++++ cli/internal/registry/registry.go | 74 +++++++++++++ cli/internal/registry/registry_test.go | 110 ++++++++++++++++++++ 3 files changed, 229 insertions(+) create mode 100644 cli/internal/registry/plugins-registry.yaml create mode 100644 cli/internal/registry/registry.go create mode 100644 cli/internal/registry/registry_test.go diff --git a/cli/internal/registry/plugins-registry.yaml b/cli/internal/registry/plugins-registry.yaml new file mode 100644 index 0000000..9a03d86 --- /dev/null +++ b/cli/internal/registry/plugins-registry.yaml @@ -0,0 +1,45 @@ +# OSSIE Plugin Registry +# +# Format: each top-level key is a platform name. The value is a list of +# versioned entries ordered oldest → newest; the last entry is the latest. +# +# Entry types: +# git_release — download from a GitHub release tarball +# raw_url — download from an arbitrary URL +# +# NOTE: All entries below are placeholders pending plugin packaging. + +dbt: + - version: "0.1.0" + type: git_release + url: https://github.com/open-semantic-interchange/OSI + tag: plugin-dbt-v0.1.0 + checksum: sha256:placeholder + +gooddata: + - version: "0.1.0" + type: git_release + url: https://github.com/open-semantic-interchange/OSI + tag: plugin-gooddata-v0.1.0 + checksum: sha256:placeholder + +polaris: + - version: "0.1.0" + type: git_release + url: https://github.com/open-semantic-interchange/OSI + tag: plugin-polaris-v0.1.0 + checksum: sha256:placeholder + +salesforce: + - version: "0.1.0" + type: git_release + url: https://github.com/open-semantic-interchange/OSI + tag: plugin-salesforce-v0.1.0 + checksum: sha256:placeholder + +snowflake: + - version: "0.1.0" + type: git_release + url: https://github.com/open-semantic-interchange/OSI + tag: plugin-snowflake-v0.1.0 + checksum: sha256:placeholder diff --git a/cli/internal/registry/registry.go b/cli/internal/registry/registry.go new file mode 100644 index 0000000..cc425df --- /dev/null +++ b/cli/internal/registry/registry.go @@ -0,0 +1,74 @@ +package registry + +import ( + _ "embed" // blank import required to activate //go:embed + "fmt" + "sort" + + "gopkg.in/yaml.v3" +) + +// Entry types supported by the registry. +const ( + EntryTypeGitRelease = "git_release" + EntryTypeRawURL = "raw_url" +) + +// Entry is a single versioned release of a plugin for one platform. +type Entry struct { + Version string `yaml:"version"` + Type string `yaml:"type"` // EntryTypeGitRelease or EntryTypeRawURL + URL string `yaml:"url"` + Tag string `yaml:"tag,omitempty"` // git_release only + Checksum string `yaml:"checksum"` +} + +// Registry maps a platform name to its list of versioned entries, ordered +// oldest to newest. The last entry in each slice is the latest version. +type Registry map[string][]Entry + +//go:embed plugins-registry.yaml +var registryData []byte + +// Load parses the embedded plugins-registry.yaml and returns a Registry. +// An error here indicates a build-time defect; registry_test.go validates +// the embedded data on every test run. +func Load() (Registry, error) { + var r Registry + if err := yaml.Unmarshal(registryData, &r); err != nil { + return nil, fmt.Errorf("failed to parse embedded plugin registry: %w", err) + } + return r, nil +} + +// Platforms returns all platform names in the registry in alphabetical order. +func (r Registry) Platforms() []string { + names := make([]string, 0, len(r)) + for name := range r { + names = append(names, name) + } + sort.Strings(names) + return names +} + +// LatestEntry returns the last (newest) entry for the given platform. +// It returns false if the platform is not in the registry or has no entries. +// Registry maintainers are responsible for keeping entries ordered oldest → newest. +func (r Registry) LatestEntry(platform string) (Entry, bool) { + entries, ok := r[platform] + if !ok || len(entries) == 0 { + return Entry{}, false + } + return entries[len(entries)-1], true +} + +// FindEntry returns the entry matching the given platform and version. +// It returns false if the platform or version is not found. +func (r Registry) FindEntry(platform, version string) (Entry, bool) { + for _, entry := range r[platform] { + if entry.Version == version { + return entry, true + } + } + return Entry{}, false +} diff --git a/cli/internal/registry/registry_test.go b/cli/internal/registry/registry_test.go new file mode 100644 index 0000000..9e110ce --- /dev/null +++ b/cli/internal/registry/registry_test.go @@ -0,0 +1,110 @@ +package registry_test + +import ( + "testing" + + "github.com/open-semantic-interchange/ossie/cli/internal/registry" +) + +// knownPlatforms is the authoritative list of platforms that must be present +// in the embedded registry for the CLI to have useful output. +var knownPlatforms = []string{"dbt", "gooddata", "polaris", "salesforce", "snowflake"} + +func TestLoad_succeeds(t *testing.T) { + reg, err := registry.Load() + if err != nil { + t.Fatalf("Load() returned error: %v", err) + } + if reg == nil { + t.Fatal("Load() returned nil registry") + } +} + +func TestLoad_containsAllPlatforms(t *testing.T) { + reg, err := registry.Load() + if err != nil { + t.Fatalf("Load() returned error: %v", err) + } + for _, platform := range knownPlatforms { + entries, ok := reg[platform] + if !ok { + t.Errorf("platform %q not found in registry", platform) + continue + } + if len(entries) == 0 { + t.Errorf("platform %q has no entries", platform) + } + } +} + +func TestRegistry_Platforms_sorted(t *testing.T) { + reg, err := registry.Load() + if err != nil { + t.Fatalf("Load() returned error: %v", err) + } + got := reg.Platforms() + for i := 1; i < len(got); i++ { + if got[i] < got[i-1] { + t.Errorf("Platforms() not sorted: %q appears after %q", got[i-1], got[i]) + } + } +} + +func TestRegistry_LatestEntry_found(t *testing.T) { + reg, err := registry.Load() + if err != nil { + t.Fatalf("Load() returned error: %v", err) + } + entry, ok := reg.LatestEntry("dbt") + if !ok { + t.Fatal("LatestEntry(\"dbt\") returned false, want true") + } + if entry.Version == "" { + t.Error("LatestEntry(\"dbt\") returned entry with empty Version") + } + if entry.Type == "" { + t.Error("LatestEntry(\"dbt\") returned entry with empty Type") + } + if entry.URL == "" { + t.Error("LatestEntry(\"dbt\") returned entry with empty URL") + } + if entry.Checksum == "" { + t.Error("LatestEntry(\"dbt\") returned entry with empty Checksum") + } +} + +func TestRegistry_LatestEntry_notFound(t *testing.T) { + reg, err := registry.Load() + if err != nil { + t.Fatalf("Load() returned error: %v", err) + } + _, ok := reg.LatestEntry("does-not-exist") + if ok { + t.Error("LatestEntry(\"does-not-exist\") returned true, want false") + } +} + +func TestRegistry_FindEntry_found(t *testing.T) { + reg, err := registry.Load() + if err != nil { + t.Fatalf("Load() returned error: %v", err) + } + entry, ok := reg.FindEntry("dbt", "0.1.0") + if !ok { + t.Fatal("FindEntry(\"dbt\", \"0.1.0\") returned false, want true") + } + if entry.Version != "0.1.0" { + t.Errorf("FindEntry version: got %q, want %q", entry.Version, "0.1.0") + } +} + +func TestRegistry_FindEntry_notFound(t *testing.T) { + reg, err := registry.Load() + if err != nil { + t.Fatalf("Load() returned error: %v", err) + } + _, ok := reg.FindEntry("dbt", "99.99.99") + if ok { + t.Error("FindEntry(\"dbt\", \"99.99.99\") returned true, want false") + } +} From 31678c8493b32fad859e053c93ab238da00bbdc7 Mon Sep 17 00:00:00 2001 From: Quigley Malcolm Date: Mon, 15 Jun 2026 16:57:28 -0500 Subject: [PATCH 2/3] docs(cli): address minor review comments in registry package - Add comment on EntryType constants noting they are consumed by P2 - Clarify why knownPlatforms is package-level (slices cannot be const) - Guard TestRegistry_Platforms_sorted against vacuous pass on empty slice --- cli/internal/registry/registry.go | 2 ++ cli/internal/registry/registry_test.go | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/cli/internal/registry/registry.go b/cli/internal/registry/registry.go index cc425df..d2817bf 100644 --- a/cli/internal/registry/registry.go +++ b/cli/internal/registry/registry.go @@ -9,6 +9,8 @@ import ( ) // Entry types supported by the registry. +// These constants are consumed by P2 (plugin install) when dispatching +// on how to download and extract a plugin archive. const ( EntryTypeGitRelease = "git_release" EntryTypeRawURL = "raw_url" diff --git a/cli/internal/registry/registry_test.go b/cli/internal/registry/registry_test.go index 9e110ce..92288d8 100644 --- a/cli/internal/registry/registry_test.go +++ b/cli/internal/registry/registry_test.go @@ -8,6 +8,8 @@ import ( // knownPlatforms is the authoritative list of platforms that must be present // in the embedded registry for the CLI to have useful output. +// Declared at package level (not inside the test) because slices cannot be +// constants in Go, and multiple tests reference this list. var knownPlatforms = []string{"dbt", "gooddata", "polaris", "salesforce", "snowflake"} func TestLoad_succeeds(t *testing.T) { @@ -43,6 +45,9 @@ func TestRegistry_Platforms_sorted(t *testing.T) { t.Fatalf("Load() returned error: %v", err) } got := reg.Platforms() + if len(got) == 0 { + t.Fatal("Platforms() returned empty slice") + } for i := 1; i < len(got); i++ { if got[i] < got[i-1] { t.Errorf("Platforms() not sorted: %q appears after %q", got[i-1], got[i]) From 97dde3164d56c675dec5fcb401ee65b5c82a0d9c Mon Sep 17 00:00:00 2001 From: Quigley Malcolm Date: Wed, 24 Jun 2026 13:54:16 -0500 Subject: [PATCH 3/3] feat(plugin): wire registry into ossie plugin list (P1) Replaces the basic installed-only listing with a full P1 implementation that cross-references installed plugins against the embedded registry. Output now shows NAME/STATUS/INSTALLED/LATEST for all registry-known platforms, with status values of installed, update available, or not installed. Community plugins (installed but absent from the registry) are printed in a separate section below. The previous no-plugins-installed short-circuit is replaced by a no-plugins-available message that fires only when the registry is empty and nothing is installed. Matching installed plugins to registry entries is done by Platform.Name, not by directory name, since the registry is keyed by platform. --- cli/cmd/plugin/list.go | 83 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 68 insertions(+), 15 deletions(-) diff --git a/cli/cmd/plugin/list.go b/cli/cmd/plugin/list.go index 01f52d0..ceb0b37 100644 --- a/cli/cmd/plugin/list.go +++ b/cli/cmd/plugin/list.go @@ -3,45 +3,98 @@ package plugin import ( "fmt" "os" - "path/filepath" "text/tabwriter" "github.com/spf13/cobra" "github.com/open-semantic-interchange/ossie/cli/internal/ossiedir" "github.com/open-semantic-interchange/ossie/cli/internal/plugin" + "github.com/open-semantic-interchange/ossie/cli/internal/registry" ) var listCmd = &cobra.Command{ Use: "list", - Short: "List installed plugins", - // TODO(P1): cross-reference against the embedded plugin registry to show - // latest available versions and update indicators once registry embedding - // (F4) is implemented. - RunE: runPluginList, + Short: "List available and installed plugins", + RunE: runPluginList, } func runPluginList(cmd *cobra.Command, args []string) error { + reg, err := registry.Load() + if err != nil { + return err + } + pluginsDir, err := ossiedir.PluginDir() if err != nil { return err } - plugins, err := plugin.Discover(pluginsDir, os.Stderr) + installed, err := plugin.Discover(pluginsDir, os.Stderr) if err != nil { return err } - if len(plugins) == 0 { - fmt.Fprintln(cmd.OutOrStdout(), "no plugins installed") + // Index installed plugins by platform name for O(1) lookup. + installedByPlatform := make(map[string]*plugin.Plugin, len(installed)) + for _, p := range installed { + installedByPlatform[p.Platform.Name] = p + } + + // Identify community plugins: installed but absent from the registry. + var community []*plugin.Plugin + for _, p := range installed { + if _, ok := reg.LatestEntry(p.Platform.Name); !ok { + community = append(community, p) + } + } + + platforms := reg.Platforms() // sorted alphabetically + out := cmd.OutOrStdout() + + if len(platforms) == 0 && len(installed) == 0 { + fmt.Fprintln(out, "no plugins available") return nil } - w := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 2, ' ', 0) - fmt.Fprintln(w, "NAME\tPLATFORM\tSPEC") - for _, p := range plugins { - name := filepath.Base(p.Path) - fmt.Fprintf(w, "%s\t%s\t%s\n", name, p.Platform.Name, p.OSSIEPluginSpec) + if len(platforms) > 0 { + w := tabwriter.NewWriter(out, 0, 0, 2, ' ', 0) + fmt.Fprintln(w, "NAME\tSTATUS\tINSTALLED\tLATEST") + for _, platform := range platforms { + latest, _ := reg.LatestEntry(platform) + p, isInstalled := installedByPlatform[platform] + + var status, installedVer string + if isInstalled { + installedVer = p.OSSIEPluginSpec + if installedVer == latest.Version { + status = "installed" + } else { + status = "update available" + } + } else { + installedVer = "—" + status = "not installed" + } + + fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", platform, status, installedVer, latest.Version) + } + if err := w.Flush(); err != nil { + return err + } } - return w.Flush() + + if len(community) > 0 { + fmt.Fprintln(out) + fmt.Fprintln(out, "Community plugins:") + w := tabwriter.NewWriter(out, 0, 0, 2, ' ', 0) + fmt.Fprintln(w, "NAME\tINSTALLED") + for _, p := range community { + fmt.Fprintf(w, "%s\t%s\n", p.Platform.Name, p.OSSIEPluginSpec) + } + if err := w.Flush(); err != nil { + return err + } + } + + return nil }