diff --git a/README.md b/README.md index b04c182..8388dfe 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ To encrypt your configuration: comanda configure --encrypt ``` -You'll be prompted to enter and confirm an encryption password. Once encrypted, all commands that need to access the configuration (process, serve, configure) will prompt for the password. +You'll be prompted to enter and confirm an encryption password. Once encrypted, all commands that need to access the configuration (process, server, configure) will prompt for the password. Example workflow: ```bash @@ -231,17 +231,21 @@ openai: ### Server Configuration -COMandA can run as an HTTP server, allowing you to process chains of models and actions defined in YAML files via HTTP requests. To configure the server: +COMandA can run as an HTTP server, allowing you to process chains of models and actions defined in YAML files via HTTP requests. The server is managed using the `server` command: ```bash -comanda configure --server -``` +# Start the server +comanda server -This will prompt you to: -1. Set the server port (default: 8080) -2. Set the data directory path (default: data) -3. Generate a bearer token for authentication -4. Enable/disable authentication +# Configure server settings +comanda server configure # Interactive configuration +comanda server show # Show current configuration +comanda server port 8080 # Set server port +comanda server datadir ./data # Set data directory +comanda server auth on # Enable authentication +comanda server auth off # Disable authentication +comanda server newtoken # Generate new bearer token +``` The server configuration is stored in your `.env` file alongside provider and model settings: @@ -256,7 +260,7 @@ server: To start the server: ```bash -comanda serve +comanda server ``` The server provides the following endpoints: diff --git a/cmd/configure.go b/cmd/configure.go index 665bfc8..e81de75 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -21,7 +21,6 @@ import ( var ( listFlag bool - serverFlag bool encryptFlag bool decryptFlag bool removeFlag string @@ -198,55 +197,6 @@ func configureDatabase(reader *bufio.Reader, envConfig *config.EnvConfig) error return nil } -func configureServer(reader *bufio.Reader, envConfig *config.EnvConfig) error { - serverConfig := envConfig.GetServerConfig() - - // Prompt for port - fmt.Printf("Enter server port (default: %d): ", serverConfig.Port) - portStr, _ := reader.ReadString('\n') - portStr = strings.TrimSpace(portStr) - if portStr != "" { - port, err := strconv.Atoi(portStr) - if err != nil { - return fmt.Errorf("invalid port number: %v", err) - } - serverConfig.Port = port - } - - // Prompt for data directory - fmt.Printf("Enter data directory path (default: %s): ", serverConfig.DataDir) - dataDir, _ := reader.ReadString('\n') - dataDir = strings.TrimSpace(dataDir) - if dataDir != "" { - serverConfig.DataDir = dataDir - } - - // Create data directory if it doesn't exist - if err := os.MkdirAll(serverConfig.DataDir, 0755); err != nil { - return fmt.Errorf("error creating data directory: %v", err) - } - - // Prompt for bearer token generation - fmt.Print("Generate new bearer token? (y/n): ") - genToken, _ := reader.ReadString('\n') - if strings.TrimSpace(strings.ToLower(genToken)) == "y" { - token, err := config.GenerateBearerToken() - if err != nil { - return fmt.Errorf("error generating bearer token: %v", err) - } - serverConfig.BearerToken = token - fmt.Printf("Generated bearer token: %s\n", token) - } - - // Prompt for server enable/disable - fmt.Print("Enable server authentication? (y/n): ") - enableStr, _ := reader.ReadString('\n') - serverConfig.Enabled = strings.TrimSpace(strings.ToLower(enableStr)) == "y" - - envConfig.UpdateServerConfig(*serverConfig) - return nil -} - func removeModel(envConfig *config.EnvConfig, modelName string) error { removed := false for providerName, provider := range envConfig.Providers { @@ -527,12 +477,6 @@ var configureCmd = &cobra.Command{ fmt.Printf("Error: %v\n", err) return } - } else if serverFlag { - reader := bufio.NewReader(os.Stdin) - if err := configureServer(reader, envConfig); err != nil { - fmt.Printf("Error configuring server: %v\n", err) - return - } } else if databaseFlag { reader := bufio.NewReader(os.Stdin) if err := configureDatabase(reader, envConfig); err != nil { @@ -775,7 +719,6 @@ func listConfiguration() { func init() { configureCmd.Flags().BoolVar(&listFlag, "list", false, "List all configured providers and models") - configureCmd.Flags().BoolVar(&serverFlag, "server", false, "Configure server settings") configureCmd.Flags().BoolVar(&encryptFlag, "encrypt", false, "Encrypt the configuration file") configureCmd.Flags().BoolVar(&decryptFlag, "decrypt", false, "Decrypt the configuration file") configureCmd.Flags().StringVar(&removeFlag, "remove", "", "Remove a model by name") diff --git a/cmd/serve.go b/cmd/serve.go deleted file mode 100644 index 3889c0b..0000000 --- a/cmd/serve.go +++ /dev/null @@ -1,31 +0,0 @@ -package cmd - -import ( - "log" - - "github.com/kris-hansen/comanda/utils/config" - "github.com/kris-hansen/comanda/utils/server" - "github.com/spf13/cobra" -) - -var serveCmd = &cobra.Command{ - Use: "serve", - Short: "Start HTTP server for processing YAML files", - Long: `Start an HTTP server that processes YAML DSL configuration files via HTTP requests.`, - Run: func(cmd *cobra.Command, args []string) { - // Load environment configuration - envConfig, err := config.LoadEnvConfigWithPassword(config.GetEnvPath()) - if err != nil { - log.Fatalf("Error loading environment configuration: %v", err) - } - - // Start the server - if err := server.Run(envConfig); err != nil { - log.Fatalf("Server error: %v", err) - } - }, -} - -func init() { - rootCmd.AddCommand(serveCmd) -} diff --git a/cmd/server.go b/cmd/server.go new file mode 100644 index 0000000..d1507d7 --- /dev/null +++ b/cmd/server.go @@ -0,0 +1,318 @@ +package cmd + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/kris-hansen/comanda/utils/config" + "github.com/kris-hansen/comanda/utils/server" + "github.com/spf13/cobra" +) + +var serverCmd = &cobra.Command{ + Use: "server", + Short: "Start and manage the HTTP server", + Long: `Start the HTTP server for processing YAML files, or manage server configuration`, + Run: func(cmd *cobra.Command, args []string) { + // Default behavior (no subcommand) is to start the server + configPath := config.GetEnvPath() + envConfig, err := config.LoadEnvConfigWithPassword(configPath) + if err != nil { + fmt.Printf("Error loading configuration: %v\n", err) + return + } + + if err := server.Run(envConfig); err != nil { + fmt.Printf("Server failed to start: %v\n", err) + return + } + }, +} + +var configureServerCmd = &cobra.Command{ + Use: "configure", + Short: "Configure server settings", + Long: `Configure server settings including port, data directory, and authentication`, + Run: func(cmd *cobra.Command, args []string) { + configPath := config.GetEnvPath() + envConfig, err := config.LoadEnvConfigWithPassword(configPath) + if err != nil { + fmt.Printf("Error loading configuration: %v\n", err) + return + } + + reader := bufio.NewReader(os.Stdin) + if err := configureServer(reader, envConfig); err != nil { + fmt.Printf("Error configuring server: %v\n", err) + return + } + + if err := config.SaveEnvConfig(configPath, envConfig); err != nil { + fmt.Printf("Error saving configuration: %v\n", err) + return + } + + fmt.Printf("Server configuration saved successfully to %s!\n", configPath) + }, +} + +var showServerCmd = &cobra.Command{ + Use: "show", + Short: "Show current server configuration", + Long: `Display the current server configuration settings`, + Run: func(cmd *cobra.Command, args []string) { + configPath := config.GetEnvPath() + envConfig, err := config.LoadEnvConfigWithPassword(configPath) + if err != nil { + fmt.Printf("Error loading configuration: %v\n", err) + return + } + + server := envConfig.GetServerConfig() + fmt.Println("\nServer Configuration:") + fmt.Printf("Port: %d\n", server.Port) + fmt.Printf("Data Directory: %s\n", server.DataDir) + fmt.Printf("Authentication Enabled: %v\n", server.Enabled) + if server.BearerToken != "" { + fmt.Printf("Bearer Token: %s\n", server.BearerToken) + } + fmt.Println() + }, +} + +var updatePortCmd = &cobra.Command{ + Use: "port [port]", + Short: "Update server port", + Long: `Update the port number that the server listens on`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + port, err := strconv.Atoi(args[0]) + if err != nil { + fmt.Printf("Error: Invalid port number: %v\n", err) + return + } + + configPath := config.GetEnvPath() + envConfig, err := config.LoadEnvConfigWithPassword(configPath) + if err != nil { + fmt.Printf("Error loading configuration: %v\n", err) + return + } + + serverConfig := envConfig.GetServerConfig() + serverConfig.Port = port + envConfig.UpdateServerConfig(*serverConfig) + + if err := config.SaveEnvConfig(configPath, envConfig); err != nil { + fmt.Printf("Error saving configuration: %v\n", err) + return + } + + fmt.Printf("Server port updated to %d\n", port) + }, +} + +var updateDataDirCmd = &cobra.Command{ + Use: "datadir [path]", + Short: "Update data directory", + Long: `Update the directory path where server data is stored`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + dataDir := args[0] + + // Clean and resolve the path + absPath, err := filepath.Abs(dataDir) + if err != nil { + fmt.Printf("Error: Invalid directory path: %v\n", err) + return + } + + // Check if path is valid for the OS + if !filepath.IsAbs(absPath) { + fmt.Printf("Error: Path must be absolute: %s\n", absPath) + return + } + + // Check if parent directory exists and is accessible + parentDir := filepath.Dir(absPath) + if _, err := os.Stat(parentDir); err != nil { + if os.IsNotExist(err) { + fmt.Printf("Error: Parent directory does not exist: %s\n", parentDir) + } else { + fmt.Printf("Error: Cannot access parent directory: %v\n", err) + } + return + } + + // Try to create the directory to verify write permissions + if err := os.MkdirAll(absPath, 0755); err != nil { + fmt.Printf("Error: Cannot create directory (check permissions): %v\n", err) + return + } + + // Verify the directory is writable by creating a test file + testFile := filepath.Join(absPath, ".write_test") + if err := os.WriteFile(testFile, []byte(""), 0644); err != nil { + fmt.Printf("Error: Directory is not writable: %v\n", err) + return + } + os.Remove(testFile) // Clean up test file + + configPath := config.GetEnvPath() + envConfig, err := config.LoadEnvConfigWithPassword(configPath) + if err != nil { + fmt.Printf("Error loading configuration: %v\n", err) + return + } + + serverConfig := envConfig.GetServerConfig() + serverConfig.DataDir = absPath + envConfig.UpdateServerConfig(*serverConfig) + + if err := config.SaveEnvConfig(configPath, envConfig); err != nil { + fmt.Printf("Error saving configuration: %v\n", err) + return + } + + fmt.Printf("Data directory updated to %s\n", absPath) + }, +} + +var toggleAuthCmd = &cobra.Command{ + Use: "auth [on|off]", + Short: "Toggle authentication", + Long: `Enable or disable server authentication`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + enable := strings.ToLower(args[0]) + if enable != "on" && enable != "off" { + fmt.Println("Error: Please specify either 'on' or 'off'") + return + } + + configPath := config.GetEnvPath() + envConfig, err := config.LoadEnvConfigWithPassword(configPath) + if err != nil { + fmt.Printf("Error loading configuration: %v\n", err) + return + } + + serverConfig := envConfig.GetServerConfig() + serverConfig.Enabled = enable == "on" + + // Generate new bearer token if enabling auth + if serverConfig.Enabled && serverConfig.BearerToken == "" { + token, err := config.GenerateBearerToken() + if err != nil { + fmt.Printf("Error generating bearer token: %v\n", err) + return + } + serverConfig.BearerToken = token + fmt.Printf("Generated new bearer token: %s\n", token) + } + + envConfig.UpdateServerConfig(*serverConfig) + + if err := config.SaveEnvConfig(configPath, envConfig); err != nil { + fmt.Printf("Error saving configuration: %v\n", err) + return + } + + fmt.Printf("Server authentication %s\n", map[bool]string{true: "enabled", false: "disabled"}[serverConfig.Enabled]) + }, +} + +var newTokenCmd = &cobra.Command{ + Use: "newtoken", + Short: "Generate new bearer token", + Long: `Generate a new bearer token for server authentication`, + Run: func(cmd *cobra.Command, args []string) { + configPath := config.GetEnvPath() + envConfig, err := config.LoadEnvConfigWithPassword(configPath) + if err != nil { + fmt.Printf("Error loading configuration: %v\n", err) + return + } + + serverConfig := envConfig.GetServerConfig() + token, err := config.GenerateBearerToken() + if err != nil { + fmt.Printf("Error generating bearer token: %v\n", err) + return + } + + serverConfig.BearerToken = token + envConfig.UpdateServerConfig(*serverConfig) + + if err := config.SaveEnvConfig(configPath, envConfig); err != nil { + fmt.Printf("Error saving configuration: %v\n", err) + return + } + + fmt.Printf("Generated new bearer token: %s\n", token) + }, +} + +// configureServer handles the interactive server configuration +func configureServer(reader *bufio.Reader, envConfig *config.EnvConfig) error { + serverConfig := envConfig.GetServerConfig() + + // Prompt for port + fmt.Printf("Enter server port (default: %d): ", serverConfig.Port) + portStr, _ := reader.ReadString('\n') + portStr = strings.TrimSpace(portStr) + if portStr != "" { + port, err := strconv.Atoi(portStr) + if err != nil { + return fmt.Errorf("invalid port number: %v", err) + } + serverConfig.Port = port + } + + // Prompt for data directory + fmt.Printf("Enter data directory path (default: %s): ", serverConfig.DataDir) + dataDir, _ := reader.ReadString('\n') + dataDir = strings.TrimSpace(dataDir) + if dataDir != "" { + serverConfig.DataDir = dataDir + } + + // Create data directory if it doesn't exist + if err := os.MkdirAll(serverConfig.DataDir, 0755); err != nil { + return fmt.Errorf("error creating data directory: %v", err) + } + + // Prompt for bearer token generation + fmt.Print("Generate new bearer token? (y/n): ") + genToken, _ := reader.ReadString('\n') + if strings.TrimSpace(strings.ToLower(genToken)) == "y" { + token, err := config.GenerateBearerToken() + if err != nil { + return fmt.Errorf("error generating bearer token: %v", err) + } + serverConfig.BearerToken = token + fmt.Printf("Generated bearer token: %s\n", token) + } + + // Prompt for server enable/disable + fmt.Print("Enable server authentication? (y/n): ") + enableStr, _ := reader.ReadString('\n') + serverConfig.Enabled = strings.TrimSpace(strings.ToLower(enableStr)) == "y" + + envConfig.UpdateServerConfig(*serverConfig) + return nil +} + +func init() { + serverCmd.AddCommand(configureServerCmd) + serverCmd.AddCommand(showServerCmd) + serverCmd.AddCommand(updatePortCmd) + serverCmd.AddCommand(updateDataDirCmd) + serverCmd.AddCommand(toggleAuthCmd) + serverCmd.AddCommand(newTokenCmd) + rootCmd.AddCommand(serverCmd) +}