package cmd import ( "fmt" "os" "path/filepath" "git.jnss.me/joakim/opal/internal/engine" "git.jnss.me/joakim/opal/internal/wizard" "github.com/spf13/cobra" ) var ( showSystemdFlag bool showEnvFlag bool quickFlag bool profileFlag string ) var setupCmd = &cobra.Command{ Use: "setup", Short: "Interactive setup wizard", Long: `Interactive setup wizard to configure Opal for first-time use. This wizard will guide you through: - Choosing a deployment profile (personal/server) - Configuring storage directories - Setting basic preferences - Optional server configuration (OAuth/JWT) - Optional sync setup Examples: opal setup # Run interactive wizard opal setup --quick # Quick setup with defaults opal setup --profile=server # Use server profile opal setup --show-systemd # Show systemd service template opal setup --show-env # Show environment file template`, PersistentPreRun: func(cmd *cobra.Command, args []string) { // Skip normal initialization - setup command handles its own init }, Run: runSetup, } func init() { rootCmd.AddCommand(setupCmd) setupCmd.Flags().BoolVar(&showSystemdFlag, "show-systemd", false, "Show systemd service template") setupCmd.Flags().BoolVar(&showEnvFlag, "show-env", false, "Show environment file template") setupCmd.Flags().BoolVar(&quickFlag, "quick", false, "Quick setup with defaults") setupCmd.Flags().StringVar(&profileFlag, "profile", "", "Profile to use (personal/server/custom)") } func runSetup(cmd *cobra.Command, args []string) { // Apply directory overrides from parent persistent flags if cmd.Parent() != nil { if configDir, _ := cmd.Parent().PersistentFlags().GetString("config-dir"); configDir != "" { engine.SetConfigDirOverride(configDir) } if dataDir, _ := cmd.Parent().PersistentFlags().GetString("data-dir"); dataDir != "" { engine.SetDataDirOverride(dataDir) } } // Handle template flags if showSystemdFlag { showSystemdTemplate() return } if showEnvFlag { showEnvTemplate() return } // Run the wizard if quickFlag { runQuickSetup() } else { runInteractiveSetup() } } func showSystemdTemplate() { template := wizard.SystemdServiceTemplate("/etc/opal", "/var/lib/opal") fmt.Println(template) fmt.Println("\nTo install:") fmt.Println(" sudo nano /etc/systemd/system/opal.service") fmt.Println(" # Paste the above content") fmt.Println(" sudo systemctl daemon-reload") fmt.Println(" sudo systemctl enable opal") fmt.Println(" sudo systemctl start opal") } func showEnvTemplate() { template := wizard.EnvFileTemplate( "https://auth.example.com/application/o/opal/", "your_client_id", "your_client_secret", "https://opal.example.com/auth/callback", "generate_with_openssl_rand_hex_32", ) fmt.Println(template) fmt.Println("\nTo use:") fmt.Println(" sudo nano /etc/opal/opal.env") fmt.Println(" # Paste and customize the above content") } func runQuickSetup() { wizard.PrintHeader("Opal Quick Setup") fmt.Println("Creating default configuration...") fmt.Println() // Get actual directories (respecting overrides) configDir, err := engine.GetConfigDir() if err != nil { wizard.PrintError(fmt.Sprintf("Failed to get config directory: %v", err)) os.Exit(1) } dataDir, err := engine.GetDataDir() if err != nil { wizard.PrintError(fmt.Sprintf("Failed to get data directory: %v", err)) os.Exit(1) } // Create config cfg := engine.Config{ DefaultFilter: "status:pending", DefaultSort: "due,priority", DefaultReport: "list", ColorOutput: true, WeekStartDay: "monday", DefaultDueTime: "", NextLimit: 5, SyncEnabled: false, SyncStrategy: "last-write-wins", SyncQueueOffline: true, UrgencyDue: 12.0, UrgencyPriorityH: 6.0, UrgencyPriorityM: 3.9, UrgencyPriorityD: 1.8, UrgencyPriorityL: 0.0, UrgencyActive: 4.0, UrgencyAge: 2.0, UrgencyAgeMax: 365, UrgencyTags: 1.0, UrgencyProject: 1.0, UrgencyWaiting: -3.0, UrgencyUrgentTag: "next", UrgencyUrgentCoeff: 15.0, } // Create directories if err := os.MkdirAll(configDir, 0755); err != nil { wizard.PrintError(fmt.Sprintf("Failed to create config directory: %v", err)) os.Exit(1) } if err := os.MkdirAll(dataDir, 0755); err != nil { wizard.PrintError(fmt.Sprintf("Failed to create data directory: %v", err)) os.Exit(1) } // Save config if err := engine.SaveConfig(&cfg); err != nil { wizard.PrintError(fmt.Sprintf("Failed to save config: %v", err)) os.Exit(1) } wizard.PrintSuccess("Configuration created!") fmt.Printf("\nConfig: %s\n", configDir) fmt.Printf("Data: %s\n", dataDir) fmt.Println("\nYou're all set! Try: opal add 'My first task'") } func runInteractiveSetup() { // Welcome fmt.Println() fmt.Println("═══════════════════════════════════════") fmt.Println(" Welcome to Opal Task Manager!") fmt.Println("═══════════════════════════════════════") fmt.Println() // Step 1: Profile selection wizard.PrintHeader("Step 1: Choose Setup Profile") profiles := wizard.GetProfiles() choices := wizard.GetProfileChoices() defaultProfile := 0 if profileFlag == "server" { defaultProfile = 1 } else if profileFlag == "custom" { defaultProfile = 2 } profileIndex := wizard.PromptChoice("Select your setup profile:", choices, defaultProfile) profile := profiles[profileIndex] fmt.Println() wizard.PrintInfo(fmt.Sprintf("Selected: %s", profile.Name)) // Step 2: Directory configuration wizard.PrintHeader("Step 2: Storage Directories") configDir := profile.ConfigDir dataDir := profile.DataDir // Show default directories first if profile.Name != "Custom" { fmt.Printf("Config: %s\n", configDir) fmt.Printf("Data: %s\n", dataDir) fmt.Println() } if profile.Name == "Custom" || wizard.Confirm("Customize directory paths?") { configDir = wizard.PromptString("Config directory", configDir) dataDir = wizard.PromptString("Data directory", dataDir) } // Step 3: Server configuration (if server profile) var envFilePath string if profile.IsServer { wizard.PrintHeader("Step 3: Server Configuration") if wizard.PromptBool("Configure OAuth/JWT settings now?", true) { oauthIssuer := wizard.PromptString("OAuth Issuer URL", "https://auth.example.com/application/o/opal/") oauthClientID := wizard.PromptString("OAuth Client ID", "") oauthClientSecret := wizard.PromptPassword("OAuth Client Secret") oauthRedirectURI := wizard.PromptString("OAuth Redirect URI", "https://opal.example.com/auth/callback") jwtSecret := wizard.PromptOrGenerate("JWT Secret", func() (string, error) { return wizard.GenerateRandomSecret(32) }) // Create env file envContent := wizard.EnvFileTemplate(oauthIssuer, oauthClientID, oauthClientSecret, oauthRedirectURI, jwtSecret) envFilePath = filepath.Join(configDir, "opal.env") // Create config directory first if err := os.MkdirAll(configDir, 0755); err != nil { wizard.PrintError(fmt.Sprintf("Failed to create config directory: %v", err)) os.Exit(1) } if err := os.WriteFile(envFilePath, []byte(envContent), 0600); err != nil { wizard.PrintError(fmt.Sprintf("Failed to write env file: %v", err)) os.Exit(1) } wizard.PrintSuccess(fmt.Sprintf("Environment file created: %s", envFilePath)) } else { wizard.PrintInfo("Skipping OAuth/JWT configuration") wizard.PrintInfo("You'll need to set these environment variables manually:") fmt.Println(" - OAUTH_ISSUER, OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET") fmt.Println(" - OAUTH_REDIRECT_URI, JWT_SECRET") } } // Step 4: Basic preferences wizard.PrintHeader(fmt.Sprintf("Step %d: Task Management Preferences", getStep(profile.IsServer, 4, 3))) weekStart := wizard.PromptChoice("Week starts on:", []string{"Monday", "Sunday"}, 0) weekStartDay := "monday" if weekStart == 1 { weekStartDay = "sunday" } colorOutput := wizard.PromptBool("Enable colored output?", true) defaultReport := wizard.PromptChoice("Default report:", []string{"list", "next", "ready"}, 0) reportNames := []string{"list", "next", "ready"} defaultFilter := wizard.PromptString("Default filter", "status:pending") // Step 5: Sync configuration wizard.PrintHeader(fmt.Sprintf("Step %d: Sync Configuration (Optional)", getStep(profile.IsServer, 5, 4))) syncEnabled := wizard.PromptBool("Enable sync with server?", false) syncURL := "" syncAPIKey := "" if syncEnabled { syncURL = wizard.PromptString("Sync server URL", "") syncAPIKey = wizard.PromptString("Sync API key", "") } // Step 6: Summary and confirmation wizard.PrintHeader("Configuration Summary") fmt.Printf("Profile: %s\n", profile.Name) fmt.Printf("Config: %s\n", configDir) fmt.Printf("Data: %s\n", dataDir) if envFilePath != "" { fmt.Printf("Environment: %s\n", envFilePath) } fmt.Printf("Week starts: %s\n", weekStartDay) fmt.Printf("Color output: %v\n", colorOutput) fmt.Printf("Default: %s\n", reportNames[defaultReport]) fmt.Printf("Filter: %s\n", defaultFilter) if syncEnabled { fmt.Printf("Sync: Enabled (%s)\n", syncURL) } else { fmt.Printf("Sync: Disabled\n") } fmt.Println() if !wizard.Confirm("Save configuration?") { fmt.Println("Setup cancelled.") return } // Create configuration cfg := &engine.Config{ DefaultFilter: defaultFilter, DefaultSort: "due,priority", DefaultReport: reportNames[defaultReport], ColorOutput: colorOutput, WeekStartDay: weekStartDay, DefaultDueTime: "", NextLimit: 5, SyncEnabled: syncEnabled, SyncURL: syncURL, SyncAPIKey: syncAPIKey, SyncStrategy: "last-write-wins", SyncQueueOffline: true, UrgencyDue: 12.0, UrgencyPriorityH: 6.0, UrgencyPriorityM: 3.9, UrgencyPriorityD: 1.8, UrgencyPriorityL: 0.0, UrgencyActive: 4.0, UrgencyAge: 2.0, UrgencyAgeMax: 365, UrgencyTags: 1.0, UrgencyProject: 1.0, UrgencyWaiting: -3.0, UrgencyUrgentTag: "next", UrgencyUrgentCoeff: 15.0, } // Set directory overrides engine.SetConfigDirOverride(configDir) engine.SetDataDirOverride(dataDir) // Create directories if err := os.MkdirAll(configDir, 0755); err != nil { wizard.PrintError(fmt.Sprintf("Failed to create config directory: %v", err)) os.Exit(1) } if err := os.MkdirAll(dataDir, 0755); err != nil { wizard.PrintError(fmt.Sprintf("Failed to create data directory: %v", err)) os.Exit(1) } // Save config if err := engine.SaveConfig(cfg); err != nil { wizard.PrintError(fmt.Sprintf("Failed to save config: %v", err)) os.Exit(1) } // Success! fmt.Println() wizard.PrintSuccess("Configuration saved!") fmt.Println() // Next steps fmt.Println("Next steps:") if profile.IsServer { if envFilePath != "" { fmt.Printf(" • Review environment file: %s\n", envFilePath) } configPath, _ := engine.GetConfigPath() fmt.Printf(" • Review configuration: %s\n", configPath) fmt.Println(" • Generate API key: opal server keygen --name 'admin'") fmt.Println(" • Start server: opal server start") fmt.Println() fmt.Println("For systemd service setup:") fmt.Println(" opal setup --show-systemd") } else { fmt.Println(" • Try it out: opal add 'My first task'") fmt.Println(" • List tasks: opal list") fmt.Println(" • Get help: opal --help") } fmt.Println() } func getStep(isServer bool, serverStep, clientStep int) int { if isServer { return serverStep } return clientStep }