6a4fdb6850
Display the default config and data directories before prompting the user if they want to customize them. This gives better context for the decision. Before: Only showed directories if user chose not to customize After: Shows directories first, then asks if user wants to change them
389 lines
12 KiB
Go
389 lines
12 KiB
Go
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
|
|
}
|