1+ """
2+ Setup command group for PraisonAI CLI.
3+
4+ Provides interactive onboarding and configuration wizard.
5+ """
6+
7+ import os
8+ from pathlib import Path
9+ from typing import Optional
10+
11+ import typer
12+
13+ from ..output .console import get_output_controller
14+
15+ app = typer .Typer (help = "Interactive onboarding / configuration wizard" )
16+
17+ # Default PRAISON_HOME directory
18+ def get_praison_home () -> Path :
19+ """Get the PraisonAI home directory."""
20+ home = os .getenv ("PRAISONAI_HOME" )
21+ if home :
22+ return Path (home )
23+ return Path .home () / ".praisonai"
24+
25+ PRAISON_HOME = get_praison_home ()
26+ ENV_FILE = PRAISON_HOME / ".env"
27+
28+ # Provider configurations
29+ PROVIDERS = {
30+ "1" : ("openai" , "OPENAI_API_KEY" , "gpt-4o-mini" ),
31+ "2" : ("anthropic" , "ANTHROPIC_API_KEY" , "claude-3-5-sonnet-latest" ),
32+ "3" : ("google" , "GEMINI_API_KEY" , "gemini-2.0-flash" ),
33+ "4" : ("ollama" , None , "llama3.2" ),
34+ "5" : ("custom" , None , None ),
35+ }
36+
37+ PROVIDER_NAMES = {
38+ "openai" : ("OpenAI" , "OPENAI_API_KEY" , "gpt-4o-mini" ),
39+ "anthropic" : ("Anthropic" , "ANTHROPIC_API_KEY" , "claude-3-5-sonnet-latest" ),
40+ "google" : ("Google" , "GEMINI_API_KEY" , "gemini-2.0-flash" ),
41+ "ollama" : ("Ollama" , None , "llama3.2" ),
42+ "custom" : ("Custom" , None , None ),
43+ }
44+
45+
46+ def _run_setup (
47+ non_interactive : bool = False ,
48+ provider : Optional [str ] = None ,
49+ api_key : Optional [str ] = None ,
50+ model : Optional [str ] = None ,
51+ ) -> int :
52+ """Run the setup wizard."""
53+ try :
54+ from ..features .setup .handler import SetupHandler
55+ handler = SetupHandler ()
56+ return handler .execute (
57+ non_interactive = non_interactive ,
58+ provider = provider ,
59+ api_key = api_key ,
60+ model = model
61+ )
62+ except ImportError as e :
63+ output = get_output_controller ()
64+ output .print_error (f"Setup module not available: { e } " )
65+ return 4
66+ except Exception as e :
67+ output = get_output_controller ()
68+ output .print_error (f"Setup error: { e } " )
69+ return 1
70+
71+
72+ @app .callback (invoke_without_command = True )
73+ def setup_callback (
74+ ctx : typer .Context ,
75+ non_interactive : bool = typer .Option (False , "--non-interactive" , help = "Run in non-interactive mode" ),
76+ provider : Optional [str ] = typer .Option (None , "--provider" , help = "LLM provider (openai, anthropic, google, ollama, custom)" ),
77+ api_key : Optional [str ] = typer .Option (None , "--api-key" , help = "API key for the provider" ),
78+ model : Optional [str ] = typer .Option (None , "--model" , help = "Default model to use" ),
79+ ):
80+ """Run the onboarding wizard (idempotent — safe to re-run)."""
81+ if ctx .invoked_subcommand :
82+ return
83+
84+ exit_code = _run_setup (
85+ non_interactive = non_interactive ,
86+ provider = provider ,
87+ api_key = api_key ,
88+ model = model
89+ )
90+ raise typer .Exit (exit_code )
91+
92+
93+ @app .command ("wizard" )
94+ def setup_wizard (
95+ provider : Optional [str ] = typer .Option (None , "--provider" , help = "LLM provider" ),
96+ api_key : Optional [str ] = typer .Option (None , "--api-key" , help = "API key" ),
97+ model : Optional [str ] = typer .Option (None , "--model" , help = "Default model" ),
98+ ):
99+ """Run the interactive setup wizard."""
100+ exit_code = _run_setup (
101+ non_interactive = False ,
102+ provider = provider ,
103+ api_key = api_key ,
104+ model = model
105+ )
106+ raise typer .Exit (exit_code )
107+
108+
109+ @app .command ("config" )
110+ def setup_config (
111+ show : bool = typer .Option (False , "--show" , help = "Show current configuration" ),
112+ edit : bool = typer .Option (False , "--edit" , help = "Edit configuration file" ),
113+ ):
114+ """Manage setup configuration."""
115+ output = get_output_controller ()
116+
117+ if show :
118+ if ENV_FILE .exists ():
119+ output .console .print (f"[bold]Configuration at { ENV_FILE } :[/bold]" )
120+ content = ENV_FILE .read_text ()
121+ # Don't show actual API keys for security
122+ lines = []
123+ for line in content .split ('\n ' ):
124+ if '=' in line and any (key in line for key in ['API_KEY' , 'TOKEN' , 'SECRET' ]):
125+ key , _ = line .split ('=' , 1 )
126+ lines .append (f"{ key } =***" )
127+ else :
128+ lines .append (line )
129+ output .console .print ('\n ' .join (lines ))
130+ else :
131+ output .print_warning (f"No configuration found at { ENV_FILE } " )
132+ output .console .print ("Run [cyan]praisonai setup[/cyan] to create one." )
133+
134+ if edit :
135+ import subprocess
136+ editor = os .getenv ("EDITOR" , "nano" )
137+ try :
138+ subprocess .run ([editor , str (ENV_FILE )], check = True )
139+ except subprocess .CalledProcessError :
140+ output .print_error (f"Failed to open editor: { editor } " )
141+ except FileNotFoundError :
142+ output .print_error (f"Editor not found: { editor } " )
143+
144+
145+ @app .command ("reset" )
146+ def setup_reset (
147+ force : bool = typer .Option (False , "--force" , help = "Skip confirmation" ),
148+ ):
149+ """Reset setup configuration."""
150+ output = get_output_controller ()
151+
152+ praison_home = get_praison_home ()
153+ env_file = praison_home / ".env"
154+ config_file = praison_home / "config.yaml"
155+ files_to_remove = [path for path in (env_file , config_file ) if path .exists ()]
156+
157+ if not files_to_remove :
158+ output .print_info ("No setup configuration to reset." )
159+ return
160+
161+ if not force :
162+ confirm = typer .confirm (f"Reset configuration at { praison_home } ?" )
163+ if not confirm :
164+ output .print_info ("Reset cancelled." )
165+ return
166+
167+ try :
168+ for path in files_to_remove :
169+ path .unlink ()
170+ output .print_success ("Configuration reset successfully." )
171+ output .console .print ("Run [cyan]praisonai setup[/cyan] to configure again." )
172+ except Exception as e :
173+ output .print_error (f"Failed to reset configuration: { e } " )
174+ raise typer .Exit (1 )
0 commit comments