Skip to content
Draft
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ dist/
autocomplete.*
fctl
coverage.*
ledger-v3-poc
40 changes: 40 additions & 0 deletions cmd/plugin/install.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package plugin

import (
"fmt"

"github.com/pterm/pterm"
"github.com/spf13/cobra"

fctl "github.com/formancehq/fctl/v3/pkg"
pluginpkg "github.com/formancehq/fctl/v3/pkg/plugin"
)

const versionFlag = "version"

func NewInstallCommand() *cobra.Command {
return fctl.NewCommand("install",
fctl.WithShortDescription("Install a plugin from the registry"),
fctl.WithArgs(cobra.ExactArgs(1)),
fctl.WithStringFlag(versionFlag, "", "Specific version to install (defaults to latest)"),
fctl.WithRunE(runInstall),
)
}

func runInstall(cmd *cobra.Command, args []string) error {
name := args[0]
version := fctl.GetString(cmd, versionFlag)
configDir := fctl.GetString(cmd, fctl.ConfigDir)

registry := pluginpkg.NewRegistryClient(fctl.GetHttpClient(cmd))
pm := pluginpkg.NewPluginManager(configDir)

pterm.Info.Printfln("Installing plugin %s...", name)

if err := pm.InstallPlugin(name, version, registry); err != nil {
return fmt.Errorf("failed to install plugin %s: %w", name, err)
}

pterm.Success.Printfln("Plugin %s installed successfully", name)
return nil
}
44 changes: 44 additions & 0 deletions cmd/plugin/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package plugin

import (
"github.com/pterm/pterm"
"github.com/spf13/cobra"

fctl "github.com/formancehq/fctl/v3/pkg"
pluginpkg "github.com/formancehq/fctl/v3/pkg/plugin"
)

func NewListCommand() *cobra.Command {
return fctl.NewCommand("list",
fctl.WithAliases("ls"),
fctl.WithShortDescription("List installed plugins"),
fctl.WithArgs(cobra.ExactArgs(0)),
fctl.WithRunE(runList),
)
}

func runList(cmd *cobra.Command, args []string) error {
configDir := fctl.GetString(cmd, fctl.ConfigDir)

cfg, err := pluginpkg.LoadPluginsConfig(configDir)
if err != nil {
return err
}

if len(cfg.Plugins) == 0 {
pterm.Info.Println("No plugins installed")
pterm.Info.Println("Use 'fctl plugin install <name>' to install a plugin")
return nil
}

tableData := [][]string{{"Name", "Version", "Path"}}
for _, p := range cfg.Plugins {
tableData = append(tableData, []string{p.Name, p.Version, p.Path})
}

return pterm.DefaultTable.
WithHasHeader().
WithWriter(cmd.OutOrStdout()).
WithData(tableData).
Render()
}
32 changes: 32 additions & 0 deletions cmd/plugin/remove.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package plugin

import (
"github.com/pterm/pterm"
"github.com/spf13/cobra"

fctl "github.com/formancehq/fctl/v3/pkg"
pluginpkg "github.com/formancehq/fctl/v3/pkg/plugin"
)

func NewRemoveCommand() *cobra.Command {
return fctl.NewCommand("remove",
fctl.WithAliases("rm", "uninstall"),
fctl.WithShortDescription("Remove an installed plugin"),
fctl.WithArgs(cobra.ExactArgs(1)),
fctl.WithRunE(runRemove),
)
}

func runRemove(cmd *cobra.Command, args []string) error {
name := args[0]
configDir := fctl.GetString(cmd, fctl.ConfigDir)

pm := pluginpkg.NewPluginManager(configDir)

if err := pm.RemovePlugin(name); err != nil {
return err
}

pterm.Success.Printfln("Plugin %s removed", name)
return nil
}
19 changes: 19 additions & 0 deletions cmd/plugin/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package plugin

import (
"github.com/spf13/cobra"

fctl "github.com/formancehq/fctl/v3/pkg"
)

func NewCommand() *cobra.Command {
return fctl.NewCommand("plugin",
fctl.WithShortDescription("Manage fctl plugins"),
fctl.WithChildCommands(
NewInstallCommand(),
NewListCommand(),
NewUpdateCommand(),
NewRemoveCommand(),
),
)
}
57 changes: 57 additions & 0 deletions cmd/plugin/update.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package plugin

import (
"fmt"

"github.com/pterm/pterm"
"github.com/spf13/cobra"

fctl "github.com/formancehq/fctl/v3/pkg"
pluginpkg "github.com/formancehq/fctl/v3/pkg/plugin"
)

const allFlag = "all"

func NewUpdateCommand() *cobra.Command {
return fctl.NewCommand("update",
fctl.WithShortDescription("Update plugins to the latest version"),
fctl.WithArgs(cobra.MaximumNArgs(1)),
fctl.WithBoolFlag(allFlag, false, "Update all installed plugins"),
fctl.WithRunE(runUpdate),
)
}

func runUpdate(cmd *cobra.Command, args []string) error {
configDir := fctl.GetString(cmd, fctl.ConfigDir)
updateAll := fctl.GetBool(cmd, allFlag)

cfg, err := pluginpkg.LoadPluginsConfig(configDir)
if err != nil {
return err
}

registry := pluginpkg.NewRegistryClient(fctl.GetHttpClient(cmd))
pm := pluginpkg.NewPluginManager(configDir)

var toUpdate []string
if updateAll {
for _, p := range cfg.Plugins {
toUpdate = append(toUpdate, p.Name)
}
} else if len(args) > 0 {
toUpdate = []string{args[0]}
} else {
return fmt.Errorf("specify a plugin name or use --all to update all plugins")
}

for _, name := range toUpdate {
pterm.Info.Printfln("Updating plugin %s...", name)
if err := pm.InstallPlugin(name, "", registry); err != nil {
pterm.Error.Printfln("Failed to update plugin %s: %v", name, err)
continue
}
pterm.Success.Printfln("Plugin %s updated", name)
}

return nil
}
28 changes: 28 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/formancehq/fctl/v3/cmd/login"
"github.com/formancehq/fctl/v3/cmd/orchestration"
"github.com/formancehq/fctl/v3/cmd/payments"
plugincmd "github.com/formancehq/fctl/v3/cmd/plugin"
"github.com/formancehq/fctl/v3/cmd/profiles"
"github.com/formancehq/fctl/v3/cmd/reconciliation"
"github.com/formancehq/fctl/v3/cmd/search"
Expand All @@ -32,6 +33,7 @@ import (
"github.com/formancehq/fctl/v3/cmd/wallets"
"github.com/formancehq/fctl/v3/cmd/webhooks"
fctl "github.com/formancehq/fctl/v3/pkg"
pluginpkg "github.com/formancehq/fctl/v3/pkg/plugin"
)

func init() {
Expand Down Expand Up @@ -63,6 +65,7 @@ func NewRootCommand() *cobra.Command {
webhooks.NewCommand(),
wallets.NewCommand(),
orchestration.NewCommand(),
plugincmd.NewCommand(),
),
fctl.WithPersistentStringPFlag(fctl.ProfileFlag, "p", "", "Configuration profile to use"),
fctl.WithPersistentStringPFlag(fctl.ConfigDir, "c", fmt.Sprintf("%s/.config/formance/fctl", homedir), "Path to configuration dir"),
Expand Down Expand Up @@ -93,6 +96,15 @@ func NewRootCommand() *cobra.Command {
return cmd
}

func removeChildCommand(parent *cobra.Command, name string) {
for _, child := range parent.Commands() {
if child.Name() == name {
parent.RemoveCommand(child)
return
}
}
}

func Execute() {
defer func() {
if e := recover(); e != nil {
Expand All @@ -102,6 +114,22 @@ func Execute() {
}()
ctx, _ := signal.NotifyContext(context.TODO(), os.Interrupt)
cmd := NewRootCommand()

// Load plugins and override built-in commands
configDir := fmt.Sprintf("%s/.config/formance/fctl", os.Getenv("HOME"))
if dir := os.Getenv("FCTL_CONFIG_DIR"); dir != "" {
configDir = dir
}
pm := pluginpkg.NewPluginManager(configDir)
pm.DiscoverAndLoad(ctx)
defer pm.Shutdown()

for _, loaded := range pm.GetLoadedPlugins() {
pluginCmd := pluginpkg.BuildCobraCommand(loaded)
removeChildCommand(cmd, loaded.Name)
cmd.AddCommand(pluginCmd)
}

if err := cmd.ExecuteContext(ctx); err != nil {
switch {
case errors.Is(err, fctl.ErrMissingApproval):
Expand Down
Loading
Loading