Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Example of Consuming an External CLI Plugin #11

Merged
merged 1 commit into from
Jul 25, 2024
Merged
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# project specific
my-plugins/

# Binaries for programs and plugins
.DS_Store
*.exe
Expand Down
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,46 @@

🔑 To access the Deepgram API you will need a [free Deepgram API Key](https://console.deepgram.com/signup?jump=keys).

## Building the CLI

When you build the Deepgram CLI for the current platform/architecture of your laptop (for example, macOS arm64), you simply need to be at the root of the repo and run:

```bash
go build .
```

> **IMPORTANT:** In order to support multiple platforms, you need to have build mechanisms (ie a Makefile for example) to orchestrate making the binaries for all your target platforms (macOS x86, macOS arm64, Linux amd64, etc, etc).

## External Plugins

### Consuming an External Plugin

You should be able to use the `deepgram-cli` to manage the plugins or download new plugins to your system. You can do this with the `deepgram-cli plugins` command.

```bash
TODO
```

### Manually Installing an External Plugin

For testing or 3rd party purposes, you can also load plugins manually. To do this, copy your Deepgram CLI plugin (this will be a `.so` file extension) into a subfolder called `plugins`.

You will need an accompanying plugin description file, this description file should be the same filename with a `.plugin` extension instead of `.so`.

The contents of the `.plugin` file will look like:

```json
{
"name": "<your plugin name>",
"description": "An example plugin description",
"version": "0.0.1"
}
```

There is an optional field in this JSON that could be used when your root [Cobra CLI Command](https://github.com/spf13/cobra) and initiation function is named something other than `MainCmd` and `InitMain`, respectively. You can specify an optional property named `entrypoint`.

Next time you run the Deepgram CLI, you should see your plugin command available on the command line.

## Development and Contributing

Interested in contributing? We ❤️ pull requests!
Expand Down
11 changes: 0 additions & 11 deletions cmd/listen/listen.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@
package listen

import (
"fmt"

"github.com/spf13/cobra"

"github.com/deepgram-devs/deepgram-cli/cmd"
rest "github.com/deepgram-devs/deepgram-cli/cmd/listen/rest"
)

// listenCmd represents the listen command

Check failure on line 14 in cmd/listen/listen.go

View workflow job for this annotation

GitHub Actions / Lint

ST1022: comment on exported var ListenCmd should be of the form "ListenCmd ..." (stylecheck)
var ListenCmd = &cobra.Command{
Use: "listen",
Short: "A brief description of your command",
Expand All @@ -23,15 +21,6 @@
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("listen (Speech-to-Text) called")
fmt.Println("Available subcommands:")
if len(args) == 0 {
for _, subCmd := range cmd.Commands() {
fmt.Printf("- %s: %s\n", subCmd.Use, subCmd.Short)
}
}
},
}

func init() {
Expand Down
53 changes: 53 additions & 0 deletions cmd/plugins/plugins.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright 2024 Deepgram CLI contributors. All Rights Reserved.
// Use of this source code is governed by a MIT license that can be found in the LICENSE file.
// SPDX-License-Identifier: MIT

package plugins

import (
"fmt"

"github.com/spf13/cobra"

"github.com/deepgram-devs/deepgram-cli/cmd"
"github.com/deepgram-devs/deepgram-cli/pkg/plugins"
)

// pluginCmd represents the manage command
var pluginCmd = &cobra.Command{
Use: "plugins",
Short: "This manages plugins in this installation.",
Long: `This manages plugins current instaled. For example:

list - lists all plugins
install -name <PLUGIN NAME> - installs a plugin
uninstall -name <PLUGIN NAME> - uninstalls a plugin`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("plugins called")
if len(args) == 0 {
pluginsInstalled := plugins.ListInstalledPlugins()
for name := range pluginsInstalled {
fmt.Printf("Plugin: %s\n", name)
}
}
},
}

func init() {
// // load all plugins
// pluginsInstalled := plugins.ListInstalledPlugins()
// for name, plugin := range pluginsInstalled {
// fmt.Printf("-----------------> Adding plugin: %s\n", name)
// cmd.RootCmd.AddCommand(plugin.Cmd)
// }

// add the plugin command
cmd.RootCmd.AddCommand(pluginCmd)

// Cobra supports Persistent Flags which will work for this command and all subcommands, e.g.:
pluginCmd.PersistentFlags().String("name", "", "Plugin name")

// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// manageCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
42 changes: 42 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
package cmd

import (
"log"
"os"
"strings"

"github.com/deepgram-devs/deepgram-cli/pkg/plugins"
"github.com/spf13/cobra"
)

// rootCmd represents the base command when called without any subcommands

Check failure on line 16 in cmd/root.go

View workflow job for this annotation

GitHub Actions / Lint

ST1022: comment on exported var RootCmd should be of the form "RootCmd ..." (stylecheck)
var RootCmd = &cobra.Command{
Use: "cli",
Short: "A brief description of your application",
Expand All @@ -35,11 +38,50 @@
}

func init() {
// Get the list of files in the "plugins" subdirectory
files, err := os.ReadDir("plugins")
if err != nil {
log.Fatal(err)
}

// Iterate over the files and create a new plugin for each one
for _, file := range files {
if !file.IsDir() && strings.HasSuffix(file.Name(), ".plugin") {

Check failure on line 49 in cmd/root.go

View workflow job for this annotation

GitHub Actions / Lint

nestingReduce: invert if cond, replace body with `continue`, move old body after the statement (gocritic)
// remove the ".plugin" suffix
pluginName := strings.TrimSuffix(file.Name(), ".plugin")

desc, err := plugins.DiscoverPlugin(pluginName)
if err != nil {
log.Printf("Failed to install plugin %s: %v", pluginName, err)
continue
}

pluginCmd, err := plugins.LoadPlugin(pluginName, desc.EntryPoint)
if err != nil {
log.Printf("Failed to install plugin %s: %v", pluginName, err)
continue
}

// add command
RootCmd.AddCommand(pluginCmd)
}
}

// load all plugins
pluginsInstalled := plugins.ListInstalledPlugins()
for _, plugin := range pluginsInstalled {
// fmt.Printf("-----------------> Adding plugin: %s\n", name)

Check failure on line 73 in cmd/root.go

View workflow job for this annotation

GitHub Actions / Lint

commentedOutCode: may want to remove commented-out code (gocritic)
RootCmd.AddCommand(plugin.Cmd)
}

// TODO: disable completion command?
RootCmd.Root().CompletionOptions.DisableDefaultCmd = true

// Here you will define your flags and configuration settings.
// Cobra supports persistent flags, which, if defined here,
// will be global for your application.

// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cli.yaml)")

Check failure on line 84 in cmd/root.go

View workflow job for this annotation

GitHub Actions / Lint

commentedOutCode: may want to remove commented-out code (gocritic)

// Cobra also supports local flags, which will only run
// when this action is called directly.
Expand Down
11 changes: 0 additions & 11 deletions cmd/speak/speak.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@
package speak

import (
"fmt"

"github.com/spf13/cobra"

"github.com/deepgram-devs/deepgram-cli/cmd"
"github.com/deepgram-devs/deepgram-cli/cmd/speak/rest"
)

// speakCmd represents the speak command

Check failure on line 14 in cmd/speak/speak.go

View workflow job for this annotation

GitHub Actions / Lint

ST1022: comment on exported var SpeakCmd should be of the form "SpeakCmd ..." (stylecheck)
var SpeakCmd = &cobra.Command{
Use: "speak",
Short: "A brief description of your command",
Expand All @@ -23,15 +21,6 @@
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("speak (Text-to-Speech) called")
fmt.Println("Available subcommands:")
if len(args) == 0 {
for _, subCmd := range cmd.Commands() {
fmt.Printf("- %s: %s\n", subCmd.Use, subCmd.Short)
}
}
},
}

func init() {
Expand Down
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
_ "github.com/deepgram-devs/deepgram-cli/cmd/analyze"
_ "github.com/deepgram-devs/deepgram-cli/cmd/listen"
_ "github.com/deepgram-devs/deepgram-cli/cmd/manage"
_ "github.com/deepgram-devs/deepgram-cli/cmd/plugins"
_ "github.com/deepgram-devs/deepgram-cli/cmd/selfhosted"
_ "github.com/deepgram-devs/deepgram-cli/cmd/speak"
)
Expand Down
132 changes: 132 additions & 0 deletions pkg/plugins/actions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Copyright 2024 Deepgram CLI contributors. All Rights Reserved.
// Use of this source code is governed by a MIT license that can be found in the LICENSE file.
// SPDX-License-Identifier: MIT

package plugins

import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"plugin"

"github.com/spf13/cobra"
)

// plugins that are currently installed
var pluginsInstalled = make(map[string]*Plugin)
var pluginDescriptors = make(map[string]*PluginDescriptor)

// ListInstalledPlugins returns a list of installed plugins
func ListInstalledPlugins() map[string]*Plugin {
return pluginsInstalled
}

func GetPlugin(pluginName string) *Plugin {
return pluginsInstalled[pluginName]
}

func DiscoverPlugin(pluginName string) (*PluginDescriptor, error) {
// Get the plugin binary file
binaryFilePath := filepath.Join("plugins", pluginName+".so")
_, err := os.Stat(binaryFilePath)
if err != nil {
fmt.Printf("Failed to find plugin binary %s: %v\n", binaryFilePath, err)
return nil, err
}

// Get the plugin descriptor file
jsonFilePath := filepath.Join("plugins", pluginName+".plugin")
_, err = os.Stat(jsonFilePath)
if err != nil {
fmt.Printf("Failed to find plugin descriptor %s: %v\n", jsonFilePath, err)
return nil, err
}

// Open the ".plugin" file
fileContent, err := os.ReadFile(jsonFilePath)
if err != nil {
log.Printf("Failed to read file %s: %v", jsonFilePath, err)
return nil, err
}

// Load the JSON according to the Plugin Descriptor
var pluginDescriptor PluginDescriptor
err = json.Unmarshal(fileContent, &pluginDescriptor)
if err != nil {
log.Printf("Failed to unmarshal plugin descriptor from file %s: %v", jsonFilePath, err)
return nil, err
}

// load and save the plugin
newPlugin, err := NewPlugin(&pluginDescriptor)
if err != nil {
log.Printf("Failed to load plugin from file %s: %v", jsonFilePath, err)
return nil, err
}

pluginsInstalled[pluginName] = newPlugin
pluginDescriptors[pluginName] = &pluginDescriptor
return &pluginDescriptor, nil
}

func LoadPlugin(pluginName, commandName string) (*cobra.Command, error) {
// Get the plugin binary file
binaryFilePath := filepath.Join("plugins", pluginName+".so")
// fmt.Printf("---------> binaryFilePath: %s\n", binaryFilePath)

Check failure on line 78 in pkg/plugins/actions.go

View workflow job for this annotation

GitHub Actions / Lint

commentedOutCode: may want to remove commented-out code (gocritic)
_, err := os.Stat(binaryFilePath)
if err != nil {
fmt.Printf("Failed to find plugin binary %s: %v\n", binaryFilePath, err)
return nil, err
}

// get plugin command
p, err := plugin.Open(binaryFilePath)
if err != nil {
fmt.Printf("Failed to Open plugin %s: %v\n", binaryFilePath, err)
return nil, err
}
b, err := p.Lookup(commandName + "Cmd")
if err != nil {
fmt.Printf("-----> Failed to Lookup plugin %s: %v\n", binaryFilePath, err)
return nil, err
}
f, err := p.Lookup("Init" + commandName)
if err == nil {
f.(func())()
}

fmt.Printf("Plugin %s installed\n", pluginName)
return *b.(**cobra.Command), nil
}

func UninstallPlugin(pluginName string) error {
delete(pluginsInstalled, pluginName)
delete(pluginDescriptors, pluginName)

// Remove the plugin file
jsonFilePath := filepath.Join("plugins", pluginName+".plugin")
err := os.Remove(jsonFilePath)
if err != nil {
log.Printf("Failed to remove plugin file %s: %v", jsonFilePath, err)
return err
}

// Remove the plugin binary
binaryFilePath := filepath.Join("plugins", pluginName)
err = os.Remove(binaryFilePath)
if err != nil {
log.Printf("Failed to remove plugin %s: %v", binaryFilePath, err)
return err
}

log.Printf("Plugin %s uninstalled", pluginName)
return nil
}

func DownloadPlugin(pluginName string) error {
// TODO: Implement this...
return nil
}
16 changes: 16 additions & 0 deletions pkg/plugins/interfaces/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright 2024 Deepgram CLI contributors. All Rights Reserved.
// Use of this source code is governed by a MIT license that can be found in the LICENSE file.
// SPDX-License-Identifier: MIT

package interfaces

// CmdGroup is a group of CLI commands.
type CmdGroup string

const (
// StartersCmdGroup are commands associated with starter apps.
StartersCmdGroup CmdGroup = "Starters"

// TestCmdGroup is the test command group.
TestCmdGroup CmdGroup = "Test"
)
Loading
Loading