Skip to content

Commit d23047f

Browse files
Upgrade CLI to typer (#1151)
Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com> Co-authored-by: Jay Hack <jayhack@users.noreply.github.com>
1 parent b28f1a5 commit d23047f

File tree

21 files changed

+162
-122
lines changed

21 files changed

+162
-122
lines changed

pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ readme = "README.md"
77
requires-python = ">=3.12, <3.14"
88
dependencies = [
99
"codegen-api-client",
10-
"click>=8.1.7",
11-
"rich-click>=1.8.5",
10+
"typer>=0.12.5",
1211
"rich>=13.7.1",
1312
"hatch-vcs>=0.4.0",
1413
"hatchling>=1.25.0",

src/codegen/cli/api/client.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from pydantic import BaseModel
66
from rich import print as rprint
77

8+
from codegen.cli.api.endpoints import IDENTIFY_ENDPOINT
9+
from codegen.cli.api.schemas import IdentifyResponse
810
from codegen.cli.env.global_env import global_env
911
from codegen.cli.errors import InvalidTokenError, ServerError
1012

@@ -75,3 +77,7 @@ def _make_request(
7577
except requests.RequestException as e:
7678
msg = f"Network error: {e!s}"
7779
raise ServerError(msg)
80+
81+
def identify(self) -> IdentifyResponse:
82+
"""Identify the current user with the authentication token."""
83+
return self._make_request("GET", IDENTIFY_ENDPOINT, None, IdentifyResponse)

src/codegen/cli/auth/decorators.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import functools
2+
import inspect
23
from collections.abc import Callable
34

4-
import click
55
import rich
6+
import typer
67

78
from codegen.cli.auth.login import login_routine
89
from codegen.cli.auth.session import CodegenSession
@@ -21,7 +22,7 @@ def wrapper(*args, **kwargs):
2122
# Check for valid session
2223
if session is None:
2324
pretty_print_error("There is currently no active session.\nPlease run 'codegen init' to initialize the project.")
24-
raise click.Abort()
25+
raise typer.Abort()
2526

2627
if (token := get_current_token()) is None:
2728
rich.print("[yellow]Not authenticated. Let's get you logged in first![/yellow]\n")
@@ -36,4 +37,10 @@ def wrapper(*args, **kwargs):
3637

3738
return f(*args, session=session, **kwargs)
3839

40+
# Remove the session parameter from the wrapper's signature so Typer doesn't see it
41+
sig = inspect.signature(f)
42+
new_params = [param for name, param in sig.parameters.items() if name != 'session']
43+
new_sig = sig.replace(parameters=new_params)
44+
wrapper.__signature__ = new_sig # type: ignore[attr-defined]
45+
3946
return wrapper

src/codegen/cli/auth/login.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import webbrowser
22

33
import rich
4-
import rich_click as click
4+
import typer
55

66
from codegen.cli.api.webapp_routes import USER_SECRETS_ROUTE
77
from codegen.cli.auth.token_manager import TokenManager
@@ -19,7 +19,7 @@ def login_routine(token: str | None = None) -> str:
1919
str: The authenticated token
2020
2121
Raises:
22-
click.ClickException: If login fails
22+
typer.Exit: If login fails
2323
2424
"""
2525
# Try environment variable first
@@ -29,11 +29,11 @@ def login_routine(token: str | None = None) -> str:
2929
if not token:
3030
rich.print(f"Opening {USER_SECRETS_ROUTE} to get your authentication token...")
3131
webbrowser.open_new(USER_SECRETS_ROUTE)
32-
token = click.prompt("Please enter your authentication token from the browser", hide_input=False)
32+
token = typer.prompt("Please enter your authentication token from the browser", hide_input=False)
3333

3434
if not token:
35-
msg = "Token must be provided via CODEGEN_USER_ACCESS_TOKEN environment variable or manual input"
36-
raise click.ClickException(msg)
35+
rich.print("[red]Error:[/red] Token must be provided via CODEGEN_USER_ACCESS_TOKEN environment variable or manual input")
36+
raise typer.Exit(1)
3737

3838
# Validate and store token
3939
try:
@@ -44,5 +44,5 @@ def login_routine(token: str | None = None) -> str:
4444
rich.print("To opt out, set [green]telemetry_enabled = false[/green] in [cyan]~/.config/codegen-sh/analytics.json[/cyan] ✨")
4545
return token
4646
except AuthError as e:
47-
msg = f"Error: {e!s}"
48-
raise click.ClickException(msg)
47+
rich.print(f"[red]Error:[/red] {e!s}")
48+
raise typer.Exit(1)

src/codegen/cli/auth/session.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from pathlib import Path
22

3-
import click
43
import rich
4+
import typer
55
from github import BadCredentialsException
66
from github.MainClass import Github
77

@@ -24,14 +24,14 @@ class CodegenSession:
2424
def __init__(self, repo_path: Path, git_token: str | None = None) -> None:
2525
if not repo_path.exists():
2626
rich.print(f"\n[bold red]Error:[/bold red] Path to git repo does not exist at {repo_path}")
27-
raise click.Abort()
27+
raise typer.Abort()
2828

2929
# Check if it's a valid git repository
3030
try:
3131
LocalGitRepo(repo_path=repo_path)
3232
except Exception:
3333
rich.print(f"\n[bold red]Error:[/bold red] Path {repo_path} is not a valid git repository")
34-
raise click.Abort()
34+
raise typer.Abort()
3535

3636
self.repo_path = repo_path
3737
self.local_git = LocalGitRepo(repo_path=repo_path)
@@ -82,12 +82,12 @@ def _validate(self) -> None:
8282
rich.print(format_command("git remote add origin <your-repo-url>"))
8383

8484
try:
85-
if git_token is not None:
85+
if git_token is not None and self.local_git.full_name is not None:
8686
Github(login_or_token=git_token).get_repo(self.local_git.full_name)
8787
except BadCredentialsException:
8888
rich.print(format_command(f"\n[bold red]Error:[/bold red] Invalid GitHub token={git_token} for repo={self.local_git.full_name}"))
8989
rich.print("[white]Please provide a valid GitHub token for this repository.[/white]")
90-
raise click.Abort()
90+
raise typer.Abort()
9191

9292
def __str__(self) -> str:
93-
return f"CodegenSession(user={self.config.repository.user_name}, repo={self.config.repository.repo_name})"
93+
return f"CodegenSession(user={self.config.repository.user_name}, repo={self.config.repository.name})"

src/codegen/cli/cli.py

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,53 @@
1-
import rich_click as click
1+
import typer
22
from rich.traceback import install
33

4-
# Removed reference to non-existent agent module
4+
# Import config command (still a Typer app)
55
from codegen.cli.commands.config.main import config_command
6-
from codegen.cli.commands.init.main import init_command
7-
from codegen.cli.commands.login.main import login_command
8-
from codegen.cli.commands.logout.main import logout_command
9-
from codegen.cli.commands.profile.main import profile_command
10-
from codegen.cli.commands.style_debug.main import style_debug_command
11-
from codegen.cli.commands.update.main import update_command
6+
from codegen import __version__
127

138
install(show_locals=True)
149

1510

16-
@click.group(name="codegen")
17-
@click.version_option(prog_name="codegen", message="%(version)s")
18-
def main():
11+
def version_callback(value: bool):
12+
"""Print version and exit."""
13+
if value:
14+
print(__version__)
15+
raise typer.Exit()
16+
17+
# Create the main Typer app
18+
main = typer.Typer(
19+
name="codegen",
20+
help="Codegen CLI - Transform your code with AI.",
21+
rich_markup_mode="rich"
22+
)
23+
24+
# Import the actual command functions
25+
from codegen.cli.commands.init.main import init
26+
from codegen.cli.commands.login.main import login
27+
from codegen.cli.commands.logout.main import logout
28+
from codegen.cli.commands.profile.main import profile
29+
from codegen.cli.commands.style_debug.main import style_debug
30+
from codegen.cli.commands.update.main import update
31+
32+
# Add individual commands to the main app
33+
main.command("init", help="Initialize or update the Codegen folder.")(init)
34+
main.command("login", help="Store authentication token.")(login)
35+
main.command("logout", help="Clear stored authentication token.")(logout)
36+
main.command("profile", help="Display information about the currently authenticated user.")(profile)
37+
main.command("style-debug", help="Debug command to visualize CLI styling (spinners, etc).")(style_debug)
38+
main.command("update", help="Update Codegen to the latest or specified version")(update)
39+
40+
# Config is a group, so add it as a typer
41+
main.add_typer(config_command, name="config")
42+
43+
44+
@main.callback()
45+
def main_callback(
46+
version: bool = typer.Option(False, "--version", callback=version_callback, is_eager=True, help="Show version and exit")
47+
):
1948
"""Codegen CLI - Transform your code with AI."""
2049
pass
2150

2251

23-
# Add commands to the main group
24-
main.add_command(init_command)
25-
main.add_command(logout_command)
26-
main.add_command(login_command)
27-
main.add_command(profile_command)
28-
main.add_command(style_debug_command)
29-
main.add_command(update_command)
30-
main.add_command(config_command)
31-
32-
3352
if __name__ == "__main__":
3453
main()

src/codegen/cli/commands/config/main.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,19 @@
11
import logging
22

33
import rich
4-
import rich_click as click
4+
import typer
55
from rich.table import Table
66

77
from codegen.configs.constants import ENV_FILENAME, GLOBAL_ENV_FILE
88
from codegen.configs.user_config import UserConfig
99
from codegen.shared.path import get_git_root_path
1010

11-
12-
@click.group(name="config")
13-
def config_command():
14-
"""Manage codegen configuration."""
15-
pass
11+
# Create a Typer app for the config command
12+
config_command = typer.Typer(help="Manage codegen configuration.")
1613

1714

1815
@config_command.command(name="list")
19-
def list_command():
16+
def list_config():
2017
"""List current configuration values."""
2118

2219
def flatten_dict(data: dict, prefix: str = "") -> dict:
@@ -80,8 +77,7 @@ def flatten_dict(data: dict, prefix: str = "") -> dict:
8077

8178

8279
@config_command.command(name="get")
83-
@click.argument("key")
84-
def get_command(key: str):
80+
def get_config(key: str = typer.Argument(..., help="Configuration key to get")):
8581
"""Get a configuration value."""
8682
config = _get_user_config()
8783
if not config.has_key(key):
@@ -94,9 +90,10 @@ def get_command(key: str):
9490

9591

9692
@config_command.command(name="set")
97-
@click.argument("key")
98-
@click.argument("value")
99-
def set_command(key: str, value: str):
93+
def set_config(
94+
key: str = typer.Argument(..., help="Configuration key to set"),
95+
value: str = typer.Argument(..., help="Configuration value to set")
96+
):
10097
"""Set a configuration value and write to .env"""
10198
config = _get_user_config()
10299
if not config.has_key(key):

src/codegen/cli/commands/init/main.py

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,38 @@
11
import sys
22
from pathlib import Path
3+
from typing import Optional
34

45
import rich
5-
import rich_click as click
6+
import typer
67

78
from codegen.cli.auth.session import CodegenSession
89
from codegen.cli.rich.codeblocks import format_command
910
from codegen.shared.path import get_git_root_path
1011

11-
12-
@click.command(name="init")
13-
@click.option("--path", type=str, help="Path within a git repository. Defaults to the current directory.")
14-
@click.option("--token", type=str, help="Access token for the git repository. Required for full functionality.")
15-
@click.option("--language", type=click.Choice(["python", "typescript"], case_sensitive=False), help="Override automatic language detection")
16-
@click.option("--fetch-docs", is_flag=True, help="Fetch docs and examples (requires auth)")
17-
def init_command(path: str | None = None, token: str | None = None, language: str | None = None, fetch_docs: bool = False):
12+
def init(
13+
path: Optional[str] = typer.Option(None, help="Path within a git repository. Defaults to the current directory."),
14+
token: Optional[str] = typer.Option(None, help="Access token for the git repository. Required for full functionality."),
15+
language: Optional[str] = typer.Option(None, help="Override automatic language detection (python or typescript)"),
16+
fetch_docs: bool = typer.Option(False, "--fetch-docs", help="Fetch docs and examples (requires auth)")
17+
):
1818
"""Initialize or update the Codegen folder."""
19+
# Validate language option
20+
if language and language.lower() not in ["python", "typescript"]:
21+
rich.print(f"[bold red]Error:[/bold red] Invalid language '{language}'. Must be 'python' or 'typescript'.")
22+
raise typer.Exit(1)
23+
1924
# Print a message if not in a git repo
20-
path = Path.cwd() if path is None else Path(path)
21-
repo_path = get_git_root_path(path)
25+
current_path = Path.cwd() if path is None else Path(path)
26+
repo_path = get_git_root_path(current_path)
2227
rich.print(f"Found git repository at: {repo_path}")
2328

2429
if repo_path is None:
25-
rich.print(f"\n[bold red]Error:[/bold red] Path={path} is not in a git repository")
30+
rich.print(f"\n[bold red]Error:[/bold red] Path={current_path} is not in a git repository")
2631
rich.print("[white]Please run this command from within a git repository.[/white]")
2732
rich.print("\n[dim]To initialize a new git repository:[/dim]")
2833
rich.print(format_command("git init"))
2934
rich.print(format_command("codegen init"))
30-
sys.exit(1)
35+
raise typer.Exit(1)
3136

3237
session = CodegenSession(repo_path=repo_path, git_token=token)
3338
if language:
Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
1-
import rich_click as click
1+
from typing import Optional
2+
import typer
3+
import rich
24

35
from codegen.cli.auth.login import login_routine
46
from codegen.cli.auth.token_manager import get_current_token
57

6-
7-
@click.command(name="login")
8-
@click.option("--token", required=False, help="API token for authentication")
9-
def login_command(token: str):
8+
def login(token: Optional[str] = typer.Option(None, help="API token for authentication")):
109
"""Store authentication token."""
1110
# Check if already authenticated
1211
if get_current_token():
13-
msg = "Already authenticated. Use 'codegen logout' to clear the token."
14-
raise click.ClickException(msg)
12+
rich.print("[yellow]Warning:[/yellow] Already authenticated. Use 'codegen logout' to clear the token.")
13+
raise typer.Exit(1)
1514

1615
login_routine(token)

src/codegen/cli/commands/logout/main.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import rich
2-
import rich_click as click
2+
import typer
33

44
from codegen.cli.auth.token_manager import TokenManager
55

6-
7-
@click.command(name="logout")
8-
def logout_command():
6+
def logout():
97
"""Clear stored authentication token."""
108
token_manager = TokenManager()
119
token_manager.clear_token()

src/codegen/cli/commands/profile/main.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import rich
2-
import rich_click as click
2+
import typer
33
from rich import box
44
from rich.panel import Panel
55

@@ -13,16 +13,14 @@ def requires_init(func):
1313
"""Simple stub decorator that does nothing."""
1414
return func
1515

16-
17-
@click.command(name="profile")
1816
@requires_auth
1917
@requires_init
20-
def profile_command(session: CodegenSession):
18+
def profile(session: CodegenSession):
2119
"""Display information about the currently authenticated user."""
2220
repo_config = session.config.repository
2321
rich.print(
2422
Panel(
25-
f"[cyan]Name:[/cyan] {repo_config.user_name}\n[cyan]Email:[/cyan] {repo_config.user_email}\n[cyan]Repo:[/cyan] {repo_config.repo_name}",
23+
f"[cyan]Name:[/cyan] {repo_config.user_name}\n[cyan]Email:[/cyan] {repo_config.user_email}\n[cyan]Repo:[/cyan] {repo_config.name}",
2624
title="🔑 [bold]Current Profile[/bold]",
2725
border_style="cyan",
2826
box=box.ROUNDED,

src/codegen/cli/commands/style_debug/main.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,11 @@
22

33
import time
44

5-
import rich_click as click
5+
import typer
66

77
from codegen.cli.rich.spinners import create_spinner
88

9-
10-
@click.command(name="style-debug")
11-
@click.option("--text", default="Loading...", help="Text to show in the spinner")
12-
def style_debug_command(text: str):
9+
def style_debug(text: str = typer.Option("Loading...", help="Text to show in the spinner")):
1310
"""Debug command to visualize CLI styling (spinners, etc)."""
1411
try:
1512
with create_spinner(text) as status:

0 commit comments

Comments
 (0)