33PraisonAI CLI — Unified Entry Point.
44
55Single entry point for all CLI invocations.
6- Routes to Typer-based commands for known subcommands,
7- falls back to legacy argparse for direct prompts and YAML files.
6+ Makes Typer the single dispatcher with narrow legacy shim for bare prompts/YAML.
87
98Design:
10- - Typer-first: all registered commands auto-discovered via Click
11- - Legacy fallback: prompts, .yaml paths, and deprecated --flags
12- - No manual command lists needed — adding a Typer command Just Works
9+ - Typer owns all command resolution
10+ - Legacy shim only for bare prompt/YAML invocations via Typer callback
11+ - Fail loud on registration errors - no silent degradation
1312"""
1413
1514import sys
1615
1716
18- # ---------------------------------------------------------------------------
19- # Internal helpers
20- # ---------------------------------------------------------------------------
21-
22- _typer_commands_cache = None
23-
24-
25- def _get_typer_commands ():
26- """Auto-discover registered Typer commands via Click introspection.
27-
28- Returns a set of command names that the Typer app knows about.
29- This is populated from app.py's register_commands() — no manual
30- lists to maintain.
17+ def _is_legacy_invocation (argv : list [str ]) -> bool :
18+ """Check if this is a bare prompt or bare YAML invocation.
19+
20+ Legacy invocations are:
21+ - Bare YAML file: "agents.yaml"
22+ - Free-text prompt: "Create a weather app"
23+
24+ All other invocations should be handled by Typer commands.
3125 """
32- global _typer_commands_cache
33- if _typer_commands_cache is not None :
34- return _typer_commands_cache
35-
36- try :
37- from praisonai .cli .app import app , register_commands
38- register_commands ()
39-
40- import typer .main
41- import click
42- click_app = typer .main .get_command (app )
43- ctx = click .Context (click_app , info_name = "praisonai" )
44- _typer_commands_cache = set (click_app .list_commands (ctx ))
45- except Exception :
46- _typer_commands_cache = set ()
47-
48- return _typer_commands_cache
49-
50-
51- def _find_first_command (argv ):
52- """Find the first non-flag argument in argv.
53-
54- Skips global flags (--json, --verbose, etc.) and their values.
55- Returns the first positional arg, or None if only flags are present.
56- """
57- # Flags that consume a following value
58- VALUE_FLAGS = {"--output-format" , "-o" }
59-
60- skip_next = False
6126 for arg in argv :
62- if skip_next :
63- skip_next = False
64- continue
6527 if arg .startswith ("-" ):
66- if arg in VALUE_FLAGS :
67- skip_next = True
6828 continue
69- return arg # First non-flag arg
70- return None
71-
72-
73- def _run_typer (argv ):
74- """Dispatch to the Typer CLI app."""
75- from praisonai .cli .app import app , register_commands
76- register_commands () # idempotent
77-
78- original = sys .argv
79- sys .argv = ["praisonai" ] + list (argv )
80- try :
81- app ()
82- except SystemExit as e :
83- sys .exit (e .code if isinstance (e .code , int ) else 0 )
84- finally :
85- sys .argv = original
29+ # Check if it's a YAML file or contains spaces (free-text prompt)
30+ return (arg .endswith ((".yaml" , ".yml" )) or
31+ " " in arg or
32+ not arg .isidentifier ())
33+ return False
8634
8735
88- def _run_legacy (argv ):
89- """Dispatch to the legacy argparse CLI (prompts, YAML, deprecated flags)."""
90- from praisonai .cli .main import PraisonAI
91-
92- original = sys .argv
93- sys .argv = ["praisonai" ] + list (argv )
94- try :
95- praison = PraisonAI ()
96- result = praison .main ()
97- code = 0 if result is None else (1 if result is False else 0 )
98- sys .exit (code )
99- except SystemExit as e :
100- sys .exit (e .code if isinstance (e .code , int ) else 0 )
101- finally :
102- sys .argv = original
103-
104-
105- # ---------------------------------------------------------------------------
106- # Main entry point
107- # ---------------------------------------------------------------------------
108-
10936def main ():
110- """Unified CLI entry point — Typer-first, legacy fallback .
37+ """Unified CLI entry point - Typer is the single dispatcher .
11138
11239 Routing rules (in order):
113- 1. --version / -V → print version and exit
114- 2. --help / -h → Typer help (global or command-level)
115- 3. No arguments → Typer interactive TUI
116- 4. First arg is a Typer cmd→ Typer (auto-discovered from app.py)
117- 5. Everything else → Legacy (prompt, .yaml, deprecated flags)
40+ 1. --version / -V → print version and exit
41+ 2. Legacy invocation → legacy shim (bare prompts/YAML only)
42+ 3. Everything else → Typer (owns all subcommands)
11843 """
11944 argv = sys .argv [1 :]
12045
@@ -124,30 +49,36 @@ def main():
12449 print (f"PraisonAI version { __version__ } " )
12550 return
12651
127- # 2. Help flags → always Typer (global help or command help)
128- if "--help" in argv or "-h" in argv :
129- _run_typer (argv )
130- return
131-
132- # 3. No arguments → Typer (interactive TUI)
133- if not argv :
134- _run_typer (argv )
52+ # 2. Check for legacy invocation (bare prompt/YAML)
53+ if _is_legacy_invocation (argv ):
54+ from praisonai .cli .main import PraisonAI
55+ original = sys .argv
56+ sys .argv = ["praisonai" ] + list (argv )
57+ try :
58+ praison = PraisonAI ()
59+ result = praison .main ()
60+ code = 0 if result is None else (1 if result is False else 0 )
61+ sys .exit (code )
62+ except SystemExit as e :
63+ sys .exit (e .code if isinstance (e .code , int ) else 0 )
64+ finally :
65+ sys .argv = original
13566 return
13667
137- # 4. Find first non-flag argument and check if it's a Typer command
138- first_cmd = _find_first_command ( argv )
139-
140- if first_cmd is None :
141- # Only flags, no command → Typer handles global flags
142- _run_typer ( argv )
143- return
144-
145- if first_cmd in _get_typer_commands () :
146- # Known Typer command → Typer
147- _run_typer ( argv )
148- else :
149- # Prompt, YAML file, or legacy invocation → legacy
150- _run_legacy ( argv )
68+ # 3. All other invocations → Typer (fail loud on registration errors)
69+ from praisonai . cli . app import app , register_commands
70+
71+ # CRITICAL: Fail loud - do not swallow registration exceptions
72+ register_commands () # Let any ImportError/other exceptions propagate
73+
74+ original = sys . argv
75+ sys . argv = [ "praisonai" ] + list ( argv )
76+ try :
77+ app ()
78+ except SystemExit as e :
79+ sys . exit ( e . code if isinstance ( e . code , int ) else 0 )
80+ finally :
81+ sys . argv = original
15182
15283
15384if __name__ == "__main__" :
0 commit comments