Skip to content

Commit

Permalink
Example of Consuming an External CLI Plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
dvonthenen committed Jul 25, 2024
1 parent 4735f1a commit afbeef5
Show file tree
Hide file tree
Showing 13 changed files with 460 additions and 22 deletions.
File renamed without changes.
File renamed without changes.
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
67 changes: 67 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 Plugin

### 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.

## Documentation

You can learn more about the Deepgram API at [developers.deepgram.com](https://developers.deepgram.com/docs).
Expand All @@ -18,6 +58,33 @@ To make sure our community is safe for all, be sure to review and agree to our
[Code of Conduct](./CODE_OF_CONDUCT.md). Then see the
[Contribution](./CONTRIBUTING.md) guidelines for more information.

## Using the DG CLI

From the root of the repo:

```bash
go run main.go
```

### Installing Plugins

After installing, you should see the commands from the plugin in the main command chain.

```bash
mkdir ./plugins
cp [PLUGIN NAME].so ./plugins

cat >./plugin/[PLUGIN NAME].plugin <<EOL
{
"name": "[PLUGIN NAME]",
"description": "An [PLUGIN NAME] plugin",
"version": "0.0.1"
}
EOL

go run main.go
```

## Getting Help

We love to hear from you so if you have questions, comments or find a bug in the
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,8 +5,6 @@
package listen

import (
"fmt"

"github.com/spf13/cobra"

"github.com/deepgram-devs/deepgram-cli/cmd"
Expand All @@ -23,15 +21,6 @@ and usage of using your command. For example:
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,8 +5,11 @@
package cmd

import (
"log"
"os"
"strings"

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

Expand Down Expand Up @@ -35,6 +38,45 @@ func Execute() {
}

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.
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,8 +5,6 @@
package speak

import (
"fmt"

"github.com/spf13/cobra"

"github.com/deepgram-devs/deepgram-cli/cmd"
Expand All @@ -23,15 +21,6 @@ and usage of using your command. For example:
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
}
Loading

0 comments on commit afbeef5

Please sign in to comment.