feat(module): add CLI commands for marketplace module management
Some checks are pending
Security Scan / Go Vulnerability Check (push) Waiting to run
Security Scan / Secret Detection (push) Waiting to run
Security Scan / Dependency & Config Scan (push) Waiting to run

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:
Claude 2026-02-18 10:36:50 +00:00
parent 61ecaa0c49
commit 41f786a222
No known key found for this signature in database
GPG key ID: AF404715446AEB41
6 changed files with 294 additions and 0 deletions

59
cmd/module/cmd.go Normal file
View 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
View 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
View 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
View 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
View 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
}

View file

@ -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"