From 41f786a222c1d585d80be9ea3a59e29c9432871c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 18 Feb 2026 10:36:50 +0000 Subject: [PATCH] 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 --- cmd/module/cmd.go | 59 +++++++++++++++++++++++++++ cmd/module/cmd_install.go | 59 +++++++++++++++++++++++++++ cmd/module/cmd_list.go | 51 ++++++++++++++++++++++++ cmd/module/cmd_remove.go | 40 +++++++++++++++++++ cmd/module/cmd_update.go | 84 +++++++++++++++++++++++++++++++++++++++ main.go | 1 + 6 files changed, 294 insertions(+) create mode 100644 cmd/module/cmd.go create mode 100644 cmd/module/cmd_install.go create mode 100644 cmd/module/cmd_list.go create mode 100644 cmd/module/cmd_remove.go create mode 100644 cmd/module/cmd_update.go diff --git a/cmd/module/cmd.go b/cmd/module/cmd.go new file mode 100644 index 00000000..3cf2b2e0 --- /dev/null +++ b/cmd/module/cmd.go @@ -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 +} diff --git a/cmd/module/cmd_install.go b/cmd/module/cmd_install.go new file mode 100644 index 00000000..b0fa9e37 --- /dev/null +++ b/cmd/module/cmd_install.go @@ -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 ", + 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 +} diff --git a/cmd/module/cmd_list.go b/cmd/module/cmd_list.go new file mode 100644 index 00000000..2b4fa5c3 --- /dev/null +++ b/cmd/module/cmd_list.go @@ -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 +} diff --git a/cmd/module/cmd_remove.go b/cmd/module/cmd_remove.go new file mode 100644 index 00000000..07b20993 --- /dev/null +++ b/cmd/module/cmd_remove.go @@ -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 ", + 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 +} diff --git a/cmd/module/cmd_update.go b/cmd/module/cmd_update.go new file mode 100644 index 00000000..86cd242c --- /dev/null +++ b/cmd/module/cmd_update.go @@ -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 +} diff --git a/main.go b/main.go index 9945cf8f..ef52ddfe 100644 --- a/main.go +++ b/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"