Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 68 additions & 15 deletions cli/cmd/plugin/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
45 changes: 45 additions & 0 deletions cli/internal/registry/plugins-registry.yaml
Original file line number Diff line number Diff line change
@@ -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
76 changes: 76 additions & 0 deletions cli/internal/registry/registry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package registry

import (
_ "embed" // blank import required to activate //go:embed
"fmt"
"sort"

"gopkg.in/yaml.v3"
)

// 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"
)

// 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
}
115 changes: 115 additions & 0 deletions cli/internal/registry/registry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
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.
// 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) {
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()
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])
}
}
}

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")
}
}
Loading