feat(module): add CLI commands for marketplace module management
Wire `core module install/remove/list/update` commands to the marketplace.Installer from pkg/marketplace. Follows the exact pattern established by cmd/plugin/. - install: clone from Git repo with optional ed25519 verification - list: table output of installed modules - update: pull latest + re-verify manifest (supports --all) - remove: confirmation prompt then cleanup Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
61ecaa0c49
commit
41f786a222
6 changed files with 294 additions and 0 deletions
59
cmd/module/cmd.go
Normal file
59
cmd/module/cmd.go
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// Package module provides CLI commands for managing marketplace modules.
|
||||
//
|
||||
// Commands:
|
||||
// - install: Install a module from a Git repo
|
||||
// - list: List installed modules
|
||||
// - update: Update a module or all modules
|
||||
// - remove: Remove an installed module
|
||||
package module
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"forge.lthn.ai/core/go/pkg/cli"
|
||||
"forge.lthn.ai/core/go/pkg/i18n"
|
||||
"forge.lthn.ai/core/go/pkg/marketplace"
|
||||
"forge.lthn.ai/core/go/pkg/store"
|
||||
)
|
||||
|
||||
func init() {
|
||||
cli.RegisterCommands(AddModuleCommands)
|
||||
}
|
||||
|
||||
// AddModuleCommands registers the 'module' command and all subcommands.
|
||||
func AddModuleCommands(root *cli.Command) {
|
||||
moduleCmd := &cli.Command{
|
||||
Use: "module",
|
||||
Short: i18n.T("Manage marketplace modules"),
|
||||
}
|
||||
root.AddCommand(moduleCmd)
|
||||
|
||||
addInstallCommand(moduleCmd)
|
||||
addListCommand(moduleCmd)
|
||||
addUpdateCommand(moduleCmd)
|
||||
addRemoveCommand(moduleCmd)
|
||||
}
|
||||
|
||||
// moduleSetup returns the modules directory, store, and installer.
|
||||
// The caller must defer st.Close().
|
||||
func moduleSetup() (string, *store.Store, *marketplace.Installer, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", nil, nil, cli.Wrap(err, "failed to determine home directory")
|
||||
}
|
||||
|
||||
modulesDir := filepath.Join(home, ".core", "modules")
|
||||
if err := os.MkdirAll(modulesDir, 0755); err != nil {
|
||||
return "", nil, nil, cli.Wrap(err, "failed to create modules directory")
|
||||
}
|
||||
|
||||
dbPath := filepath.Join(modulesDir, "modules.db")
|
||||
st, err := store.New(dbPath)
|
||||
if err != nil {
|
||||
return "", nil, nil, cli.Wrap(err, "failed to open module store")
|
||||
}
|
||||
|
||||
inst := marketplace.NewInstaller(modulesDir, st)
|
||||
return modulesDir, st, inst, nil
|
||||
}
|
||||
59
cmd/module/cmd_install.go
Normal file
59
cmd/module/cmd_install.go
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
package module
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"forge.lthn.ai/core/go/pkg/cli"
|
||||
"forge.lthn.ai/core/go/pkg/i18n"
|
||||
"forge.lthn.ai/core/go/pkg/marketplace"
|
||||
)
|
||||
|
||||
var (
|
||||
installRepo string
|
||||
installSignKey string
|
||||
)
|
||||
|
||||
func addInstallCommand(parent *cli.Command) {
|
||||
installCmd := cli.NewCommand(
|
||||
"install <code>",
|
||||
i18n.T("Install a module from a Git repo"),
|
||||
i18n.T("Install a module by cloning its Git repository, verifying the manifest signature, and registering it.\n\nThe --repo flag is required and specifies the Git URL to clone from."),
|
||||
func(cmd *cli.Command, args []string) error {
|
||||
if installRepo == "" {
|
||||
return fmt.Errorf("--repo flag is required")
|
||||
}
|
||||
return runInstall(args[0], installRepo, installSignKey)
|
||||
},
|
||||
)
|
||||
installCmd.Args = cli.ExactArgs(1)
|
||||
installCmd.Example = " core module install my-module --repo https://forge.lthn.ai/modules/my-module.git\n core module install signed-mod --repo ssh://git@forge.lthn.ai:2223/modules/signed.git --sign-key abc123"
|
||||
|
||||
cli.StringFlag(installCmd, &installRepo, "repo", "r", "", i18n.T("Git repository URL to clone"))
|
||||
cli.StringFlag(installCmd, &installSignKey, "sign-key", "k", "", i18n.T("Hex-encoded ed25519 public key for manifest verification"))
|
||||
|
||||
parent.AddCommand(installCmd)
|
||||
}
|
||||
|
||||
func runInstall(code, repo, signKey string) error {
|
||||
_, st, inst, err := moduleSetup()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer st.Close()
|
||||
|
||||
cli.Dim("Installing module " + code + " from " + repo + "...")
|
||||
|
||||
mod := marketplace.Module{
|
||||
Code: code,
|
||||
Repo: repo,
|
||||
SignKey: signKey,
|
||||
}
|
||||
|
||||
if err := inst.Install(context.Background(), mod); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cli.Success("Module " + code + " installed successfully")
|
||||
return nil
|
||||
}
|
||||
51
cmd/module/cmd_list.go
Normal file
51
cmd/module/cmd_list.go
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package module
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"forge.lthn.ai/core/go/pkg/cli"
|
||||
"forge.lthn.ai/core/go/pkg/i18n"
|
||||
)
|
||||
|
||||
func addListCommand(parent *cli.Command) {
|
||||
listCmd := cli.NewCommand(
|
||||
"list",
|
||||
i18n.T("List installed modules"),
|
||||
"",
|
||||
func(cmd *cli.Command, args []string) error {
|
||||
return runList()
|
||||
},
|
||||
)
|
||||
|
||||
parent.AddCommand(listCmd)
|
||||
}
|
||||
|
||||
func runList() error {
|
||||
_, st, inst, err := moduleSetup()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer st.Close()
|
||||
|
||||
installed, err := inst.Installed()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(installed) == 0 {
|
||||
cli.Dim("No modules installed")
|
||||
return nil
|
||||
}
|
||||
|
||||
table := cli.NewTable("Code", "Name", "Version", "Repo")
|
||||
for _, m := range installed {
|
||||
table.AddRow(m.Code, m.Name, m.Version, m.Repo)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
table.Render()
|
||||
fmt.Println()
|
||||
cli.Dim(fmt.Sprintf("%d module(s) installed", len(installed)))
|
||||
|
||||
return nil
|
||||
}
|
||||
40
cmd/module/cmd_remove.go
Normal file
40
cmd/module/cmd_remove.go
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
package module
|
||||
|
||||
import (
|
||||
"forge.lthn.ai/core/go/pkg/cli"
|
||||
"forge.lthn.ai/core/go/pkg/i18n"
|
||||
)
|
||||
|
||||
func addRemoveCommand(parent *cli.Command) {
|
||||
removeCmd := cli.NewCommand(
|
||||
"remove <code>",
|
||||
i18n.T("Remove an installed module"),
|
||||
"",
|
||||
func(cmd *cli.Command, args []string) error {
|
||||
return runRemove(args[0])
|
||||
},
|
||||
)
|
||||
removeCmd.Args = cli.ExactArgs(1)
|
||||
|
||||
parent.AddCommand(removeCmd)
|
||||
}
|
||||
|
||||
func runRemove(code string) error {
|
||||
_, st, inst, err := moduleSetup()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer st.Close()
|
||||
|
||||
if !cli.Confirm("Remove module " + code + "?") {
|
||||
cli.Dim("Cancelled")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := inst.Remove(code); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cli.Success("Module " + code + " removed")
|
||||
return nil
|
||||
}
|
||||
84
cmd/module/cmd_update.go
Normal file
84
cmd/module/cmd_update.go
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
package module
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"forge.lthn.ai/core/go/pkg/cli"
|
||||
"forge.lthn.ai/core/go/pkg/i18n"
|
||||
)
|
||||
|
||||
var updateAll bool
|
||||
|
||||
func addUpdateCommand(parent *cli.Command) {
|
||||
updateCmd := cli.NewCommand(
|
||||
"update [code]",
|
||||
i18n.T("Update a module or all modules"),
|
||||
i18n.T("Update a specific module to the latest version, or use --all to update all installed modules."),
|
||||
func(cmd *cli.Command, args []string) error {
|
||||
if updateAll {
|
||||
return runUpdateAll()
|
||||
}
|
||||
if len(args) == 0 {
|
||||
return fmt.Errorf("module code required (or use --all)")
|
||||
}
|
||||
return runUpdate(args[0])
|
||||
},
|
||||
)
|
||||
|
||||
cli.BoolFlag(updateCmd, &updateAll, "all", "a", false, i18n.T("Update all installed modules"))
|
||||
|
||||
parent.AddCommand(updateCmd)
|
||||
}
|
||||
|
||||
func runUpdate(code string) error {
|
||||
_, st, inst, err := moduleSetup()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer st.Close()
|
||||
|
||||
cli.Dim("Updating " + code + "...")
|
||||
|
||||
if err := inst.Update(context.Background(), code); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cli.Success("Module " + code + " updated successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
func runUpdateAll() error {
|
||||
_, st, inst, err := moduleSetup()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer st.Close()
|
||||
|
||||
installed, err := inst.Installed()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(installed) == 0 {
|
||||
cli.Dim("No modules installed")
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
var updated, failed int
|
||||
for _, m := range installed {
|
||||
cli.Dim("Updating " + m.Code + "...")
|
||||
if err := inst.Update(ctx, m.Code); err != nil {
|
||||
cli.Errorf("Failed to update %s: %v", m.Code, err)
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
cli.Success(m.Code + " updated")
|
||||
updated++
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
cli.Dim(fmt.Sprintf("%d updated, %d failed", updated, failed))
|
||||
return nil
|
||||
}
|
||||
1
main.go
1
main.go
|
|
@ -20,6 +20,7 @@ import (
|
|||
_ "forge.lthn.ai/core/cli/cmd/lab"
|
||||
_ "forge.lthn.ai/core/cli/cmd/mcpcmd"
|
||||
_ "forge.lthn.ai/core/cli/cmd/ml"
|
||||
_ "forge.lthn.ai/core/cli/cmd/module"
|
||||
_ "forge.lthn.ai/core/cli/cmd/monitor"
|
||||
_ "forge.lthn.ai/core/cli/cmd/pkgcmd"
|
||||
_ "forge.lthn.ai/core/cli/cmd/plugin"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue