refactor(cmd): extract dragonscale CLI to cobra-based internal structure
Slim main.go from ~2000 lines to ~46 lines. All subcommands (agent, gateway, daemon, auth, memory, migrate, skills, cron, secret, onboard, status, version) moved to cmd/dragonscale/internal/cli/commands/ with cobra. - cmd/dragonscale/internal/cli/: AppContext, registry, palette, root - cmd/dragonscale/internal/cli/commands/: agent, auth, cron, daemon, gateway, memory, migrate, onboard, root, secret, skills, status, version - main.go: embeds workspace, wires sdk.Service, builds root via commands.BuildRoot
This commit is contained in:
parent
3ead775a10
commit
3286555321
20 changed files with 1405 additions and 2013 deletions
53
cmd/dragonscale/internal/cli/commands/agent.go
Normal file
53
cmd/dragonscale/internal/cli/commands/agent.go
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/ZanzyTHEbar/dragonscale/cmd/dragonscale/internal/cli"
|
||||||
|
"github.com/ZanzyTHEbar/dragonscale/pkg/dragonscale/sdk"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerAgentCommand() {
|
||||||
|
cli.Register(func(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return buildAgentCommand(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildAgentCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "agent",
|
||||||
|
Short: "Interact with the agent",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
if ctx == nil || ctx.Service == nil {
|
||||||
|
return errors.New("service is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
message, err := cmd.Flags().GetString("message")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
session, err := cmd.Flags().GetString("session")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
debug, err := cmd.Flags().GetBool("debug")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Service.Agent(cmd.Context(), ctx.In, cmd.OutOrStdout(), sdk.AgentOptions{
|
||||||
|
Message: message,
|
||||||
|
SessionKey: session,
|
||||||
|
Debug: debug,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().BoolP("debug", "d", false, "Enable debug logging")
|
||||||
|
cmd.Flags().StringP("message", "m", "", "Send a single message and exit")
|
||||||
|
cmd.Flags().StringP("session", "s", "cli:default", "Session key for conversation context")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
99
cmd/dragonscale/internal/cli/commands/auth.go
Normal file
99
cmd/dragonscale/internal/cli/commands/auth.go
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/ZanzyTHEbar/dragonscale/cmd/dragonscale/internal/cli"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerAuthCommand() {
|
||||||
|
cli.Register(func(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return buildAuthCommand(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildAuthCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "auth",
|
||||||
|
Short: "Manage authentication",
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
return cmd.Help()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.AddCommand(buildAuthLoginCommand(ctx))
|
||||||
|
cmd.AddCommand(buildAuthLogoutCommand(ctx))
|
||||||
|
cmd.AddCommand(buildAuthStatusCommand(ctx))
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildAuthLoginCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "login",
|
||||||
|
Short: "Authenticate with a provider",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
if ctx == nil || ctx.Service == nil {
|
||||||
|
return errors.New("service is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
provider, err := cmd.Flags().GetString("provider")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if provider == "" {
|
||||||
|
return errors.New("provider is required")
|
||||||
|
}
|
||||||
|
useDeviceCode, err := cmd.Flags().GetBool("device-code")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Service.AuthLogin(cmd.Context(), ctx.In, cmd.OutOrStdout(), provider, useDeviceCode)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringP("provider", "p", "", "Provider to authenticate")
|
||||||
|
cmd.Flags().Bool("device-code", false, "Use device code flow")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildAuthLogoutCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "logout",
|
||||||
|
Short: "Clear authentication credentials",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
if ctx == nil || ctx.Service == nil {
|
||||||
|
return errors.New("service is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
provider, err := cmd.Flags().GetString("provider")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Service.AuthLogout(cmd.Context(), cmd.OutOrStdout(), provider)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringP("provider", "p", "", "Provider to logout (optional)")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildAuthStatusCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "status",
|
||||||
|
Short: "Show authentication status",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
if ctx == nil || ctx.Service == nil {
|
||||||
|
return errors.New("service is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Service.AuthStatus(cmd.Context(), cmd.OutOrStdout())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
131
cmd/dragonscale/internal/cli/commands/command_handlers_test.go
Normal file
131
cmd/dragonscale/internal/cli/commands/command_handlers_test.go
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseCronOptions(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("builds options from CLI args", func(t *testing.T) {
|
||||||
|
opts, err := parseCronOptions("nightly", "250", "0 0 * * *")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if opts.Name != "nightly" {
|
||||||
|
t.Fatalf("expected name nightly, got %q", opts.Name)
|
||||||
|
}
|
||||||
|
if opts.EveryMS == nil || *opts.EveryMS != 250 {
|
||||||
|
t.Fatalf("expected everyMS=250, got %#v", opts.EveryMS)
|
||||||
|
}
|
||||||
|
if opts.Cron != "0 0 * * *" {
|
||||||
|
t.Fatalf("expected cron expr, got %q", opts.Cron)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns error for invalid every value", func(t *testing.T) {
|
||||||
|
if _, err := parseCronOptions("bad", "not-a-number", ""); err == nil {
|
||||||
|
t.Fatal("expected parse error for invalid every value")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns default options for empty inputs", func(t *testing.T) {
|
||||||
|
opts, err := parseCronOptions("", "", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if opts.Name != "" || opts.Cron != "" || opts.EveryMS != nil {
|
||||||
|
t.Fatalf("expected empty defaults, got %#v", opts)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCronListCommandRequiresService(t *testing.T) {
|
||||||
|
cmd := buildCronListCommand(nil)
|
||||||
|
cmd.SetOut(&bytes.Buffer{})
|
||||||
|
cmd.SetErr(&bytes.Buffer{})
|
||||||
|
cmd.SilenceUsage = true
|
||||||
|
|
||||||
|
err := cmd.Execute()
|
||||||
|
if err == nil || err.Error() != "service is not initialized" {
|
||||||
|
t.Fatalf("unexpected error for cron list: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCronAddCommandRequiresService(t *testing.T) {
|
||||||
|
cmd := buildCronAddCommand(nil)
|
||||||
|
cmd.SetOut(&bytes.Buffer{})
|
||||||
|
cmd.SetErr(&bytes.Buffer{})
|
||||||
|
cmd.SilenceUsage = true
|
||||||
|
|
||||||
|
// verify high-risk flag surface is wired before execution
|
||||||
|
for _, name := range []string{"name", "message", "every", "cron", "deliver", "to", "channel"} {
|
||||||
|
if cmd.Flags().Lookup(name) == nil {
|
||||||
|
t.Fatalf("missing cron flag: %s", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Execute()
|
||||||
|
if err == nil || err.Error() != "service is not initialized" {
|
||||||
|
t.Fatalf("unexpected error for cron add: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSkillsListCommandRequiresService(t *testing.T) {
|
||||||
|
cmd := buildSkillsListCommand(nil)
|
||||||
|
cmd.SetOut(&bytes.Buffer{})
|
||||||
|
cmd.SetErr(&bytes.Buffer{})
|
||||||
|
cmd.SilenceUsage = true
|
||||||
|
|
||||||
|
err := cmd.Execute()
|
||||||
|
if err == nil || err.Error() != "service is not initialized" {
|
||||||
|
t.Fatalf("unexpected error for skills list: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatusCommandRequiresService(t *testing.T) {
|
||||||
|
cmd := buildStatusCommand(nil)
|
||||||
|
cmd.SetOut(&bytes.Buffer{})
|
||||||
|
cmd.SetErr(&bytes.Buffer{})
|
||||||
|
cmd.SilenceUsage = true
|
||||||
|
|
||||||
|
err := cmd.Execute()
|
||||||
|
if err == nil || err.Error() != "service is not initialized" {
|
||||||
|
t.Fatalf("unexpected error for status: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDaemonStatusCommandRequiresService(t *testing.T) {
|
||||||
|
cmd := buildDaemonStatusCommand(nil)
|
||||||
|
cmd.SetOut(&bytes.Buffer{})
|
||||||
|
cmd.SetErr(&bytes.Buffer{})
|
||||||
|
cmd.SilenceUsage = true
|
||||||
|
|
||||||
|
err := cmd.Execute()
|
||||||
|
if err == nil || err.Error() != "service is not initialized" {
|
||||||
|
t.Fatalf("unexpected error for daemon status: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGatewayCommandHasDebugFlag(t *testing.T) {
|
||||||
|
cmd := buildGatewayCommand(nil)
|
||||||
|
cmd.SetOut(&bytes.Buffer{})
|
||||||
|
cmd.SetErr(&bytes.Buffer{})
|
||||||
|
|
||||||
|
if cmd.Flags().Lookup("debug") == nil {
|
||||||
|
t.Fatalf("missing gateway debug flag")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGatewayCommandRequiresService(t *testing.T) {
|
||||||
|
cmd := buildGatewayCommand(nil)
|
||||||
|
cmd.SetOut(&bytes.Buffer{})
|
||||||
|
cmd.SetErr(&bytes.Buffer{})
|
||||||
|
cmd.SilenceUsage = true
|
||||||
|
|
||||||
|
err := cmd.Execute()
|
||||||
|
if err == nil || err.Error() != "service is not initialized" {
|
||||||
|
t.Fatalf("unexpected error for gateway: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
175
cmd/dragonscale/internal/cli/commands/cron.go
Normal file
175
cmd/dragonscale/internal/cli/commands/cron.go
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/ZanzyTHEbar/dragonscale/cmd/dragonscale/internal/cli"
|
||||||
|
"github.com/ZanzyTHEbar/dragonscale/pkg/dragonscale/sdk"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerCronCommand() {
|
||||||
|
cli.Register(func(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return buildCronCommand(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildCronCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "cron",
|
||||||
|
Short: "Manage cron jobs",
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
return cmd.Help()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.AddCommand(buildCronListCommand(ctx))
|
||||||
|
cmd.AddCommand(buildCronAddCommand(ctx))
|
||||||
|
cmd.AddCommand(buildCronRemoveCommand(ctx))
|
||||||
|
cmd.AddCommand(buildCronEnableCommand(ctx))
|
||||||
|
cmd.AddCommand(buildCronDisableCommand(ctx))
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildCronListCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "List configured cron jobs",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
if ctx == nil || ctx.Service == nil {
|
||||||
|
return errors.New("service is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Service.CronList(cmd.Context(), cmd.OutOrStdout())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildCronAddCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "add",
|
||||||
|
Short: "Add a cron job",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
if ctx == nil || ctx.Service == nil {
|
||||||
|
return errors.New("service is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
name, err := cmd.Flags().GetString("name")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
message, err := cmd.Flags().GetString("message")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
every, err := cmd.Flags().GetString("every")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cronExpr, err := cmd.Flags().GetString("cron")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
deliver, err := cmd.Flags().GetBool("deliver")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
to, err := cmd.Flags().GetString("to")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
channel, err := cmd.Flags().GetString("channel")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
opts, err := parseCronOptions(name, every, cronExpr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.Message = message
|
||||||
|
opts.Deliver = deliver
|
||||||
|
opts.To = to
|
||||||
|
opts.Channel = channel
|
||||||
|
|
||||||
|
return ctx.Service.CronAdd(cmd.Context(), cmd.OutOrStdout(), opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringP("name", "n", "", "Unique job name")
|
||||||
|
cmd.Flags().StringP("message", "m", "", "Message prompt for job execution")
|
||||||
|
cmd.Flags().StringP("every", "e", "", "Run every N milliseconds")
|
||||||
|
cmd.Flags().StringP("cron", "c", "", "Cron expression schedule")
|
||||||
|
cmd.Flags().BoolP("deliver", "d", false, "Deliver result to recipient")
|
||||||
|
cmd.Flags().String("to", "", "Delivery recipient")
|
||||||
|
cmd.Flags().String("channel", "", "Delivery channel")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCronOptions(name, every, cronExpr string) (sdk.CronAddOptions, error) {
|
||||||
|
opts := sdk.CronAddOptions{}
|
||||||
|
if name != "" {
|
||||||
|
opts.Name = name
|
||||||
|
}
|
||||||
|
if every != "" {
|
||||||
|
val, err := strconv.ParseInt(every, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return opts, err
|
||||||
|
}
|
||||||
|
opts.EveryMS = &val
|
||||||
|
}
|
||||||
|
if cronExpr != "" {
|
||||||
|
opts.Cron = cronExpr
|
||||||
|
}
|
||||||
|
return opts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildCronRemoveCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "remove [job-id]",
|
||||||
|
Short: "Remove a cron job",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if ctx == nil || ctx.Service == nil {
|
||||||
|
return errors.New("service is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Service.CronRemove(cmd.Context(), cmd.OutOrStdout(), args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildCronEnableCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "enable [job-id]",
|
||||||
|
Short: "Enable a cron job",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if ctx == nil || ctx.Service == nil {
|
||||||
|
return errors.New("service is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Service.CronEnable(cmd.Context(), cmd.OutOrStdout(), args[0], true)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildCronDisableCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "disable [job-id]",
|
||||||
|
Short: "Disable a cron job",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if ctx == nil || ctx.Service == nil {
|
||||||
|
return errors.New("service is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Service.CronEnable(cmd.Context(), cmd.OutOrStdout(), args[0], false)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
75
cmd/dragonscale/internal/cli/commands/daemon.go
Normal file
75
cmd/dragonscale/internal/cli/commands/daemon.go
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/ZanzyTHEbar/dragonscale/cmd/dragonscale/internal/cli"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerDaemonCommand() {
|
||||||
|
cli.Register(func(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return buildDaemonCommand(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildDaemonCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "daemon",
|
||||||
|
Short: "Manage dragonscale daemon",
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
return cmd.Help()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.AddCommand(buildDaemonStartCommand(ctx))
|
||||||
|
cmd.AddCommand(buildDaemonStopCommand(ctx))
|
||||||
|
cmd.AddCommand(buildDaemonStatusCommand(ctx))
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildDaemonStartCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "start",
|
||||||
|
Short: "Start daemon process",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
if ctx == nil || ctx.Service == nil {
|
||||||
|
return errors.New("service is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Service.DaemonStart(cmd.Context(), cmd.OutOrStdout())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildDaemonStopCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "stop",
|
||||||
|
Short: "Stop daemon process",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
if ctx == nil || ctx.Service == nil {
|
||||||
|
return errors.New("service is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Service.DaemonStop(cmd.Context(), cmd.OutOrStdout())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildDaemonStatusCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "status",
|
||||||
|
Short: "Show daemon status",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
if ctx == nil || ctx.Service == nil {
|
||||||
|
return errors.New("service is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Service.DaemonStatus(cmd.Context(), cmd.OutOrStdout())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
40
cmd/dragonscale/internal/cli/commands/gateway.go
Normal file
40
cmd/dragonscale/internal/cli/commands/gateway.go
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/ZanzyTHEbar/dragonscale/cmd/dragonscale/internal/cli"
|
||||||
|
"github.com/ZanzyTHEbar/dragonscale/pkg/dragonscale/sdk"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerGatewayCommand() {
|
||||||
|
cli.Register(func(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return buildGatewayCommand(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildGatewayCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "gateway",
|
||||||
|
Short: "Start dragonscale gateway",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
if ctx == nil || ctx.Service == nil {
|
||||||
|
return errors.New("service is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
debug, err := cmd.Flags().GetBool("debug")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Service.Gateway(cmd.Context(), cmd.OutOrStdout(), sdk.GatewayOptions{
|
||||||
|
Debug: debug,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().BoolP("debug", "d", false, "Enable debug logging")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
59
cmd/dragonscale/internal/cli/commands/memory.go
Normal file
59
cmd/dragonscale/internal/cli/commands/memory.go
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/ZanzyTHEbar/dragonscale/cmd/dragonscale/internal/cli"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerMemoryCommand() {
|
||||||
|
cli.Register(func(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return buildMemoryCommand(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildMemoryCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "memory",
|
||||||
|
Short: "Manage memory system",
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
return cmd.Help()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.AddCommand(buildMemoryMigrateSessionsCommand(ctx))
|
||||||
|
cmd.AddCommand(buildMemoryDBStatusCommand(ctx))
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildMemoryMigrateSessionsCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "migrate-sessions",
|
||||||
|
Short: "Migrate sessions into memory DB",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
if ctx == nil || ctx.Service == nil {
|
||||||
|
return errors.New("service is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Service.MemoryMigrateSessions(cmd.Context(), cmd.OutOrStdout())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildMemoryDBStatusCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "db-status",
|
||||||
|
Short: "Show memory DB status",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
if ctx == nil || ctx.Service == nil {
|
||||||
|
return errors.New("service is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Service.MemoryDBStatus(cmd.Context(), cmd.OutOrStdout())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
79
cmd/dragonscale/internal/cli/commands/migrate.go
Normal file
79
cmd/dragonscale/internal/cli/commands/migrate.go
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/ZanzyTHEbar/dragonscale/cmd/dragonscale/internal/cli"
|
||||||
|
"github.com/ZanzyTHEbar/dragonscale/pkg/dragonscale/sdk"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerMigrateCommand() {
|
||||||
|
cli.Register(func(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return buildMigrateCommand(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildMigrateCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "migrate",
|
||||||
|
Short: "Migrate dragonscale configuration and workspace",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
if ctx == nil || ctx.Service == nil {
|
||||||
|
return errors.New("service is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
dryRun, err := cmd.Flags().GetBool("dry-run")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
configOnly, err := cmd.Flags().GetBool("config-only")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
workspaceOnly, err := cmd.Flags().GetBool("workspace-only")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
force, err := cmd.Flags().GetBool("force")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
refresh, err := cmd.Flags().GetBool("refresh")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
openClawHome, err := cmd.Flags().GetString("openclaw-home")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dragonscaleHome, err := cmd.Flags().GetString("dragonscale-home")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := sdk.MigrateOptions{
|
||||||
|
DryRun: dryRun,
|
||||||
|
ConfigOnly: configOnly,
|
||||||
|
WorkspaceOnly: workspaceOnly,
|
||||||
|
Force: force,
|
||||||
|
Refresh: refresh,
|
||||||
|
OpenClawHome: openClawHome,
|
||||||
|
DragonscaleHome: dragonscaleHome,
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Service.Migrate(cmd.Context(), opts, cmd.OutOrStdout())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().Bool("dry-run", false, "Simulate migration without writing")
|
||||||
|
cmd.Flags().Bool("config-only", false, "Migrate only config files")
|
||||||
|
cmd.Flags().Bool("workspace-only", false, "Migrate only workspace files")
|
||||||
|
cmd.Flags().Bool("force", false, "Overwrite existing files")
|
||||||
|
cmd.Flags().Bool("refresh", false, "Re-run migrations")
|
||||||
|
cmd.Flags().String("openclaw-home", "", "Path to OpenClaw home directory")
|
||||||
|
cmd.Flags().String("dragonscale-home", "", "Path to dragonscale home directory")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
29
cmd/dragonscale/internal/cli/commands/onboard.go
Normal file
29
cmd/dragonscale/internal/cli/commands/onboard.go
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/ZanzyTHEbar/dragonscale/cmd/dragonscale/internal/cli"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerOnboardCommand() {
|
||||||
|
cli.Register(func(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return buildOnboardCommand(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildOnboardCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "onboard",
|
||||||
|
Short: "Initialize dragonscale configuration and workspace",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
if ctx == nil || ctx.Service == nil {
|
||||||
|
return errors.New("service is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Service.Onboard(cmd.Context(), ctx.In, cmd.OutOrStdout())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
29
cmd/dragonscale/internal/cli/commands/root.go
Normal file
29
cmd/dragonscale/internal/cli/commands/root.go
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ZanzyTHEbar/dragonscale/cmd/dragonscale/internal/cli"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterAll wires command factories into the global palette.
|
||||||
|
func RegisterAll() {
|
||||||
|
// Command factories are intentionally registered by their feature modules.
|
||||||
|
registerAgentCommand()
|
||||||
|
registerGatewayCommand()
|
||||||
|
registerMigrateCommand()
|
||||||
|
registerAuthCommand()
|
||||||
|
registerCronCommand()
|
||||||
|
registerSkillsCommand()
|
||||||
|
registerSecretCommand()
|
||||||
|
registerDaemonCommand()
|
||||||
|
registerMemoryCommand()
|
||||||
|
registerOnboardCommand()
|
||||||
|
registerStatusCommand()
|
||||||
|
registerVersionCommand()
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildRoot returns the composed root command with registered command factories.
|
||||||
|
func BuildRoot(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
RegisterAll()
|
||||||
|
return cli.BuildRoot(ctx)
|
||||||
|
}
|
||||||
130
cmd/dragonscale/internal/cli/commands/root_test.go
Normal file
130
cmd/dragonscale/internal/cli/commands/root_test.go
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ZanzyTHEbar/dragonscale/cmd/dragonscale/internal/cli"
|
||||||
|
"github.com/ZanzyTHEbar/dragonscale/pkg/dragonscale/sdk"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPaletteCommandsRejectsDuplicateCommandNames(t *testing.T) {
|
||||||
|
p := cli.Palette{
|
||||||
|
func(*cli.AppContext) *cobra.Command {
|
||||||
|
return &cobra.Command{Use: "agent"}
|
||||||
|
},
|
||||||
|
func(*cli.AppContext) *cobra.Command {
|
||||||
|
return &cobra.Command{Use: "agent"}
|
||||||
|
},
|
||||||
|
func(*cli.AppContext) *cobra.Command {
|
||||||
|
return &cobra.Command{Use: "daemon"}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := cli.NewAppContext(sdk.NewService(sdk.WithVersion("test", "", "", "")), "test", "", "", "")
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r == nil {
|
||||||
|
t.Fatal("expected duplicate command registration to panic")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
_ = p.Commands(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPaletteCommandsAllowsUniqueCommandNames(t *testing.T) {
|
||||||
|
p := cli.Palette{
|
||||||
|
func(*cli.AppContext) *cobra.Command {
|
||||||
|
return &cobra.Command{Use: "agent"}
|
||||||
|
},
|
||||||
|
func(*cli.AppContext) *cobra.Command {
|
||||||
|
return &cobra.Command{Use: "daemon"}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := cli.NewAppContext(sdk.NewService(sdk.WithVersion("test", "", "", "")), "test", "", "", "")
|
||||||
|
cmds := p.Commands(ctx)
|
||||||
|
if len(cmds) != 2 {
|
||||||
|
t.Fatalf("expected 2 unique commands, got %d", len(cmds))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildRoot_RegistersExpectedCommands(t *testing.T) {
|
||||||
|
originalPalette := cli.DefaultPalette
|
||||||
|
cli.DefaultPalette = nil
|
||||||
|
t.Cleanup(func() {
|
||||||
|
cli.DefaultPalette = originalPalette
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := cli.NewAppContext(sdk.NewService(sdk.WithVersion("test", "", "", "")), "test", "", "", "")
|
||||||
|
out := &bytes.Buffer{}
|
||||||
|
ctx = ctx.WithIO(bytes.NewBuffer(nil), out, out)
|
||||||
|
|
||||||
|
root := BuildRoot(ctx)
|
||||||
|
if got, want := root.Use, "dragonscale"; got != want {
|
||||||
|
t.Fatalf("root command use = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
found := map[string]bool{}
|
||||||
|
for _, cmd := range root.Commands() {
|
||||||
|
found[cmd.Name()] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := []string{
|
||||||
|
"agent",
|
||||||
|
"gateway",
|
||||||
|
"migrate",
|
||||||
|
"auth",
|
||||||
|
"cron",
|
||||||
|
"skills",
|
||||||
|
"secret",
|
||||||
|
"daemon",
|
||||||
|
"memory",
|
||||||
|
"onboard",
|
||||||
|
"status",
|
||||||
|
"version",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range expected {
|
||||||
|
if !found[name] {
|
||||||
|
t.Fatalf("missing expected command: %s", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(found) != len(expected) {
|
||||||
|
t.Fatalf("expected %d registered commands, got %d", len(expected), len(found))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVersionCommandWritesVersionInfo(t *testing.T) {
|
||||||
|
svc := sdk.NewService(sdk.WithVersion("v9.9.9", "", "", ""))
|
||||||
|
ctx := cli.NewAppContext(svc, "v9.9.9", "", "", "")
|
||||||
|
out := &bytes.Buffer{}
|
||||||
|
ctx = ctx.WithIO(bytes.NewBuffer(nil), out, out)
|
||||||
|
|
||||||
|
cmd := buildVersionCommand(ctx)
|
||||||
|
cmd.SetArgs([]string{})
|
||||||
|
cmd.SetOut(out)
|
||||||
|
|
||||||
|
if err := cmd.Execute(); err != nil {
|
||||||
|
t.Fatalf("version command returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := out.String()
|
||||||
|
if !strings.Contains(got, "dragonscale v9.9.9") {
|
||||||
|
t.Fatalf("unexpected version output: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVersionCommandRequiresService(t *testing.T) {
|
||||||
|
cmd := buildVersionCommand(nil)
|
||||||
|
cmd.SetOut(&bytes.Buffer{})
|
||||||
|
err := cmd.Execute()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error when service context is nil")
|
||||||
|
}
|
||||||
|
if err.Error() != "service is not initialized" {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
91
cmd/dragonscale/internal/cli/commands/secret.go
Normal file
91
cmd/dragonscale/internal/cli/commands/secret.go
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/ZanzyTHEbar/dragonscale/cmd/dragonscale/internal/cli"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerSecretCommand() {
|
||||||
|
cli.Register(func(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return buildSecretCommand(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSecretCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "secret",
|
||||||
|
Short: "Manage encrypted secrets",
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
return cmd.Help()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.AddCommand(buildSecretInitCommand(ctx))
|
||||||
|
cmd.AddCommand(buildSecretAddCommand(ctx))
|
||||||
|
cmd.AddCommand(buildSecretListCommand(ctx))
|
||||||
|
cmd.AddCommand(buildSecretDeleteCommand(ctx))
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSecretInitCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "init",
|
||||||
|
Short: "Initialize secret store",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
if ctx == nil || ctx.Service == nil {
|
||||||
|
return errors.New("service is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Service.SecretInit(cmd.Context(), cmd.OutOrStdout())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSecretAddCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "add <name>",
|
||||||
|
Short: "Add a secret",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if ctx == nil || ctx.Service == nil {
|
||||||
|
return errors.New("service is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Service.SecretAdd(cmd.Context(), ctx.In, cmd.OutOrStdout(), args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSecretListCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "List secrets",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
if ctx == nil || ctx.Service == nil {
|
||||||
|
return errors.New("service is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Service.SecretList(cmd.Context(), cmd.OutOrStdout())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSecretDeleteCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "delete <name>",
|
||||||
|
Short: "Delete a secret",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if ctx == nil || ctx.Service == nil {
|
||||||
|
return errors.New("service is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Service.SecretDelete(cmd.Context(), cmd.OutOrStdout(), args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
140
cmd/dragonscale/internal/cli/commands/skills.go
Normal file
140
cmd/dragonscale/internal/cli/commands/skills.go
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/ZanzyTHEbar/dragonscale/cmd/dragonscale/internal/cli"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerSkillsCommand() {
|
||||||
|
cli.Register(func(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return buildSkillsCommand(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSkillsCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "skills",
|
||||||
|
Short: "Manage skills",
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
return cmd.Help()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.AddCommand(buildSkillsListCommand(ctx))
|
||||||
|
cmd.AddCommand(buildSkillsInstallCommand(ctx))
|
||||||
|
cmd.AddCommand(buildSkillsRemoveCommand(ctx))
|
||||||
|
cmd.AddCommand(buildSkillsInstallBuiltinCommand(ctx))
|
||||||
|
cmd.AddCommand(buildSkillsListBuiltinCommand(ctx))
|
||||||
|
cmd.AddCommand(buildSkillsSearchCommand(ctx))
|
||||||
|
cmd.AddCommand(buildSkillsShowCommand(ctx))
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSkillsListCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "List installed skills",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
if ctx == nil || ctx.Service == nil {
|
||||||
|
return errors.New("service is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Service.SkillsList(cmd.Context(), cmd.OutOrStdout())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSkillsInstallCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "install <repo>",
|
||||||
|
Short: "Install a skill from GitHub",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if ctx == nil || ctx.Service == nil {
|
||||||
|
return errors.New("service is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Service.SkillsInstall(cmd.Context(), cmd.OutOrStdout(), args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSkillsRemoveCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "remove <name>",
|
||||||
|
Aliases: []string{"uninstall"},
|
||||||
|
Short: "Uninstall a skill",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if ctx == nil || ctx.Service == nil {
|
||||||
|
return errors.New("service is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Service.SkillsRemove(cmd.Context(), cmd.OutOrStdout(), args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSkillsInstallBuiltinCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "install-builtin",
|
||||||
|
Short: "Install built-in skills",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
if ctx == nil || ctx.Service == nil {
|
||||||
|
return errors.New("service is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Service.SkillsInstallBuiltin(cmd.Context(), cmd.OutOrStdout(), "")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSkillsListBuiltinCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "list-builtin",
|
||||||
|
Short: "List built-in skills",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
if ctx == nil || ctx.Service == nil {
|
||||||
|
return errors.New("service is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Service.SkillsListBuiltin(cmd.Context(), cmd.OutOrStdout())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSkillsSearchCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "search",
|
||||||
|
Short: "Search available skills",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
if ctx == nil || ctx.Service == nil {
|
||||||
|
return errors.New("service is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Service.SkillsSearch(cmd.Context(), cmd.OutOrStdout())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSkillsShowCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "show <name>",
|
||||||
|
Short: "Show skill details",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if ctx == nil || ctx.Service == nil {
|
||||||
|
return errors.New("service is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Service.SkillsShow(cmd.Context(), cmd.OutOrStdout(), args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
28
cmd/dragonscale/internal/cli/commands/status.go
Normal file
28
cmd/dragonscale/internal/cli/commands/status.go
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/ZanzyTHEbar/dragonscale/cmd/dragonscale/internal/cli"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerStatusCommand() {
|
||||||
|
cli.Register(func(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return buildStatusCommand(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildStatusCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "status",
|
||||||
|
Short: "Show dragonscale status",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
if ctx == nil || ctx.Service == nil {
|
||||||
|
return errors.New("service is not initialized")
|
||||||
|
}
|
||||||
|
return ctx.Service.Status(cmd.Context(), cmd.OutOrStdout())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
28
cmd/dragonscale/internal/cli/commands/version.go
Normal file
28
cmd/dragonscale/internal/cli/commands/version.go
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/ZanzyTHEbar/dragonscale/cmd/dragonscale/internal/cli"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerVersionCommand() {
|
||||||
|
cli.Register(func(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return buildVersionCommand(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildVersionCommand(ctx *cli.AppContext) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "version",
|
||||||
|
Short: "Show dragonscale version information",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
if ctx == nil || ctx.Service == nil {
|
||||||
|
return errors.New("service is not initialized")
|
||||||
|
}
|
||||||
|
return ctx.Service.PrintVersion(cmd.Context(), cmd.OutOrStdout())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
85
cmd/dragonscale/internal/cli/context.go
Normal file
85
cmd/dragonscale/internal/cli/context.go
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/ZanzyTHEbar/dragonscale/pkg/dragonscale/sdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AppContext carries shared dependencies for CLI command construction.
|
||||||
|
type AppContext struct {
|
||||||
|
Service *sdk.Service
|
||||||
|
|
||||||
|
In io.Reader
|
||||||
|
Out io.Writer
|
||||||
|
ErrOut io.Writer
|
||||||
|
|
||||||
|
Version string
|
||||||
|
GitCommit string
|
||||||
|
BuildTime string
|
||||||
|
GoVersion string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAppContext creates a CLI context with resilient defaults.
|
||||||
|
func NewAppContext(service *sdk.Service, version, gitCommit, buildTime, goVersion string) *AppContext {
|
||||||
|
if service == nil {
|
||||||
|
service = sdk.NewService(sdk.WithVersion(version, gitCommit, buildTime, goVersion))
|
||||||
|
} else {
|
||||||
|
if version != "" {
|
||||||
|
service.Version = version
|
||||||
|
}
|
||||||
|
if gitCommit != "" {
|
||||||
|
service.GitCommit = gitCommit
|
||||||
|
}
|
||||||
|
if buildTime != "" {
|
||||||
|
service.BuildTime = buildTime
|
||||||
|
}
|
||||||
|
if goVersion != "" {
|
||||||
|
service.GoVersion = goVersion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &AppContext{
|
||||||
|
Service: service,
|
||||||
|
In: os.Stdin,
|
||||||
|
Out: os.Stdout,
|
||||||
|
ErrOut: os.Stderr,
|
||||||
|
Version: service.Version,
|
||||||
|
GitCommit: service.GitCommit,
|
||||||
|
BuildTime: service.BuildTime,
|
||||||
|
GoVersion: service.GoVersion,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithIO returns a copy with explicit streams.
|
||||||
|
func (c *AppContext) WithIO(in io.Reader, out io.Writer, errOut io.Writer) *AppContext {
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
copy := *c
|
||||||
|
if in != nil {
|
||||||
|
copy.In = in
|
||||||
|
}
|
||||||
|
if out != nil {
|
||||||
|
copy.Out = out
|
||||||
|
}
|
||||||
|
if errOut != nil {
|
||||||
|
copy.ErrOut = errOut
|
||||||
|
}
|
||||||
|
return ©
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AppContext) stdout() io.Writer {
|
||||||
|
if c == nil || c.Out == nil {
|
||||||
|
return os.Stdout
|
||||||
|
}
|
||||||
|
return c.Out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AppContext) stderr() io.Writer {
|
||||||
|
if c == nil || c.ErrOut == nil {
|
||||||
|
return os.Stderr
|
||||||
|
}
|
||||||
|
return c.ErrOut
|
||||||
|
}
|
||||||
50
cmd/dragonscale/internal/cli/palette.go
Normal file
50
cmd/dragonscale/internal/cli/palette.go
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CommandFactory builds a cobra command using the provided AppContext.
|
||||||
|
type CommandFactory func(*AppContext) *cobra.Command
|
||||||
|
|
||||||
|
// Palette is an ordered collection of CommandFactory instances that compose the command surface.
|
||||||
|
type Palette []CommandFactory
|
||||||
|
|
||||||
|
// Register appends one or more command factories to the palette.
|
||||||
|
func (p *Palette) Register(factories ...CommandFactory) {
|
||||||
|
*p = append(*p, factories...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commands materializes command factories into concrete Cobra commands.
|
||||||
|
func (p Palette) Commands(ctx *AppContext) []*cobra.Command {
|
||||||
|
cmds := make([]*cobra.Command, 0, len(p))
|
||||||
|
seen := map[string]struct{}{}
|
||||||
|
for _, f := range p {
|
||||||
|
if f == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cmd := f(ctx)
|
||||||
|
if cmd == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := cmd.Name()
|
||||||
|
if name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := seen[name]; exists {
|
||||||
|
panic(fmt.Sprintf("duplicate command registration: %q", name))
|
||||||
|
}
|
||||||
|
seen[name] = struct{}{}
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
}
|
||||||
|
return cmds
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromCommand lifts an already-built command into a factory.
|
||||||
|
func FromCommand(c *cobra.Command) CommandFactory {
|
||||||
|
return func(_ *AppContext) *cobra.Command {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
9
cmd/dragonscale/internal/cli/registry.go
Normal file
9
cmd/dragonscale/internal/cli/registry.go
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
// DefaultPalette is the global command registry.
|
||||||
|
var DefaultPalette Palette
|
||||||
|
|
||||||
|
// Register registers command factories into the global palette.
|
||||||
|
func Register(factories ...CommandFactory) {
|
||||||
|
DefaultPalette.Register(factories...)
|
||||||
|
}
|
||||||
60
cmd/dragonscale/internal/cli/root.go
Normal file
60
cmd/dragonscale/internal/cli/root.go
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewRoot creates a root command and composes commands from the palette.
|
||||||
|
func NewRoot(ctx *AppContext, p Palette) *cobra.Command {
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = NewAppContext(nil, "", "", "", "")
|
||||||
|
}
|
||||||
|
if p == nil {
|
||||||
|
p = Palette{}
|
||||||
|
}
|
||||||
|
|
||||||
|
root := &cobra.Command{
|
||||||
|
Use: "dragonscale",
|
||||||
|
Short: "Personal AI Assistant",
|
||||||
|
Long: "dragonscale is a CLI for interacting with your local DragonScale agent and services.",
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
return cmd.Help()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
root.Version = ctx.Version
|
||||||
|
root.SetContext(context.Background())
|
||||||
|
root.AddCommand(p.Commands(ctx)...)
|
||||||
|
root.SetIn(ctx.In)
|
||||||
|
root.SetOut(ctx.stdout())
|
||||||
|
root.SetErr(ctx.stderr())
|
||||||
|
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildRoot builds using the default palette.
|
||||||
|
func BuildRoot(ctx *AppContext) *cobra.Command {
|
||||||
|
return NewRoot(ctx, DefaultPalette)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute builds and executes the root command.
|
||||||
|
func Execute(ctx *AppContext) error {
|
||||||
|
root := BuildRoot(ctx)
|
||||||
|
if err := root.Execute(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteOrExit executes the root command and exits with status 1 on error.
|
||||||
|
func ExecuteOrExit(ctx *AppContext) {
|
||||||
|
if err := Execute(ctx); err != nil {
|
||||||
|
out := ctx.stderr()
|
||||||
|
_, _ = fmt.Fprintln(out, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue