Add Github wehbook management

Change-Id: I4b7a23a77838f345d65adf51877fab5978bd6055
diff --git a/server/cmd/commands/webhook.go b/server/cmd/commands/webhook.go
new file mode 100644
index 0000000..e62d1ce
--- /dev/null
+++ b/server/cmd/commands/webhook.go
@@ -0,0 +1,251 @@
+package commands
+
+import (
+	"bufio"
+	"context"
+	"crypto/rand"
+	"encoding/hex"
+	"fmt"
+	"net/url"
+	"os"
+	"strings"
+
+	"github.com/iomodo/staff/git"
+	"github.com/spf13/cobra"
+	"gopkg.in/yaml.v3"
+)
+
+var webhookCmd = &cobra.Command{
+	Use:   "webhook",
+	Short: "Webhook management commands",
+	Long: `Manage GitHub webhooks for the Staff AI system.
+
+Webhooks are used to automatically detect when Pull Requests are merged
+and complete corresponding tasks in the Staff system.`,
+}
+
+var webhookConfigureCmd = &cobra.Command{
+	Use:   "configure",
+	Short: "Configure GitHub webhook for PR merge events",
+	Long: `Configure a GitHub webhook to automatically detect when Pull Requests are merged.
+
+This command will:
+1. Generate a webhook secret if not already configured
+2. Register a webhook on GitHub for pull_request events
+3. Update the config.yaml file with the webhook secret
+
+Examples:
+  staff webhook configure                                    # Interactive setup
+  staff webhook configure --webhook-url https://example.com/webhooks/github
+  staff webhook configure --dry-run                         # Show what will be done
+  staff webhook configure --force                           # Force re-registration`,
+	RunE: runWebhookConfigure,
+}
+
+var (
+	webhookURL string
+	dryRun     bool
+	force      bool
+)
+
+func init() {
+	webhookConfigureCmd.Flags().StringVar(&webhookURL, "webhook-url", "", "Webhook URL (if not provided, will prompt)")
+	webhookConfigureCmd.Flags().BoolVar(&dryRun, "dry-run", false, "Show what would be done without executing")
+	webhookConfigureCmd.Flags().BoolVar(&force, "force", false, "Force re-registration even if webhook exists")
+	
+	webhookCmd.AddCommand(webhookConfigureCmd)
+	rootCmd.AddCommand(webhookCmd)
+}
+
+func runWebhookConfigure(cmd *cobra.Command, args []string) error {
+	ctx := context.Background()
+	
+	// Validate GitHub configuration
+	if !cfg.HasGitHubConfig() {
+		return fmt.Errorf("GitHub configuration is required. Please configure github.token, github.owner, and github.repo in config.yaml")
+	}
+
+	// Get webhook URL if not provided
+	if webhookURL == "" {
+		var err error
+		webhookURL, err = promptWebhookURL()
+		if err != nil {
+			return fmt.Errorf("failed to get webhook URL: %w", err)
+		}
+	}
+
+	// Validate webhook URL
+	if err := validateWebhookURL(webhookURL); err != nil {
+		return fmt.Errorf("invalid webhook URL: %w", err)
+	}
+
+	// Generate webhook secret if not configured
+	secret := cfg.GitHub.WebhookSecret
+	if secret == "" {
+		var err error
+		secret, err = generateWebhookSecret()
+		if err != nil {
+			return fmt.Errorf("failed to generate webhook secret: %w", err)
+		}
+		fmt.Printf("Generated new webhook secret: %s\n", secret)
+	} else {
+		fmt.Printf("Using existing webhook secret: %s\n", secret)
+	}
+
+	if dryRun {
+		fmt.Printf("\nDry run mode - showing what would be done:\n")
+		fmt.Printf("- Webhook URL: %s\n", webhookURL)
+		fmt.Printf("- Events: [pull_request]\n")
+		fmt.Printf("- Secret: %s\n", secret)
+		fmt.Printf("- Repository: %s/%s\n", cfg.GitHub.Owner, cfg.GitHub.Repo)
+		if cfg.GitHub.WebhookSecret == "" {
+			fmt.Printf("- Would update config.yaml with webhook secret\n")
+		}
+		fmt.Printf("- Would register webhook on GitHub\n")
+		return nil
+	}
+
+	// Check existing webhooks
+	fmt.Printf("Checking existing webhooks...\n")
+	webhooks, err := gitInterface.ListWebhooks(ctx)
+	if err != nil {
+		return fmt.Errorf("failed to list existing webhooks: %w", err)
+	}
+
+	// Look for existing webhook with our URL
+	var existingWebhook *git.GitHubWebhookResponse
+	for i, webhook := range webhooks {
+		if webhook.Config.URL == webhookURL && webhook.Name == "web" {
+			existingWebhook = &webhooks[i]
+			break
+		}
+	}
+
+	if existingWebhook != nil {
+		if !force {
+			return fmt.Errorf("webhook already exists with URL %s (ID: %d). Use --force to update it", webhookURL, existingWebhook.ID)
+		}
+		
+		fmt.Printf("Updating existing webhook (ID: %d)...\n", existingWebhook.ID)
+		updatedWebhook, err := gitInterface.UpdateWebhook(ctx, existingWebhook.ID, webhookURL, secret)
+		if err != nil {
+			return fmt.Errorf("failed to update webhook: %w", err)
+		}
+		
+		fmt.Printf("Successfully updated webhook!\n")
+		fmt.Printf("- Webhook ID: %d\n", updatedWebhook.ID)
+		fmt.Printf("- URL: %s\n", updatedWebhook.Config.URL)
+		fmt.Printf("- Events: %v\n", updatedWebhook.Events)
+		fmt.Printf("- Secret: %s\n", secret)
+	} else {
+		fmt.Printf("Creating new webhook...\n")
+		newWebhook, err := gitInterface.CreateWebhook(ctx, webhookURL, secret)
+		if err != nil {
+			return fmt.Errorf("failed to create webhook: %w", err)
+		}
+		
+		fmt.Printf("Successfully created webhook!\n")
+		fmt.Printf("- Webhook ID: %d\n", newWebhook.ID)
+		fmt.Printf("- URL: %s\n", newWebhook.Config.URL)
+		fmt.Printf("- Events: %v\n", newWebhook.Events)
+		fmt.Printf("- Secret: %s\n", secret)
+	}
+
+	// Update config file if webhook secret changed
+	if cfg.GitHub.WebhookSecret != secret {
+		fmt.Printf("Updating config.yaml with webhook secret...\n")
+		if err := updateConfigWebhookSecret(secret); err != nil {
+			return fmt.Errorf("failed to update config file: %w", err)
+		}
+		fmt.Printf("Config file updated successfully!\n")
+	}
+
+	fmt.Printf("\nWebhook configuration complete!\n")
+	fmt.Printf("\nWebhook Secret: %s\n", secret)
+	fmt.Printf("Please save this secret for your webhook endpoint configuration.\n")
+	fmt.Printf("\nYour Staff system will now automatically detect merged PRs and complete tasks.\n")
+	
+	return nil
+}
+
+func promptWebhookURL() (string, error) {
+	reader := bufio.NewReader(os.Stdin)
+	
+	// Show server listen address if configured
+	if cfg.Server.ListenAddress != "" {
+		fmt.Printf("Detected server listen address: %s\n", cfg.Server.ListenAddress)
+		fmt.Printf("Suggested webhook URL: http://%s/webhooks/github\n", cfg.Server.ListenAddress)
+		fmt.Printf("(Note: Use https:// and a public domain for production)\n")
+	}
+	
+	fmt.Printf("Enter webhook URL (e.g., https://your-domain.com/webhooks/github): ")
+	url, err := reader.ReadString('\n')
+	if err != nil {
+		return "", err
+	}
+	
+	return strings.TrimSpace(url), nil
+}
+
+func validateWebhookURL(webhookURL string) error {
+	parsedURL, err := url.Parse(webhookURL)
+	if err != nil {
+		return fmt.Errorf("invalid URL format: %w", err)
+	}
+	
+	if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
+		return fmt.Errorf("URL must use http or https scheme")
+	}
+	
+	if parsedURL.Host == "" {
+		return fmt.Errorf("URL must include a host")
+	}
+	
+	return nil
+}
+
+func generateWebhookSecret() (string, error) {
+	// Generate 32 bytes of random data
+	bytes := make([]byte, 32)
+	if _, err := rand.Read(bytes); err != nil {
+		return "", fmt.Errorf("failed to generate random bytes: %w", err)
+	}
+	
+	// Encode as hex string
+	return hex.EncodeToString(bytes), nil
+}
+
+func updateConfigWebhookSecret(secret string) error {
+	// Read current config file
+	configData, err := os.ReadFile("config.yaml")
+	if err != nil {
+		return fmt.Errorf("failed to read config file: %w", err)
+	}
+	
+	// Parse YAML
+	var configMap map[string]interface{}
+	if err := yaml.Unmarshal(configData, &configMap); err != nil {
+		return fmt.Errorf("failed to parse config YAML: %w", err)
+	}
+	
+	// Update webhook secret
+	github, ok := configMap["github"].(map[string]interface{})
+	if !ok {
+		github = make(map[string]interface{})
+		configMap["github"] = github
+	}
+	github["webhook_secret"] = secret
+	
+	// Marshal back to YAML
+	updatedData, err := yaml.Marshal(configMap)
+	if err != nil {
+		return fmt.Errorf("failed to marshal config YAML: %w", err)
+	}
+	
+	// Write back to file
+	if err := os.WriteFile("config.yaml", updatedData, 0644); err != nil {
+		return fmt.Errorf("failed to write config file: %w", err)
+	}
+	
+	return nil
+}
\ No newline at end of file