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:
ZanzyTHEbar 2026-02-22 15:30:45 +00:00
parent 3ead775a10
commit 3286555321
20 changed files with 1405 additions and 2013 deletions

View 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
}

View 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())
},
}
}

View 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)
}
}

View 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)
},
}
}

View 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())
},
}
}

View 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
}

View 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())
},
}
}

View 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
}

View 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())
},
}
}

View 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)
}

View 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)
}
}

View 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])
},
}
}

View 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])
},
}
}

View 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())
},
}
}

View 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())
},
}
}

View 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 &copy
}
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
}

View 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
}
}

View 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...)
}

View 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