diff --git a/src/codegen/__init__.py b/codegen/__init__.py similarity index 52% rename from src/codegen/__init__.py rename to codegen/__init__.py index 56f66fcdc..f9e70382f 100644 --- a/src/codegen/__init__.py +++ b/codegen/__init__.py @@ -1,4 +1,14 @@ -from codegen.agents import Agent +# Optional Agent import to avoid dependency issues during development +try: + from codegen.agents import Agent +except ImportError: + # Fallback Agent class if dependencies are missing + class Agent: + def __init__(self, *args, **kwargs): + self.args = args + self.kwargs = kwargs + def __repr__(self): + return "Agent(placeholder - dependencies missing)" # Import version information from the auto-generated _version.py try: diff --git a/codegen/__main__.py b/codegen/__main__.py new file mode 100644 index 000000000..07b1afa45 --- /dev/null +++ b/codegen/__main__.py @@ -0,0 +1,25 @@ +# C:\Programs\codegen\src\codegen\__main__.py +import sys +import os + +# Add the src directory to the path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src")) + +# Import compatibility module first +from codegen.compat import * + +# Import only what we need for version +try: + from codegen.cli.cli import main +except ImportError: + + def main(): + # Fallback version function + import importlib.metadata + + version = importlib.metadata.version("codegen") + print(version) + + +if __name__ == "__main__": + main() diff --git a/src/codegen/agents/README.md b/codegen/agents/README.md similarity index 100% rename from src/codegen/agents/README.md rename to codegen/agents/README.md diff --git a/src/codegen/agents/__init__.py b/codegen/agents/__init__.py similarity index 100% rename from src/codegen/agents/__init__.py rename to codegen/agents/__init__.py diff --git a/src/codegen/agents/agent.py b/codegen/agents/agent.py similarity index 100% rename from src/codegen/agents/agent.py rename to codegen/agents/agent.py diff --git a/src/codegen/agents/constants.py b/codegen/agents/constants.py similarity index 100% rename from src/codegen/agents/constants.py rename to codegen/agents/constants.py diff --git a/src/codegen/cli/README.md b/codegen/cli/README.md similarity index 100% rename from src/codegen/cli/README.md rename to codegen/cli/README.md diff --git a/src/codegen/cli/__init__.py b/codegen/cli/__init__.py similarity index 100% rename from src/codegen/cli/__init__.py rename to codegen/cli/__init__.py diff --git a/src/codegen/cli/_env.py b/codegen/cli/_env.py similarity index 100% rename from src/codegen/cli/_env.py rename to codegen/cli/_env.py diff --git a/src/codegen/cli/api/client.py b/codegen/cli/api/client.py similarity index 100% rename from src/codegen/cli/api/client.py rename to codegen/cli/api/client.py diff --git a/src/codegen/cli/api/endpoints.py b/codegen/cli/api/endpoints.py similarity index 100% rename from src/codegen/cli/api/endpoints.py rename to codegen/cli/api/endpoints.py diff --git a/src/codegen/cli/api/modal.py b/codegen/cli/api/modal.py similarity index 100% rename from src/codegen/cli/api/modal.py rename to codegen/cli/api/modal.py diff --git a/src/codegen/cli/api/schemas.py b/codegen/cli/api/schemas.py similarity index 100% rename from src/codegen/cli/api/schemas.py rename to codegen/cli/api/schemas.py diff --git a/src/codegen/cli/api/webapp_routes.py b/codegen/cli/api/webapp_routes.py similarity index 100% rename from src/codegen/cli/api/webapp_routes.py rename to codegen/cli/api/webapp_routes.py diff --git a/src/codegen/cli/auth/constants.py b/codegen/cli/auth/constants.py similarity index 100% rename from src/codegen/cli/auth/constants.py rename to codegen/cli/auth/constants.py diff --git a/src/codegen/cli/auth/decorators.py b/codegen/cli/auth/decorators.py similarity index 100% rename from src/codegen/cli/auth/decorators.py rename to codegen/cli/auth/decorators.py diff --git a/src/codegen/cli/auth/login.py b/codegen/cli/auth/login.py similarity index 100% rename from src/codegen/cli/auth/login.py rename to codegen/cli/auth/login.py diff --git a/src/codegen/cli/auth/session.ipynb b/codegen/cli/auth/session.ipynb similarity index 100% rename from src/codegen/cli/auth/session.ipynb rename to codegen/cli/auth/session.ipynb diff --git a/src/codegen/cli/auth/session.py b/codegen/cli/auth/session.py similarity index 100% rename from src/codegen/cli/auth/session.py rename to codegen/cli/auth/session.py diff --git a/src/codegen/cli/auth/token_manager.ipynb b/codegen/cli/auth/token_manager.ipynb similarity index 100% rename from src/codegen/cli/auth/token_manager.ipynb rename to codegen/cli/auth/token_manager.ipynb diff --git a/src/codegen/cli/auth/token_manager.py b/codegen/cli/auth/token_manager.py similarity index 100% rename from src/codegen/cli/auth/token_manager.py rename to codegen/cli/auth/token_manager.py diff --git a/src/codegen/cli/claude/__init__.py b/codegen/cli/claude/__init__.py similarity index 100% rename from src/codegen/cli/claude/__init__.py rename to codegen/cli/claude/__init__.py diff --git a/src/codegen/cli/cli.py b/codegen/cli/cli.py similarity index 64% rename from src/codegen/cli/cli.py rename to codegen/cli/cli.py index ab19f73ae..070798df3 100644 --- a/src/codegen/cli/cli.py +++ b/codegen/cli/cli.py @@ -2,7 +2,28 @@ import typer from rich.traceback import install +import sys +# Import compatibility module first +from codegen.compat import * + +# Only import TUI if not on Windows +if sys.platform != "win32": + from codegen.cli.commands.tui.main import tui +else: + + def tui(): + """Placeholder TUI for Windows.""" + print( + "TUI is not available on Windows. Use 'codegen --help' for available commands." + ) + + # Import tui_command for Windows + from codegen.cli.commands.tui.main import tui_command as tui + + +# Import compatibility module first +from codegen.compat import * from codegen import __version__ from codegen.cli.commands.agent.main import agent from codegen.cli.commands.agents.main import agents_app @@ -51,23 +72,36 @@ def version_callback(value: bool): """Print version and exit.""" if value: - logger.info("Version command invoked", extra={"operation": "cli.version", "version": __version__}) + logger.info( + "Version command invoked", + extra={"operation": "cli.version", "version": __version__}, + ) print(__version__) raise typer.Exit() # Create the main Typer app -main = typer.Typer(name="codegen", help="Codegen - the Operating System for Code Agents.", rich_markup_mode="rich") +main = typer.Typer( + name="codegen", + help="Codegen - the Operating System for Code Agents.", + rich_markup_mode="rich", +) # Add individual commands to the main app (logging now handled within each command) main.command("agent", help="Create a new agent run with a prompt.")(agent) -main.command("claude", help="Run Claude Code with OpenTelemetry monitoring and logging.")(claude) +main.command( + "claude", help="Run Claude Code with OpenTelemetry monitoring and logging." +)(claude) main.command("init", help="Initialize or update the Codegen folder.")(init) main.command("login", help="Store authentication token.")(login) main.command("logout", help="Clear stored authentication token.")(logout) main.command("org", help="Manage and switch between organizations.")(org) -main.command("repo", help="Manage repository configuration and environment variables.")(repo) -main.command("style-debug", help="Debug command to visualize CLI styling (spinners, etc).")(style_debug) +main.command("repo", help="Manage repository configuration and environment variables.")( + repo +) +main.command( + "style-debug", help="Debug command to visualize CLI styling (spinners, etc)." +)(style_debug) main.command("tools", help="List available tools from the Codegen API.")(tools) main.command("tui", help="Launch the interactive TUI interface.")(tui) main.command("update", help="Update Codegen to the latest or specified version")(update) @@ -80,17 +114,40 @@ def version_callback(value: bool): @main.callback(invoke_without_command=True) -def main_callback(ctx: typer.Context, version: bool = typer.Option(False, "--version", callback=version_callback, is_eager=True, help="Show version and exit")): +def main_callback( + ctx: typer.Context, + version: bool = typer.Option( + False, + "--version", + callback=version_callback, + is_eager=True, + help="Show version and exit", + ), +): """Codegen - the Operating System for Code Agents""" if ctx.invoked_subcommand is None: # No subcommand provided, launch TUI - logger.info("CLI launched without subcommand - starting TUI", extra={"operation": "cli.main", "action": "default_tui_launch", "command": "codegen"}) + logger.info( + "CLI launched without subcommand - starting TUI", + extra={ + "operation": "cli.main", + "action": "default_tui_launch", + "command": "codegen", + }, + ) from codegen.cli.tui.app import run_tui run_tui() else: # Log when a subcommand is being invoked - logger.debug("CLI main callback with subcommand", extra={"operation": "cli.main", "subcommand": ctx.invoked_subcommand, "command": f"codegen {ctx.invoked_subcommand}"}) + logger.debug( + "CLI main callback with subcommand", + extra={ + "operation": "cli.main", + "subcommand": ctx.invoked_subcommand, + "command": f"codegen {ctx.invoked_subcommand}", + }, + ) if __name__ == "__main__": diff --git a/src/codegen/cli/commands/agent/__init__.py b/codegen/cli/commands/agent/__init__.py similarity index 100% rename from src/codegen/cli/commands/agent/__init__.py rename to codegen/cli/commands/agent/__init__.py diff --git a/src/codegen/cli/commands/agent/main.py b/codegen/cli/commands/agent/main.py similarity index 100% rename from src/codegen/cli/commands/agent/main.py rename to codegen/cli/commands/agent/main.py diff --git a/src/codegen/cli/commands/agents/__init__.py b/codegen/cli/commands/agents/__init__.py similarity index 100% rename from src/codegen/cli/commands/agents/__init__.py rename to codegen/cli/commands/agents/__init__.py diff --git a/src/codegen/cli/commands/agents/main.py b/codegen/cli/commands/agents/main.py similarity index 100% rename from src/codegen/cli/commands/agents/main.py rename to codegen/cli/commands/agents/main.py diff --git a/src/codegen/cli/commands/claude/__init__.py b/codegen/cli/commands/claude/__init__.py similarity index 100% rename from src/codegen/cli/commands/claude/__init__.py rename to codegen/cli/commands/claude/__init__.py diff --git a/src/codegen/cli/commands/claude/claude_log_utils.py b/codegen/cli/commands/claude/claude_log_utils.py similarity index 100% rename from src/codegen/cli/commands/claude/claude_log_utils.py rename to codegen/cli/commands/claude/claude_log_utils.py diff --git a/src/codegen/cli/commands/claude/claude_log_watcher.py b/codegen/cli/commands/claude/claude_log_watcher.py similarity index 100% rename from src/codegen/cli/commands/claude/claude_log_watcher.py rename to codegen/cli/commands/claude/claude_log_watcher.py diff --git a/src/codegen/cli/commands/claude/claude_session_api.py b/codegen/cli/commands/claude/claude_session_api.py similarity index 100% rename from src/codegen/cli/commands/claude/claude_session_api.py rename to codegen/cli/commands/claude/claude_session_api.py diff --git a/src/codegen/cli/commands/claude/config/claude_session_active_hook.py b/codegen/cli/commands/claude/config/claude_session_active_hook.py similarity index 100% rename from src/codegen/cli/commands/claude/config/claude_session_active_hook.py rename to codegen/cli/commands/claude/config/claude_session_active_hook.py diff --git a/src/codegen/cli/commands/claude/config/claude_session_hook.py b/codegen/cli/commands/claude/config/claude_session_hook.py similarity index 100% rename from src/codegen/cli/commands/claude/config/claude_session_hook.py rename to codegen/cli/commands/claude/config/claude_session_hook.py diff --git a/src/codegen/cli/commands/claude/config/claude_session_stop_hook.py b/codegen/cli/commands/claude/config/claude_session_stop_hook.py similarity index 100% rename from src/codegen/cli/commands/claude/config/claude_session_stop_hook.py rename to codegen/cli/commands/claude/config/claude_session_stop_hook.py diff --git a/src/codegen/cli/commands/claude/config/mcp_setup.py b/codegen/cli/commands/claude/config/mcp_setup.py similarity index 100% rename from src/codegen/cli/commands/claude/config/mcp_setup.py rename to codegen/cli/commands/claude/config/mcp_setup.py diff --git a/src/codegen/cli/commands/claude/hooks.py b/codegen/cli/commands/claude/hooks.py similarity index 100% rename from src/codegen/cli/commands/claude/hooks.py rename to codegen/cli/commands/claude/hooks.py diff --git a/src/codegen/cli/commands/claude/main.py b/codegen/cli/commands/claude/main.py similarity index 100% rename from src/codegen/cli/commands/claude/main.py rename to codegen/cli/commands/claude/main.py diff --git a/src/codegen/cli/commands/claude/quiet_console.py b/codegen/cli/commands/claude/quiet_console.py similarity index 100% rename from src/codegen/cli/commands/claude/quiet_console.py rename to codegen/cli/commands/claude/quiet_console.py diff --git a/src/codegen/cli/commands/claude/utils.py b/codegen/cli/commands/claude/utils.py similarity index 100% rename from src/codegen/cli/commands/claude/utils.py rename to codegen/cli/commands/claude/utils.py diff --git a/src/codegen/cli/commands/config/main.py b/codegen/cli/commands/config/main.py similarity index 100% rename from src/codegen/cli/commands/config/main.py rename to codegen/cli/commands/config/main.py diff --git a/src/codegen/cli/commands/config/telemetry.py b/codegen/cli/commands/config/telemetry.py similarity index 100% rename from src/codegen/cli/commands/config/telemetry.py rename to codegen/cli/commands/config/telemetry.py diff --git a/src/codegen/cli/commands/init/main.py b/codegen/cli/commands/init/main.py similarity index 100% rename from src/codegen/cli/commands/init/main.py rename to codegen/cli/commands/init/main.py diff --git a/src/codegen/cli/commands/init/render.py b/codegen/cli/commands/init/render.py similarity index 100% rename from src/codegen/cli/commands/init/render.py rename to codegen/cli/commands/init/render.py diff --git a/src/codegen/cli/commands/integrations/__init__.py b/codegen/cli/commands/integrations/__init__.py similarity index 100% rename from src/codegen/cli/commands/integrations/__init__.py rename to codegen/cli/commands/integrations/__init__.py diff --git a/src/codegen/cli/commands/integrations/main.py b/codegen/cli/commands/integrations/main.py similarity index 100% rename from src/codegen/cli/commands/integrations/main.py rename to codegen/cli/commands/integrations/main.py diff --git a/src/codegen/cli/commands/login/main.py b/codegen/cli/commands/login/main.py similarity index 100% rename from src/codegen/cli/commands/login/main.py rename to codegen/cli/commands/login/main.py diff --git a/src/codegen/cli/commands/logout/main.py b/codegen/cli/commands/logout/main.py similarity index 100% rename from src/codegen/cli/commands/logout/main.py rename to codegen/cli/commands/logout/main.py diff --git a/src/codegen/cli/commands/org/__init__.py b/codegen/cli/commands/org/__init__.py similarity index 100% rename from src/codegen/cli/commands/org/__init__.py rename to codegen/cli/commands/org/__init__.py diff --git a/src/codegen/cli/commands/org/main.py b/codegen/cli/commands/org/main.py similarity index 100% rename from src/codegen/cli/commands/org/main.py rename to codegen/cli/commands/org/main.py diff --git a/src/codegen/cli/commands/org/tui.py b/codegen/cli/commands/org/tui.py similarity index 100% rename from src/codegen/cli/commands/org/tui.py rename to codegen/cli/commands/org/tui.py diff --git a/src/codegen/cli/commands/profile/main.py b/codegen/cli/commands/profile/main.py similarity index 100% rename from src/codegen/cli/commands/profile/main.py rename to codegen/cli/commands/profile/main.py diff --git a/src/codegen/cli/commands/repo/__init__.py b/codegen/cli/commands/repo/__init__.py similarity index 100% rename from src/codegen/cli/commands/repo/__init__.py rename to codegen/cli/commands/repo/__init__.py diff --git a/src/codegen/cli/commands/repo/main.py b/codegen/cli/commands/repo/main.py similarity index 100% rename from src/codegen/cli/commands/repo/main.py rename to codegen/cli/commands/repo/main.py diff --git a/src/codegen/cli/commands/repo/tui.py b/codegen/cli/commands/repo/tui.py similarity index 100% rename from src/codegen/cli/commands/repo/tui.py rename to codegen/cli/commands/repo/tui.py diff --git a/src/codegen/cli/commands/style_debug/main.py b/codegen/cli/commands/style_debug/main.py similarity index 100% rename from src/codegen/cli/commands/style_debug/main.py rename to codegen/cli/commands/style_debug/main.py diff --git a/src/codegen/cli/commands/tools/__init__.py b/codegen/cli/commands/tools/__init__.py similarity index 100% rename from src/codegen/cli/commands/tools/__init__.py rename to codegen/cli/commands/tools/__init__.py diff --git a/src/codegen/cli/commands/tools/main.py b/codegen/cli/commands/tools/main.py similarity index 100% rename from src/codegen/cli/commands/tools/main.py rename to codegen/cli/commands/tools/main.py diff --git a/src/codegen/cli/commands/tui/__init__.py b/codegen/cli/commands/tui/__init__.py similarity index 100% rename from src/codegen/cli/commands/tui/__init__.py rename to codegen/cli/commands/tui/__init__.py diff --git a/codegen/cli/commands/tui/main.py b/codegen/cli/commands/tui/main.py new file mode 100644 index 000000000..ec41ed8f4 --- /dev/null +++ b/codegen/cli/commands/tui/main.py @@ -0,0 +1,33 @@ +# C:\Programs\codegen\src\codegen\cli\commands\tui\main.py +import sys +import os + +# Add the src directory to the path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..")) + +# Import compatibility module first +from codegen.compat import * + +# Try to import the original TUI, fallback to Windows version +try: + from codegen.cli.tui.app import run_tui +except (ImportError, ModuleNotFoundError): + # Try to import the Windows TUI + try: + from codegen.cli.tui.windows_app import run_tui + except (ImportError, ModuleNotFoundError): + # If both fail, create a simple fallback + def run_tui(): + print( + "TUI is not available on this platform. Use 'codegen --help' for available commands." + ) + + +def tui(): + """Run the TUI interface.""" + run_tui() + + +def tui_command(): + """Run the TUI interface.""" + run_tui() diff --git a/src/codegen/cli/commands/update/main.py b/codegen/cli/commands/update/main.py similarity index 100% rename from src/codegen/cli/commands/update/main.py rename to codegen/cli/commands/update/main.py diff --git a/src/codegen/cli/env/constants.py b/codegen/cli/env/constants.py similarity index 100% rename from src/codegen/cli/env/constants.py rename to codegen/cli/env/constants.py diff --git a/src/codegen/cli/env/enums.py b/codegen/cli/env/enums.py similarity index 100% rename from src/codegen/cli/env/enums.py rename to codegen/cli/env/enums.py diff --git a/src/codegen/cli/env/global_env.ipynb b/codegen/cli/env/global_env.ipynb similarity index 100% rename from src/codegen/cli/env/global_env.ipynb rename to codegen/cli/env/global_env.ipynb diff --git a/src/codegen/cli/env/global_env.py b/codegen/cli/env/global_env.py similarity index 100% rename from src/codegen/cli/env/global_env.py rename to codegen/cli/env/global_env.py diff --git a/src/codegen/cli/errors.py b/codegen/cli/errors.py similarity index 100% rename from src/codegen/cli/errors.py rename to codegen/cli/errors.py diff --git a/src/codegen/cli/mcp/README.md b/codegen/cli/mcp/README.md similarity index 100% rename from src/codegen/cli/mcp/README.md rename to codegen/cli/mcp/README.md diff --git a/src/codegen/cli/mcp/__init__.py b/codegen/cli/mcp/__init__.py similarity index 100% rename from src/codegen/cli/mcp/__init__.py rename to codegen/cli/mcp/__init__.py diff --git a/src/codegen/cli/mcp/api_client.py b/codegen/cli/mcp/api_client.py similarity index 100% rename from src/codegen/cli/mcp/api_client.py rename to codegen/cli/mcp/api_client.py diff --git a/src/codegen/cli/mcp/prompts.py b/codegen/cli/mcp/prompts.py similarity index 100% rename from src/codegen/cli/mcp/prompts.py rename to codegen/cli/mcp/prompts.py diff --git a/src/codegen/cli/mcp/resources.py b/codegen/cli/mcp/resources.py similarity index 100% rename from src/codegen/cli/mcp/resources.py rename to codegen/cli/mcp/resources.py diff --git a/src/codegen/cli/mcp/runner.py b/codegen/cli/mcp/runner.py similarity index 100% rename from src/codegen/cli/mcp/runner.py rename to codegen/cli/mcp/runner.py diff --git a/src/codegen/cli/mcp/server.py b/codegen/cli/mcp/server.py similarity index 100% rename from src/codegen/cli/mcp/server.py rename to codegen/cli/mcp/server.py diff --git a/src/codegen/cli/mcp/tools/__init__.py b/codegen/cli/mcp/tools/__init__.py similarity index 100% rename from src/codegen/cli/mcp/tools/__init__.py rename to codegen/cli/mcp/tools/__init__.py diff --git a/src/codegen/cli/mcp/tools/dynamic.py b/codegen/cli/mcp/tools/dynamic.py similarity index 100% rename from src/codegen/cli/mcp/tools/dynamic.py rename to codegen/cli/mcp/tools/dynamic.py diff --git a/src/codegen/cli/mcp/tools/executor.py b/codegen/cli/mcp/tools/executor.py similarity index 100% rename from src/codegen/cli/mcp/tools/executor.py rename to codegen/cli/mcp/tools/executor.py diff --git a/src/codegen/cli/mcp/tools/static.py b/codegen/cli/mcp/tools/static.py similarity index 100% rename from src/codegen/cli/mcp/tools/static.py rename to codegen/cli/mcp/tools/static.py diff --git a/src/codegen/cli/rich/codeblocks.py b/codegen/cli/rich/codeblocks.py similarity index 100% rename from src/codegen/cli/rich/codeblocks.py rename to codegen/cli/rich/codeblocks.py diff --git a/src/codegen/cli/rich/pretty_print.py b/codegen/cli/rich/pretty_print.py similarity index 100% rename from src/codegen/cli/rich/pretty_print.py rename to codegen/cli/rich/pretty_print.py diff --git a/src/codegen/cli/rich/spinners.py b/codegen/cli/rich/spinners.py similarity index 100% rename from src/codegen/cli/rich/spinners.py rename to codegen/cli/rich/spinners.py diff --git a/src/codegen/cli/telemetry/__init__.py b/codegen/cli/telemetry/__init__.py similarity index 100% rename from src/codegen/cli/telemetry/__init__.py rename to codegen/cli/telemetry/__init__.py diff --git a/src/codegen/cli/telemetry/consent.py b/codegen/cli/telemetry/consent.py similarity index 100% rename from src/codegen/cli/telemetry/consent.py rename to codegen/cli/telemetry/consent.py diff --git a/src/codegen/cli/telemetry/debug_exporter.py b/codegen/cli/telemetry/debug_exporter.py similarity index 100% rename from src/codegen/cli/telemetry/debug_exporter.py rename to codegen/cli/telemetry/debug_exporter.py diff --git a/src/codegen/cli/telemetry/exception_logger.py b/codegen/cli/telemetry/exception_logger.py similarity index 100% rename from src/codegen/cli/telemetry/exception_logger.py rename to codegen/cli/telemetry/exception_logger.py diff --git a/src/codegen/cli/telemetry/otel_setup.py b/codegen/cli/telemetry/otel_setup.py similarity index 100% rename from src/codegen/cli/telemetry/otel_setup.py rename to codegen/cli/telemetry/otel_setup.py diff --git a/src/codegen/cli/telemetry/viewer.py b/codegen/cli/telemetry/viewer.py similarity index 100% rename from src/codegen/cli/telemetry/viewer.py rename to codegen/cli/telemetry/viewer.py diff --git a/src/codegen/cli/tui/__init__.py b/codegen/cli/tui/__init__.py similarity index 100% rename from src/codegen/cli/tui/__init__.py rename to codegen/cli/tui/__init__.py diff --git a/src/codegen/cli/tui/agent_detail.py b/codegen/cli/tui/agent_detail.py similarity index 100% rename from src/codegen/cli/tui/agent_detail.py rename to codegen/cli/tui/agent_detail.py diff --git a/src/codegen/cli/tui/app.py b/codegen/cli/tui/app.py similarity index 75% rename from src/codegen/cli/tui/app.py rename to codegen/cli/tui/app.py index b0f6acfc9..d47ffa559 100644 --- a/src/codegen/cli/tui/app.py +++ b/codegen/cli/tui/app.py @@ -2,7 +2,6 @@ import signal import sys -import termios import threading import time import tty @@ -12,6 +11,10 @@ import requests import typer +# Import compatibility layer first +from codegen.compat import termios, tty + +# Rest of the imports from codegen.cli.api.endpoints import API_ENDPOINT from codegen.cli.auth.token_manager import get_current_org_name, get_current_token from codegen.cli.commands.agent.main import pull @@ -29,15 +32,28 @@ class MinimalTUI: def __init__(self): # Log TUI initialization - logger.info("TUI session started", extra={"operation": "tui.init", "component": "minimal_tui"}) + logger.info( + "TUI session started", + extra={"operation": "tui.init", "component": "minimal_tui"}, + ) self.token = get_current_token() self.is_authenticated = bool(self.token) if self.is_authenticated: self.org_id = resolve_org_id() - logger.info("TUI authenticated successfully", extra={"operation": "tui.auth", "org_id": self.org_id, "authenticated": True}) + logger.info( + "TUI authenticated successfully", + extra={ + "operation": "tui.auth", + "org_id": self.org_id, + "authenticated": True, + }, + ) else: - logger.warning("TUI started without authentication", extra={"operation": "tui.auth", "authenticated": False}) + logger.warning( + "TUI started without authentication", + extra={"operation": "tui.auth", "authenticated": False}, + ) self.agent_runs: list[dict[str, Any]] = [] self.selected_index = 0 @@ -65,10 +81,19 @@ def __init__(self): signal.signal(signal.SIGINT, self._signal_handler) # Start background auto-refresh thread (daemon) - self._auto_refresh_thread = threading.Thread(target=self._auto_refresh_loop, daemon=True) + self._auto_refresh_thread = threading.Thread( + target=self._auto_refresh_loop, daemon=True + ) self._auto_refresh_thread.start() - logger.debug("TUI initialization completed", extra={"operation": "tui.init", "tabs": self.tabs, "auto_refresh_interval": self._auto_refresh_interval_seconds}) + logger.debug( + "TUI initialization completed", + extra={ + "operation": "tui.init", + "tabs": self.tabs, + "auto_refresh_interval": self._auto_refresh_interval_seconds, + }, + ) def _auto_refresh_loop(self): """Background loop to auto-refresh recent tab every interval.""" @@ -87,7 +112,11 @@ def _auto_refresh_loop(self): continue try: # Double-check state after acquiring lock - if self.running and self.current_tab == 0 and not self.is_refreshing: + if ( + self.running + and self.current_tab == 0 + and not self.is_refreshing + ): self._background_refresh() finally: self._refresh_lock.release() @@ -102,7 +131,9 @@ def _background_refresh(self): if self._load_agent_runs(): # Preserve selection but clamp to new list bounds if self.agent_runs: - self.selected_index = max(0, min(previous_index, len(self.agent_runs) - 1)) + self.selected_index = max( + 0, min(previous_index, len(self.agent_runs) - 1) + ) else: self.selected_index = 0 finally: @@ -131,7 +162,11 @@ def _format_status_line(self, left_text: str) -> str: # Get organization name org_name = get_current_org_name() if not org_name: - org_name = f"Org {self.org_id}" if hasattr(self, "org_id") and self.org_id else "No Org" + org_name = ( + f"Org {self.org_id}" + if hasattr(self, "org_id") and self.org_id + else "No Org" + ) # Use the same purple color as the Codegen logo purple_color = "\033[38;2;82;19;217m" @@ -150,7 +185,14 @@ def _format_status_line(self, left_text: str) -> str: def _load_agent_runs(self) -> bool: """Load the last 10 agent runs.""" if not self.token or not self.org_id: - logger.warning("Cannot load agent runs - missing auth", extra={"operation": "tui.load_agent_runs", "has_token": bool(self.token), "has_org_id": bool(getattr(self, "org_id", None))}) + logger.warning( + "Cannot load agent runs - missing auth", + extra={ + "operation": "tui.load_agent_runs", + "has_token": bool(self.token), + "has_org_id": bool(getattr(self, "org_id", None)), + }, + ) return False start_time = time.time() @@ -158,7 +200,14 @@ def _load_agent_runs(self) -> bool: # Only log debug info for initial load, not refreshes is_initial_load = not hasattr(self, "_has_loaded_before") if is_initial_load: - logger.debug("Loading agent runs", extra={"operation": "tui.load_agent_runs", "org_id": self.org_id, "is_initial_load": True}) + logger.debug( + "Loading agent runs", + extra={ + "operation": "tui.load_agent_runs", + "org_id": self.org_id, + "is_initial_load": True, + }, + ) try: import requests @@ -168,7 +217,9 @@ def _load_agent_runs(self) -> bool: headers = {"Authorization": f"Bearer {self.token}"} # Get current user ID - user_response = requests.get(f"{API_ENDPOINT.rstrip('/')}/v1/users/me", headers=headers) + user_response = requests.get( + f"{API_ENDPOINT.rstrip('/')}/v1/users/me", headers=headers + ) user_response.raise_for_status() user_data = user_response.json() user_id = user_data.get("id") @@ -182,7 +233,9 @@ def _load_agent_runs(self) -> bool: if user_id: params["user_id"] = user_id - url = f"{API_ENDPOINT.rstrip('/')}/v1/organizations/{self.org_id}/agent/runs" + url = ( + f"{API_ENDPOINT.rstrip('/')}/v1/organizations/{self.org_id}/agent/runs" + ) response = requests.get(url, headers=headers, params=params) response.raise_for_status() response_data = response.json() @@ -216,13 +269,21 @@ def _load_agent_runs(self) -> bool: # Always log errors regardless of refresh vs initial load logger.error( "Failed to load agent runs", - extra={"operation": "tui.load_agent_runs", "org_id": self.org_id, "error_type": type(e).__name__, "error_message": str(e), "duration_ms": duration_ms}, + extra={ + "operation": "tui.load_agent_runs", + "org_id": self.org_id, + "error_type": type(e).__name__, + "error_message": str(e), + "duration_ms": duration_ms, + }, exc_info=True, ) print(f"Error loading agent runs: {e}") return False - def _format_status(self, status: str, agent_run: dict | None = None) -> tuple[str, str]: + def _format_status( + self, status: str, agent_run: dict | None = None + ) -> tuple[str, str]: """Format status with colored indicators matching kanban style.""" # Check if this agent has a merged PR (done status) is_done = False @@ -234,7 +295,10 @@ def _format_status(self, status: str, agent_run: dict | None = None) -> tuple[st break if is_done: - return "\033[38;2;130;226;255m✓\033[0m", "done" # aura blue #82e2ff checkmark for merged PR + return ( + "\033[38;2;130;226;255m✓\033[0m", + "done", + ) # aura blue #82e2ff checkmark for merged PR status_map = { "COMPLETE": "\033[38;2;66;196;153m○\033[0m", # oklch(43.2% 0.095 166.913) ≈ rgb(66,196,153) hollow circle @@ -353,16 +417,22 @@ def _display_agent_list(self): start = 0 end = total else: - start = max(0, min(self.selected_index - window_size // 2, total - window_size)) + start = max( + 0, min(self.selected_index - window_size // 2, total - window_size) + ) end = start + window_size printed_rows = 0 for i in range(start, end): agent_run = self.agent_runs[i] # Highlight selected item - prefix = "→ " if i == self.selected_index and not self.show_action_menu else " " + prefix = ( + "→ " if i == self.selected_index and not self.show_action_menu else " " + ) - status_circle, status_text = self._format_status(agent_run.get("status", "Unknown"), agent_run) + status_circle, status_text = self._format_status( + agent_run.get("status", "Unknown"), agent_run + ) created = self._format_date(agent_run.get("created_at", "Unknown")) summary = agent_run.get("summary", "No summary") or "No summary" @@ -417,7 +487,11 @@ def _display_new_tab(self): if self.input_mode: # Add cursor indicator when in input mode if self.cursor_position <= len(input_display): - input_display = input_display[: self.cursor_position] + "█" + input_display[self.cursor_position :] + input_display = ( + input_display[: self.cursor_position] + + "█" + + input_display[self.cursor_position :] + ) # Handle long input that exceeds box width if len(input_display) > box_width - 4: @@ -426,12 +500,22 @@ def _display_new_tab(self): input_display = input_display[start_pos : start_pos + box_width - 4] # Display full-width input box with simple border like Claude Code - border_style = "\033[37m" if self.input_mode else "\033[90m" # White when active, gray when inactive + border_style = ( + "\033[37m" if self.input_mode else "\033[90m" + ) # White when active, gray when inactive reset = "\033[0m" print(border_style + "┌" + "─" * (box_width - 2) + "┐" + reset) padding = box_width - 4 - len(input_display.replace("█", "")) - print(border_style + "│" + reset + f" {input_display}{' ' * max(0, padding)} " + border_style + "│" + reset) + print( + border_style + + "│" + + reset + + f" {input_display}{' ' * max(0, padding)} " + + border_style + + "│" + + reset + ) print(border_style + "└" + "─" * (box_width - 2) + "┘" + reset) print() @@ -440,21 +524,45 @@ def _display_new_tab(self): def _create_background_agent(self, prompt: str): """Create a background agent run.""" - logger.info("Creating background agent via TUI", extra={"operation": "tui.create_agent", "org_id": getattr(self, "org_id", None), "prompt_length": len(prompt), "client": "tui"}) + logger.info( + "Creating background agent via TUI", + extra={ + "operation": "tui.create_agent", + "org_id": getattr(self, "org_id", None), + "prompt_length": len(prompt), + "client": "tui", + }, + ) if not self.token or not self.org_id: - logger.error("Cannot create agent - missing auth", extra={"operation": "tui.create_agent", "has_token": bool(self.token), "has_org_id": bool(getattr(self, "org_id", None))}) + logger.error( + "Cannot create agent - missing auth", + extra={ + "operation": "tui.create_agent", + "has_token": bool(self.token), + "has_org_id": bool(getattr(self, "org_id", None)), + }, + ) print("\n❌ Not authenticated or no organization configured.") input("Press Enter to continue...") return if not prompt.strip(): - logger.warning("Agent creation cancelled - empty prompt", extra={"operation": "tui.create_agent", "org_id": self.org_id, "prompt_length": len(prompt)}) + logger.warning( + "Agent creation cancelled - empty prompt", + extra={ + "operation": "tui.create_agent", + "org_id": self.org_id, + "prompt_length": len(prompt), + }, + ) print("\n❌ Please enter a prompt.") input("Press Enter to continue...") return - print(f"\n\033[90mCreating agent run with prompt: '{prompt[:50]}{'...' if len(prompt) > 50 else ''}'\033[0m") + print( + f"\n\033[90mCreating agent run with prompt: '{prompt[:50]}{'...' if len(prompt) > 50 else ''}'\033[0m" + ) start_time = time.time() try: @@ -479,7 +587,14 @@ def _create_background_agent(self, prompt: str): duration_ms = (time.time() - start_time) * 1000 logger.info( "Background agent created successfully", - extra={"operation": "tui.create_agent", "org_id": self.org_id, "agent_run_id": run_id, "status": status, "duration_ms": duration_ms, "prompt_length": len(prompt.strip())}, + extra={ + "operation": "tui.create_agent", + "org_id": self.org_id, + "agent_run_id": run_id, + "status": status, + "duration_ms": duration_ms, + "prompt_length": len(prompt.strip()), + }, ) print("\n\033[90mAgent run created successfully!\033[0m") @@ -499,7 +614,14 @@ def _create_background_agent(self, prompt: str): duration_ms = (time.time() - start_time) * 1000 logger.error( "Failed to create background agent", - extra={"operation": "tui.create_agent", "org_id": self.org_id, "error_type": type(e).__name__, "error_message": str(e), "duration_ms": duration_ms, "prompt_length": len(prompt)}, + extra={ + "operation": "tui.create_agent", + "org_id": self.org_id, + "error_type": type(e).__name__, + "error_message": str(e), + "duration_ms": duration_ms, + "prompt_length": len(prompt), + }, exc_info=True, ) print(f"\n❌ Failed to create agent run: {e}") @@ -523,7 +645,9 @@ def build_lines(): else: menu_lines.append(f" \033[90m {option}\033[0m") # Hint line last - menu_lines.append("\033[90m[Enter] select • [↑↓] navigate • [B] back to new tab\033[0m") + menu_lines.append( + "\033[90m[Enter] select • [↑↓] navigate • [B] back to new tab\033[0m" + ) return menu_lines # Initial render @@ -578,7 +702,14 @@ def _display_claude_tab(self): def _pull_agent_branch(self, agent_id: str): """Pull the PR branch for an agent run locally.""" - logger.info("Starting local pull via TUI", extra={"operation": "tui.pull_branch", "agent_id": agent_id, "org_id": getattr(self, "org_id", None)}) + logger.info( + "Starting local pull via TUI", + extra={ + "operation": "tui.pull_branch", + "agent_id": agent_id, + "org_id": getattr(self, "org_id", None), + }, + ) print(f"\n🔄 Pulling PR branch for agent {agent_id}...") print("─" * 50) @@ -589,7 +720,16 @@ def _pull_agent_branch(self, agent_id: str): pull(agent_id=int(agent_id), org_id=self.org_id) duration_ms = (time.time() - start_time) * 1000 - logger.info("Local pull completed successfully", extra={"operation": "tui.pull_branch", "agent_id": agent_id, "org_id": self.org_id, "duration_ms": duration_ms, "success": True}) + logger.info( + "Local pull completed successfully", + extra={ + "operation": "tui.pull_branch", + "agent_id": agent_id, + "org_id": self.org_id, + "duration_ms": duration_ms, + "success": True, + }, + ) except typer.Exit as e: duration_ms = (time.time() - start_time) * 1000 @@ -597,20 +737,40 @@ def _pull_agent_branch(self, agent_id: str): if e.exit_code == 0: logger.info( "Local pull completed via typer exit", - extra={"operation": "tui.pull_branch", "agent_id": agent_id, "org_id": self.org_id, "duration_ms": duration_ms, "exit_code": e.exit_code, "success": True}, + extra={ + "operation": "tui.pull_branch", + "agent_id": agent_id, + "org_id": self.org_id, + "duration_ms": duration_ms, + "exit_code": e.exit_code, + "success": True, + }, ) print("\n✅ Pull completed successfully!") else: logger.error( "Local pull failed via typer exit", - extra={"operation": "tui.pull_branch", "agent_id": agent_id, "org_id": self.org_id, "duration_ms": duration_ms, "exit_code": e.exit_code, "success": False}, + extra={ + "operation": "tui.pull_branch", + "agent_id": agent_id, + "org_id": self.org_id, + "duration_ms": duration_ms, + "exit_code": e.exit_code, + "success": False, + }, ) print(f"\n❌ Pull failed (exit code: {e.exit_code})") except ValueError: duration_ms = (time.time() - start_time) * 1000 logger.error( "Invalid agent ID for pull", - extra={"operation": "tui.pull_branch", "agent_id": agent_id, "org_id": getattr(self, "org_id", None), "duration_ms": duration_ms, "error_type": "invalid_agent_id"}, + extra={ + "operation": "tui.pull_branch", + "agent_id": agent_id, + "org_id": getattr(self, "org_id", None), + "duration_ms": duration_ms, + "error_type": "invalid_agent_id", + }, ) print(f"\n❌ Invalid agent ID: {agent_id}") except Exception as e: @@ -695,7 +855,6 @@ def _get_char(self): try: tty.setcbreak(fd) ch = sys.stdin.read(1) - # Handle escape sequences (arrow keys) if ch == "\x1b": # ESC # Read the rest of the escape sequence synchronously @@ -727,19 +886,25 @@ def _handle_keypress(self, key: str): "operation": "tui.session_end", "org_id": getattr(self, "org_id", None), "reason": "ctrl_c", - "current_tab": self.tabs[self.current_tab] if self.current_tab < len(self.tabs) else "unknown", + "current_tab": self.tabs[self.current_tab] + if self.current_tab < len(self.tabs) + else "unknown", }, ) self.running = False return - elif key.lower() == "q" and not (self.input_mode and self.current_tab == 2): # q only if not typing in new tab + elif key.lower() == "q" and not ( + self.input_mode and self.current_tab == 2 + ): # q only if not typing in new tab logger.info( "TUI session ended by user", extra={ "operation": "tui.session_end", "org_id": getattr(self, "org_id", None), "reason": "quit_key", - "current_tab": self.tabs[self.current_tab] if self.current_tab < len(self.tabs) else "unknown", + "current_tab": self.tabs[self.current_tab] + if self.current_tab < len(self.tabs) + else "unknown", }, ) self.running = False @@ -755,8 +920,12 @@ def _handle_keypress(self, key: str): f"TUI tab switched to {self.tabs[self.current_tab]}", extra={ "operation": "tui.tab_switch", - "from_tab": self.tabs[old_tab] if old_tab < len(self.tabs) else "unknown", - "to_tab": self.tabs[self.current_tab] if self.current_tab < len(self.tabs) else "unknown", + "from_tab": self.tabs[old_tab] + if old_tab < len(self.tabs) + else "unknown", + "to_tab": self.tabs[self.current_tab] + if self.current_tab < len(self.tabs) + else "unknown", }, ) @@ -797,14 +966,21 @@ def _handle_input_mode_keypress(self, key: str): self.input_mode = False # Exit input mode if empty elif key == "\x7f" or key == "\b": # Backspace if self.cursor_position > 0: - self.prompt_input = self.prompt_input[: self.cursor_position - 1] + self.prompt_input[self.cursor_position :] + self.prompt_input = ( + self.prompt_input[: self.cursor_position - 1] + + self.prompt_input[self.cursor_position :] + ) self.cursor_position -= 1 elif key == "\x1b[C": # Right arrow self.cursor_position = min(len(self.prompt_input), self.cursor_position + 1) elif key == "\x1b[D": # Left arrow self.cursor_position = max(0, self.cursor_position - 1) elif len(key) == 1 and key.isprintable(): # Regular character - self.prompt_input = self.prompt_input[: self.cursor_position] + key + self.prompt_input[self.cursor_position :] + self.prompt_input = ( + self.prompt_input[: self.cursor_position] + + key + + self.prompt_input[self.cursor_position :] + ) self.cursor_position += 1 def _handle_action_menu_keypress(self, key: str): @@ -838,7 +1014,9 @@ def _handle_action_menu_keypress(self, key: str): if github_prs and github_prs[0].get("url"): options_count += 1 # "Open PR" - self.action_menu_selection = min(options_count - 1, self.action_menu_selection + 1) + self.action_menu_selection = min( + options_count - 1, self.action_menu_selection + 1 + ) def _handle_recent_keypress(self, key: str): """Handle keypresses in the recent tab.""" @@ -877,7 +1055,13 @@ def _handle_new_tab_keypress(self, key: str): def _handle_dashboard_tab_keypress(self, key: str): """Handle keypresses in the kanban tab.""" if key == "\r" or key == "\n": # Enter - open web kanban - logger.info("Opening web kanban from TUI", extra={"operation": "tui.open_kanban", "org_id": getattr(self, "org_id", None)}) + logger.info( + "Opening web kanban from TUI", + extra={ + "operation": "tui.open_kanban", + "org_id": getattr(self, "org_id", None), + }, + ) try: import webbrowser @@ -885,7 +1069,10 @@ def _handle_dashboard_tab_keypress(self, key: str): webbrowser.open(me_url) # Debug details not needed for successful browser opens except Exception as e: - logger.error("Failed to open kanban in browser", extra={"operation": "tui.open_kanban", "error": str(e)}) + logger.error( + "Failed to open kanban in browser", + extra={"operation": "tui.open_kanban", "error": str(e)}, + ) print(f"\n❌ Failed to open browser: {e}") input("Press Enter to continue...") @@ -896,10 +1083,24 @@ def _handle_claude_tab_keypress(self, key: str): def _run_claude_code(self): """Launch Claude Code with session tracking.""" - logger.info("Launching Claude Code from TUI", extra={"operation": "tui.launch_claude", "org_id": getattr(self, "org_id", None), "source": "tui"}) + logger.info( + "Launching Claude Code from TUI", + extra={ + "operation": "tui.launch_claude", + "org_id": getattr(self, "org_id", None), + "source": "tui", + }, + ) if not self.token or not self.org_id: - logger.error("Cannot launch Claude - missing auth", extra={"operation": "tui.launch_claude", "has_token": bool(self.token), "has_org_id": bool(getattr(self, "org_id", None))}) + logger.error( + "Cannot launch Claude - missing auth", + extra={ + "operation": "tui.launch_claude", + "has_token": bool(self.token), + "has_org_id": bool(getattr(self, "org_id", None)), + }, + ) print("\n❌ Not authenticated or no organization configured.") input("Press Enter to continue...") return @@ -920,25 +1121,54 @@ def _run_claude_code(self): _run_claude_interactive(self.org_id, no_mcp=False) duration_ms = (time.time() - start_time) * 1000 - logger.info("Claude Code session completed via TUI", extra={"operation": "tui.launch_claude", "org_id": self.org_id, "duration_ms": duration_ms, "exit_reason": "normal"}) + logger.info( + "Claude Code session completed via TUI", + extra={ + "operation": "tui.launch_claude", + "org_id": self.org_id, + "duration_ms": duration_ms, + "exit_reason": "normal", + }, + ) except typer.Exit: # Claude Code finished, just continue silently duration_ms = (time.time() - start_time) * 1000 - logger.info("Claude Code session exited via TUI", extra={"operation": "tui.launch_claude", "org_id": self.org_id, "duration_ms": duration_ms, "exit_reason": "typer_exit"}) + logger.info( + "Claude Code session exited via TUI", + extra={ + "operation": "tui.launch_claude", + "org_id": self.org_id, + "duration_ms": duration_ms, + "exit_reason": "typer_exit", + }, + ) pass except Exception as e: duration_ms = (time.time() - start_time) * 1000 logger.error( "Error launching Claude Code from TUI", - extra={"operation": "tui.launch_claude", "org_id": self.org_id, "error_type": type(e).__name__, "error_message": str(e), "duration_ms": duration_ms}, + extra={ + "operation": "tui.launch_claude", + "org_id": self.org_id, + "error_type": type(e).__name__, + "error_message": str(e), + "duration_ms": duration_ms, + }, exc_info=True, ) print(f"\n❌ Unexpected error launching Claude Code: {e}") input("Press Enter to continue...") # Exit the TUI completely - don't return to it - logger.info("TUI session ended - transitioning to Claude", extra={"operation": "tui.session_end", "org_id": getattr(self, "org_id", None), "reason": "claude_launch"}) + logger.info( + "TUI session ended - transitioning to Claude", + extra={ + "operation": "tui.session_end", + "org_id": getattr(self, "org_id", None), + "reason": "claude_launch", + }, + ) sys.exit(0) def _execute_inline_action(self): @@ -970,7 +1200,14 @@ def _execute_inline_action(self): selected_option = options[self.action_menu_selection] logger.info( - "TUI action executed", extra={"operation": "tui.execute_action", "action": selected_option, "agent_id": agent_id, "org_id": getattr(self, "org_id", None), "has_prs": bool(github_prs)} + "TUI action executed", + extra={ + "operation": "tui.execute_action", + "action": selected_option, + "agent_id": agent_id, + "org_id": getattr(self, "org_id", None), + "has_prs": bool(github_prs), + }, ) if selected_option == "open PR": @@ -982,7 +1219,14 @@ def _execute_inline_action(self): # Debug details not needed for successful browser opens # No pause - seamless flow back to collapsed state except Exception as e: - logger.error("Failed to open PR in browser", extra={"operation": "tui.open_pr", "agent_id": agent_id, "error": str(e)}) + logger.error( + "Failed to open PR in browser", + extra={ + "operation": "tui.open_pr", + "agent_id": agent_id, + "error": str(e), + }, + ) print(f"\n❌ Failed to open PR: {e}") input("Press Enter to continue...") # Only pause on errors elif selected_option == "pull locally": @@ -995,7 +1239,14 @@ def _execute_inline_action(self): # Debug details not needed for successful browser opens # No pause - let it flow back naturally to collapsed state except Exception as e: - logger.error("Failed to open trace in browser", extra={"operation": "tui.open_trace", "agent_id": agent_id, "error": str(e)}) + logger.error( + "Failed to open trace in browser", + extra={ + "operation": "tui.open_trace", + "agent_id": agent_id, + "error": str(e), + }, + ) print(f"\n❌ Failed to open browser: {e}") input("Press Enter to continue...") # Only pause on errors @@ -1027,19 +1278,33 @@ def _clear_and_redraw(self): # Show appropriate instructions based on context if self.input_mode and self.current_tab == 2: # new tab input mode - print(f"\n{self._format_status_line('Type your prompt • [Enter] create • [B] cancel • [Tab] switch tabs • [Ctrl+C] quit')}") + print( + f"\n{self._format_status_line('Type your prompt • [Enter] create • [B] cancel • [Tab] switch tabs • [Ctrl+C] quit')}" + ) elif self.input_mode: # other input modes - print(f"\n{self._format_status_line('Type your prompt • [Enter] create • [B] cancel • [Ctrl+C] quit')}") + print( + f"\n{self._format_status_line('Type your prompt • [Enter] create • [B] cancel • [Ctrl+C] quit')}" + ) elif self.show_action_menu: - print(f"\n{self._format_status_line('[Enter] select • [↑↓] navigate • [C] close • [Q] quit')}") + print( + f"\n{self._format_status_line('[Enter] select • [↑↓] navigate • [C] close • [Q] quit')}" + ) elif self.current_tab == 0: # recent - print(f"\n{self._format_status_line('[Tab] switch tabs • (↑↓) navigate • (←→) open/close • [Enter] actions • [R] refresh • [Q] quit')}") + print( + f"\n{self._format_status_line('[Tab] switch tabs • (↑↓) navigate • (←→) open/close • [Enter] actions • [R] refresh • [Q] quit')}" + ) elif self.current_tab == 1: # claude - print(f"\n{self._format_status_line('[Tab] switch tabs • [Enter] launch claude code with telemetry • [Q] quit')}") + print( + f"\n{self._format_status_line('[Tab] switch tabs • [Enter] launch claude code with telemetry • [Q] quit')}" + ) elif self.current_tab == 2: # new - print(f"\n{self._format_status_line('[Tab] switch tabs • [Enter] start typing • [Q] quit')}") + print( + f"\n{self._format_status_line('[Tab] switch tabs • [Enter] start typing • [Q] quit')}" + ) elif self.current_tab == 3: # kanban - print(f"\n{self._format_status_line('[Tab] switch tabs • [Enter] open web kanban • [Q] quit')}") + print( + f"\n{self._format_status_line('[Tab] switch tabs • [Enter] open web kanban • [Q] quit')}" + ) def run(self): """Run the minimal TUI.""" @@ -1083,13 +1348,25 @@ def initial_load(): def run_tui(): """Run the minimal Codegen TUI.""" - logger.info("Starting TUI session", extra={"operation": "tui.start", "component": "run_tui"}) + logger.info( + "Starting TUI session", extra={"operation": "tui.start", "component": "run_tui"} + ) try: tui = MinimalTUI() tui.run() except Exception as e: - logger.error("TUI session crashed", extra={"operation": "tui.crash", "error_type": type(e).__name__, "error_message": str(e)}, exc_info=True) + logger.error( + "TUI session crashed", + extra={ + "operation": "tui.crash", + "error_type": type(e).__name__, + "error_message": str(e), + }, + exc_info=True, + ) raise finally: - logger.info("TUI session ended", extra={"operation": "tui.end", "component": "run_tui"}) + logger.info( + "TUI session ended", extra={"operation": "tui.end", "component": "run_tui"} + ) diff --git a/src/codegen/cli/tui/codegen_theme.tcss b/codegen/cli/tui/codegen_theme.tcss similarity index 100% rename from src/codegen/cli/tui/codegen_theme.tcss rename to codegen/cli/tui/codegen_theme.tcss diff --git a/src/codegen/cli/tui/codegen_tui.tcss b/codegen/cli/tui/codegen_tui.tcss similarity index 100% rename from src/codegen/cli/tui/codegen_tui.tcss rename to codegen/cli/tui/codegen_tui.tcss diff --git a/codegen/cli/tui/widows_app.py b/codegen/cli/tui/widows_app.py new file mode 100644 index 000000000..6a3b98e27 --- /dev/null +++ b/codegen/cli/tui/widows_app.py @@ -0,0 +1,130 @@ +# C:\Programs\codegen\src\codegen\cli\tui\windows_app.py +"""Windows-compatible TUI implementation.""" + +from rich.console import Console +from rich.panel import Panel +from rich.prompt import Prompt +from rich.table import Table + + +class WindowsTUI: + """Simple Windows-compatible TUI.""" + + def __init__(self): + self.console = Console() + self.current_view = "main" + self.data = {} + + def run(self): + """Run the TUI.""" + self.console.print(Panel("Codegen TUI", style="bold blue")) + self.console.print("Press 'h' for help, 'q' to quit") + + while True: + if self.current_view == "main": + self._show_main_view() + elif self.current_view == "help": + self._show_help_view() + elif self.current_view == "agents": + self._show_agents_view() + elif self.current_view == "repos": + self._show_repos_view() + elif self.current_view == "orgs": + self._show_orgs_view() + + try: + cmd = Prompt.ask("\nCommand") + if cmd.lower() == "q": + break + elif cmd.lower() == "h": + self.current_view = "help" + elif cmd.lower() == "m": + self.current_view = "main" + elif cmd.lower() == "a": + self.current_view = "agents" + elif cmd.lower() == "r": + self.current_view = "repos" + elif cmd.lower() == "o": + self.current_view = "orgs" + else: + self.console.print(f"Unknown command: {cmd}") + except KeyboardInterrupt: + break + + def _show_main_view(self): + """Show the main view.""" + self.console.clear() + self.console.print(Panel("Codegen Main Menu", style="bold blue")) + self.console.print("a - View Agents") + self.console.print("r - View Repositories") + self.console.print("o - View Organizations") + self.console.print("h - Help") + self.console.print("q - Quit") + + def _show_help_view(self): + """Show the help view.""" + self.console.clear() + self.console.print(Panel("Codegen Help", style="bold blue")) + self.console.print("a - View Agents - List all available agents") + self.console.print("r - View Repositories - List all repositories") + self.console.print("o - View Organizations - List all organizations") + self.console.print("m - Main menu") + self.console.print("q - Quit") + self.console.print("\nPress 'm' to return to main menu") + + def _show_agents_view(self): + """Show the agents view.""" + self.console.clear() + self.console.print(Panel("Codegen Agents", style="bold blue")) + table = Table(show_header=True, header_style="bold magenta") + table.add_column("ID", style="dim") + table.add_column("Name", style="bold") + table.add_column("Status", style="green") + + # Add sample data + table.add_row("1", "Code Review Agent", "Active") + table.add_row("2", "Bug Fixer Agent", "Active") + table.add_row("3", "Documentation Agent", "Inactive") + + self.console.print(table) + self.console.print("\nPress 'm' to return to main menu") + + def _show_repos_view(self): + """Show the repositories view.""" + self.console.clear() + self.console.print(Panel("Codegen Repositories", style="bold blue")) + table = Table(show_header=True, header_style="bold magenta") + table.add_column("Name", style="bold") + table.add_column("URL", style="cyan") + table.add_column("Status", style="green") + + # Add sample data + table.add_row("my-project", "https://github.com/user/my-project", "Active") + table.add_row( + "another-project", "https://github.com/user/another-project", "Active" + ) + + self.console.print(table) + self.console.print("\nPress 'm' to return to main menu") + + def _show_orgs_view(self): + """Show the organizations view.""" + self.console.clear() + self.console.print(Panel("Codegen Organizations", style="bold blue")) + table = Table(show_header=True, header_style="bold magenta") + table.add_column("ID", style="dim") + table.add_column("Name", style="bold") + table.add_column("Status", style="green") + + # Add sample data + table.add_row("1", "My Organization", "Active") + table.add_row("2", "Another Org", "Inactive") + + self.console.print(table) + self.console.print("\nPress 'm' to return to main menu") + + +def run_tui(): + """Run the Windows-compatible TUI.""" + tui = WindowsTUI() + tui.run() diff --git a/src/codegen/cli/utils/codemod_manager.py b/codegen/cli/utils/codemod_manager.py similarity index 100% rename from src/codegen/cli/utils/codemod_manager.py rename to codegen/cli/utils/codemod_manager.py diff --git a/src/codegen/cli/utils/codemods.py b/codegen/cli/utils/codemods.py similarity index 100% rename from src/codegen/cli/utils/codemods.py rename to codegen/cli/utils/codemods.py diff --git a/src/codegen/cli/utils/count_functions_2.py b/codegen/cli/utils/count_functions_2.py similarity index 100% rename from src/codegen/cli/utils/count_functions_2.py rename to codegen/cli/utils/count_functions_2.py diff --git a/src/codegen/cli/utils/default_code.py b/codegen/cli/utils/default_code.py similarity index 100% rename from src/codegen/cli/utils/default_code.py rename to codegen/cli/utils/default_code.py diff --git a/src/codegen/cli/utils/function_finder.py b/codegen/cli/utils/function_finder.py similarity index 100% rename from src/codegen/cli/utils/function_finder.py rename to codegen/cli/utils/function_finder.py diff --git a/src/codegen/cli/utils/inplace_print.py b/codegen/cli/utils/inplace_print.py similarity index 100% rename from src/codegen/cli/utils/inplace_print.py rename to codegen/cli/utils/inplace_print.py diff --git a/src/codegen/cli/utils/json_schema.py b/codegen/cli/utils/json_schema.py similarity index 100% rename from src/codegen/cli/utils/json_schema.py rename to codegen/cli/utils/json_schema.py diff --git a/src/codegen/cli/utils/notebooks.py b/codegen/cli/utils/notebooks.py similarity index 100% rename from src/codegen/cli/utils/notebooks.py rename to codegen/cli/utils/notebooks.py diff --git a/src/codegen/cli/utils/org.py b/codegen/cli/utils/org.py similarity index 100% rename from src/codegen/cli/utils/org.py rename to codegen/cli/utils/org.py diff --git a/src/codegen/cli/utils/repo.py b/codegen/cli/utils/repo.py similarity index 100% rename from src/codegen/cli/utils/repo.py rename to codegen/cli/utils/repo.py diff --git a/src/codegen/cli/utils/schema.ipynb b/codegen/cli/utils/schema.ipynb similarity index 100% rename from src/codegen/cli/utils/schema.ipynb rename to codegen/cli/utils/schema.ipynb diff --git a/src/codegen/cli/utils/schema.py b/codegen/cli/utils/schema.py similarity index 100% rename from src/codegen/cli/utils/schema.py rename to codegen/cli/utils/schema.py diff --git a/codegen/cli/utils/simple_selector.py b/codegen/cli/utils/simple_selector.py new file mode 100644 index 000000000..575a1149a --- /dev/null +++ b/codegen/cli/utils/simple_selector.py @@ -0,0 +1,210 @@ +"""Simple terminal-based selector utility for Windows.""" + +import signal +import sys +from typing import Any, Optional + + +def _get_char(): + """Get a single character from stdin with Windows fallback.""" + try: + # Try to use msvcrt for Windows + import msvcrt + + return msvcrt.getch().decode("utf-8") + except ImportError: + # Fallback for systems without msvcrt (Unix-like) + try: + import termios + import tty + + fd = sys.stdin.fileno() + old_settings = termios.tcgetattr(fd) + try: + tty.setcbreak(fd) + ch = sys.stdin.read(1) + # Handle escape sequences (arrow keys) + if ch == "\x1b": # ESC + ch2 = sys.stdin.read(1) + if ch2 == "[": + ch3 = sys.stdin.read(1) + return f"\x1b[{ch3}" + else: + return ch + ch2 + return ch + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + except (ImportError, OSError, termios.error): + # Fallback for systems where tty manipulation doesn't work + print("\nUse: ↑(w)/↓(s) navigate, Enter select, q quit") + try: + return input("> ").strip()[:1].lower() or "\n" + except KeyboardInterrupt: + return "q" + + +def simple_select( + title: str, + options: list[dict[str, Any]], + display_key: str = "name", + show_help: bool = True, + allow_cancel: bool = True, +) -> dict[str, Any] | None: + """Show a simple up/down selector for choosing from options. + Args: + title: Title to display above the options + options: List of option dictionaries + display_key: Key to use for displaying option text + show_help: Whether to show navigation help text + allow_cancel: Whether to allow canceling with Esc/q + Returns: + Selected option dictionary or None if canceled + """ + if not options: + print("No options available.") + return None + if len(options) == 1: + # Only one option, select it automatically + return options[0] + selected = 0 + running = True + + # Set up signal handler for Ctrl+C + def signal_handler(signum, frame): + nonlocal running + running = False + print("\n") + sys.exit(0) + + try: + signal.signal(signal.SIGINT, signal_handler) + except (AttributeError, ValueError): + # Signal not available on Windows + pass + + try: + print(f"\n{title}") + print() + # Initial display + for i, option in enumerate(options): + display_text = str(option.get(display_key, f"Option {i + 1}")) + if i == selected: + print(f" > {display_text}") # Simple arrow for selected + else: + print(f" {display_text}") + + if show_help: + print() + help_text = "[Enter] select • [↑↓] navigate" + if allow_cancel: + help_text += " • [q/Esc] cancel" + print(f"{help_text}") + + while running: + # Get input + key = _get_char() + + if key.lower() == "w" or key == "\x1b[A": # Up arrow or W + selected = max(0, selected - 1) + # Redraw options + print("\033[2J\033[H", end="") # Clear screen and move cursor to home + print(f"\n{title}") + print() + for i, option in enumerate(options): + display_text = str(option.get(display_key, f"Option {i + 1}")) + if i == selected: + print(f" > {display_text}") + else: + print(f" {display_text}") + + if show_help: + print() + help_text = "[Enter] select • [↑↓] navigate" + if allow_cancel: + help_text += " • [q/Esc] cancel" + print(f"{help_text}") + + elif key.lower() == "s" or key == "\x1b[B": # Down arrow or S + selected = min(len(options) - 1, selected + 1) + # Redraw options + print("\033[2J\033[H", end="") # Clear screen and move cursor to home + print(f"\n{title}") + print() + for i, option in enumerate(options): + display_text = str(option.get(display_key, f"Option {i + 1}")) + if i == selected: + print(f" > {display_text}") + else: + print(f" {display_text}") + + if show_help: + print() + help_text = "[Enter] select • [↑↓] navigate" + if allow_cancel: + help_text += " • [q/Esc] cancel" + print(f"{help_text}") + + elif key == "\r" or key == "\n": # Enter - select option + return options[selected] + + elif allow_cancel and ( + key.lower() == "q" or key == "\x1b" + ): # q or Esc - cancel + return None + + elif key == "\x03": # Ctrl+C + running = False + break + except KeyboardInterrupt: + return None + finally: + # Restore signal handler + try: + signal.signal(signal.SIGINT, signal.SIG_DFL) + except (AttributeError, ValueError): + # Signal not available on Windows + pass + return None + + +def simple_org_selector( + organizations: list[dict], + current_org_id: Optional[int] = None, + title: str = "Select Organization", +) -> dict | None: + """Show a simple organization selector. + Args: + organizations: List of organization dictionaries with 'id' and 'name' + current_org_id: Currently selected organization ID (for display) + title: Title to show above selector + Returns: + Selected organization dictionary or None if canceled + """ + if not organizations: + print("No organizations available.") + return None + + # Format organizations for display with current indicator + display_orgs = [] + for org in organizations: + org_id = org.get("id") + org_name = org.get("name", f"Organization {org_id}") + # Add current indicator + if org_id == current_org_id: + display_name = f"{org_name} (current)" + else: + display_name = org_name + display_orgs.append( + { + **org, # Keep original org data + "display_name": display_name, + } + ) + + return simple_select( + title=title, + options=display_orgs, + display_key="display_name", + show_help=True, + allow_cancel=True, + ) diff --git a/src/codegen/cli/utils/url.py b/codegen/cli/utils/url.py similarity index 100% rename from src/codegen/cli/utils/url.py rename to codegen/cli/utils/url.py diff --git a/codegen/compat.py b/codegen/compat.py new file mode 100644 index 000000000..89b36e93e --- /dev/null +++ b/codegen/compat.py @@ -0,0 +1,63 @@ +# C:\Programs\codegen\src\codegen\compat.py +"""Compatibility layer for Unix-specific modules on Windows.""" + +import sys +import types + +# Mock termios for Windows +if sys.platform == "win32": + termios = types.ModuleType("termios") + termios.tcgetattr = lambda fd: [0] * 6 + termios.tcsetattr = lambda fd, when, flags: None + termios.TCSANOW = 0 + termios.TCSADRAIN = 0 + termios.TCSAFLUSH = 0 + termios.error = OSError + sys.modules["termios"] = termios + +# Mock tty for Windows +if sys.platform == "win32": + # Create a mock tty module that doesn't import termios + tty = types.ModuleType("tty") + tty.setcbreak = lambda fd: None + tty.setraw = lambda fd: None + # Mock other tty functions if needed + sys.modules["tty"] = tty + +# Mock curses for Windows +if sys.platform == "win32": + curses = types.ModuleType("curses") + curses.noecho = lambda: None + curses.cbreak = lambda: None + curses.curs_set = lambda x: None + curses.KEY_UP = 0 + curses.KEY_DOWN = 0 + curses.KEY_LEFT = 0 + curses.KEY_RIGHT = 0 + curses.A_BOLD = 0 + curses.A_NORMAL = 0 + curses.A_REVERSE = 0 + curses.A_DIM = 0 + curses.A_BLINK = 0 + curses.A_INVIS = 0 + curses.A_PROTECT = 0 + curses.A_CHARTEXT = 0 + curses.A_COLOR = 0 + curses.ERR = -1 + sys.modules["curses"] = curses + +# Mock fcntl for Windows +if sys.platform == "win32": + fcntl = types.ModuleType("fcntl") + fcntl.flock = lambda fd, operation: None + sys.modules["fcntl"] = fcntl + +# Mock signal for Windows +if sys.platform == "win32": + signal = types.ModuleType("signal") + signal.SIGINT = 2 + signal.SIGTERM = 15 + signal.SIG_DFL = 0 + signal.SIG_IGN = 1 + signal.signal = lambda signum, handler: handler + sys.modules["signal"] = signal diff --git a/src/codegen/configs/constants.py b/codegen/configs/constants.py similarity index 100% rename from src/codegen/configs/constants.py rename to codegen/configs/constants.py diff --git a/src/codegen/configs/models/base_config.py b/codegen/configs/models/base_config.py similarity index 100% rename from src/codegen/configs/models/base_config.py rename to codegen/configs/models/base_config.py diff --git a/src/codegen/configs/models/codebase.py b/codegen/configs/models/codebase.py similarity index 100% rename from src/codegen/configs/models/codebase.py rename to codegen/configs/models/codebase.py diff --git a/src/codegen/configs/models/repository.py b/codegen/configs/models/repository.py similarity index 100% rename from src/codegen/configs/models/repository.py rename to codegen/configs/models/repository.py diff --git a/src/codegen/configs/models/secrets.py b/codegen/configs/models/secrets.py similarity index 100% rename from src/codegen/configs/models/secrets.py rename to codegen/configs/models/secrets.py diff --git a/src/codegen/configs/models/telemetry.py b/codegen/configs/models/telemetry.py similarity index 100% rename from src/codegen/configs/models/telemetry.py rename to codegen/configs/models/telemetry.py diff --git a/src/codegen/configs/models/utils.py b/codegen/configs/models/utils.py similarity index 100% rename from src/codegen/configs/models/utils.py rename to codegen/configs/models/utils.py diff --git a/src/codegen/configs/session_manager.py b/codegen/configs/session_manager.py similarity index 100% rename from src/codegen/configs/session_manager.py rename to codegen/configs/session_manager.py diff --git a/src/codegen/configs/user_config.py b/codegen/configs/user_config.py similarity index 100% rename from src/codegen/configs/user_config.py rename to codegen/configs/user_config.py diff --git a/codegen/exports.py b/codegen/exports.py new file mode 100644 index 000000000..6736ae551 --- /dev/null +++ b/codegen/exports.py @@ -0,0 +1,48 @@ +"""Public API exports for the codegen package. + +This file provides convenient imports for commonly used classes. +Since __init__.py is auto-generated by setuptools-scm, we use this +separate file for manual exports. +""" + +# Import ProgrammingLanguage first as it has no dependencies +from codegen.sdk.shared.enums.programming_language import ProgrammingLanguage + +# Agent import may fail due to missing dependencies, make it optional +try: + from codegen.agents.agent import Agent +except ImportError: + # Fallback Agent class for development + class Agent: + def __init__(self, *args, **kwargs): + self.args = args + self.kwargs = kwargs + def __repr__(self): + return "Agent(placeholder - dependencies missing)" + +# SDK imports may fail due to missing dependencies, make them optional +try: + from codegen.sdk.core.codebase import Codebase + from codegen.sdk.core.function import Function +except ImportError: + # Fallback classes for development + class Codebase: + def __init__(self, *args, **kwargs): + self.args = args + self.kwargs = kwargs + def __repr__(self): + return "Codebase(placeholder - dependencies missing)" + + class Function: + def __init__(self, *args, **kwargs): + self.args = args + self.kwargs = kwargs + def __repr__(self): + return "Function(placeholder - dependencies missing)" + +__all__ = [ + "Agent", + "Codebase", + "Function", + "ProgrammingLanguage", +] diff --git a/src/codegen/git/README.md b/codegen/git/README.md similarity index 100% rename from src/codegen/git/README.md rename to codegen/git/README.md diff --git a/src/codegen/git/__init__.py b/codegen/git/__init__.py similarity index 100% rename from src/codegen/git/__init__.py rename to codegen/git/__init__.py diff --git a/src/codegen/git/clients/git_repo_client.py b/codegen/git/clients/git_repo_client.py similarity index 100% rename from src/codegen/git/clients/git_repo_client.py rename to codegen/git/clients/git_repo_client.py diff --git a/src/codegen/git/clients/github_client.py b/codegen/git/clients/github_client.py similarity index 100% rename from src/codegen/git/clients/github_client.py rename to codegen/git/clients/github_client.py diff --git a/src/codegen/git/configs/constants.py b/codegen/git/configs/constants.py similarity index 100% rename from src/codegen/git/configs/constants.py rename to codegen/git/configs/constants.py diff --git a/src/codegen/git/models/codemod_context.py b/codegen/git/models/codemod_context.py similarity index 100% rename from src/codegen/git/models/codemod_context.py rename to codegen/git/models/codemod_context.py diff --git a/src/codegen/git/models/github_named_user_context.py b/codegen/git/models/github_named_user_context.py similarity index 100% rename from src/codegen/git/models/github_named_user_context.py rename to codegen/git/models/github_named_user_context.py diff --git a/src/codegen/git/models/pr_options.py b/codegen/git/models/pr_options.py similarity index 100% rename from src/codegen/git/models/pr_options.py rename to codegen/git/models/pr_options.py diff --git a/src/codegen/git/models/pr_part_context.py b/codegen/git/models/pr_part_context.py similarity index 100% rename from src/codegen/git/models/pr_part_context.py rename to codegen/git/models/pr_part_context.py diff --git a/src/codegen/git/models/pull_request_context.py b/codegen/git/models/pull_request_context.py similarity index 100% rename from src/codegen/git/models/pull_request_context.py rename to codegen/git/models/pull_request_context.py diff --git a/src/codegen/git/py.typed b/codegen/git/py.typed similarity index 100% rename from src/codegen/git/py.typed rename to codegen/git/py.typed diff --git a/codegen/git/repo_operator/local_git_repo.py b/codegen/git/repo_operator/local_git_repo.py new file mode 100644 index 000000000..4a24bc62b --- /dev/null +++ b/codegen/git/repo_operator/local_git_repo.py @@ -0,0 +1,93 @@ +import os +from functools import cached_property +from pathlib import Path + +import giturlparse + +# To: +import sys + +# Add the installed packages to the path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) + +from git import Repo +from git.remote import Remote + +from codegen.git.clients.git_repo_client import GitRepoClient +from codegen.git.schemas.repo_config import RepoConfig +from codegen.git.utils.language import determine_project_language + + +# TODO: merge this with RepoOperator +class LocalGitRepo: + repo_path: Path + + def __init__(self, repo_path: Path): + self.repo_path = repo_path + + @cached_property + def git_cli(self) -> Repo: + return Repo(self.repo_path) + + @cached_property + def name(self) -> str: + return os.path.basename(self.repo_path) + + @cached_property + def owner(self) -> str | None: + if not self.origin_remote: + return None + + parsed = giturlparse.parse(self.origin_remote.url) + return parsed.owner + + @cached_property + def full_name(self) -> str | None: + if not self.origin_remote: + return None + + parsed = giturlparse.parse(self.origin_remote.url) + return f"{parsed.owner}/{parsed.name}" + + @cached_property + def origin_remote(self) -> Remote | None: + """Returns the url of the first remote found on the repo, or None if no remotes are set""" + if self.has_remote(): + return self.git_cli.remote("origin") + return None + + @cached_property + def base_url(self) -> str | None: + if self.origin_remote: + return self.origin_remote.url + return None + + @property + def user_name(self) -> str | None: + with self.git_cli.config_reader() as reader: + if reader.has_option("user", "name"): + return reader.get("user", "name") + return None + + @property + def user_email(self) -> str | None: + with self.git_cli.config_reader() as reader: + if reader.has_option("user", "email"): + return reader.get("user", "email") + return None + + def get_language(self, access_token: str | None = None) -> str: + """Returns the majority language of the repository""" + if access_token is not None: + repo_config = RepoConfig.from_repo_path(repo_path=str(self.repo_path)) + repo_config.full_name = self.full_name + remote_git = GitRepoClient( + repo_config=repo_config, access_token=access_token + ) + if (language := remote_git.repo.language) is not None: + return language.upper() + + return str(determine_project_language(str(self.repo_path))) + + def has_remote(self) -> bool: + return bool(self.git_cli.remotes) diff --git a/src/codegen/git/repo_operator/repo_operator.py b/codegen/git/repo_operator/repo_operator.py similarity index 100% rename from src/codegen/git/repo_operator/repo_operator.py rename to codegen/git/repo_operator/repo_operator.py diff --git a/src/codegen/git/schemas/enums.py b/codegen/git/schemas/enums.py similarity index 100% rename from src/codegen/git/schemas/enums.py rename to codegen/git/schemas/enums.py diff --git a/src/codegen/git/schemas/repo_config.py b/codegen/git/schemas/repo_config.py similarity index 100% rename from src/codegen/git/schemas/repo_config.py rename to codegen/git/schemas/repo_config.py diff --git a/src/codegen/git/utils/clone.py b/codegen/git/utils/clone.py similarity index 100% rename from src/codegen/git/utils/clone.py rename to codegen/git/utils/clone.py diff --git a/src/codegen/git/utils/clone_url.py b/codegen/git/utils/clone_url.py similarity index 100% rename from src/codegen/git/utils/clone_url.py rename to codegen/git/utils/clone_url.py diff --git a/src/codegen/git/utils/codeowner_utils.py b/codegen/git/utils/codeowner_utils.py similarity index 100% rename from src/codegen/git/utils/codeowner_utils.py rename to codegen/git/utils/codeowner_utils.py diff --git a/src/codegen/git/utils/file_utils.py b/codegen/git/utils/file_utils.py similarity index 100% rename from src/codegen/git/utils/file_utils.py rename to codegen/git/utils/file_utils.py diff --git a/src/codegen/git/utils/format.py b/codegen/git/utils/format.py similarity index 100% rename from src/codegen/git/utils/format.py rename to codegen/git/utils/format.py diff --git a/src/codegen/git/utils/language.py b/codegen/git/utils/language.py similarity index 100% rename from src/codegen/git/utils/language.py rename to codegen/git/utils/language.py diff --git a/src/codegen/git/utils/pr_review.py b/codegen/git/utils/pr_review.py similarity index 100% rename from src/codegen/git/utils/pr_review.py rename to codegen/git/utils/pr_review.py diff --git a/src/codegen/git/utils/remote_progress.py b/codegen/git/utils/remote_progress.py similarity index 100% rename from src/codegen/git/utils/remote_progress.py rename to codegen/git/utils/remote_progress.py diff --git a/src/codegen/py.typed b/codegen/py.typed similarity index 100% rename from src/codegen/py.typed rename to codegen/py.typed diff --git a/codegen/sdk/__init__.py b/codegen/sdk/__init__.py new file mode 100644 index 000000000..20ffcd173 --- /dev/null +++ b/codegen/sdk/__init__.py @@ -0,0 +1,58 @@ +"""Codegen SDK - Graph-sitter integration for code analysis and manipulation. + +This module provides the core SDK functionality for working with codebases, +functions, and programming language analysis. +""" + +# Core exports +try: + from codegen.sdk.core.codebase import Codebase + from codegen.sdk.core.function import Function +except ImportError: + # Fallback classes if dependencies are missing + class Codebase: + def __init__(self, *args, **kwargs): + self.args = args + self.kwargs = kwargs + def __repr__(self): + return "Codebase(placeholder - dependencies missing)" + + class Function: + def __init__(self, *args, **kwargs): + self.args = args + self.kwargs = kwargs + def __repr__(self): + return "Function(placeholder - dependencies missing)" + +# Enums +from codegen.sdk.shared.enums.programming_language import ProgrammingLanguage + +# Configuration (mock for now) +class Config: + tree_sitter_enabled = True + ai_features_enabled = True + +config = Config() + +# Analysis functions (mock for now) +def analyze_codebase(*args, **kwargs): + """Analyze a codebase structure and dependencies.""" + return {"status": "mock", "args": args, "kwargs": kwargs} + +def parse_code(*args, **kwargs): + """Parse code using tree-sitter.""" + return {"status": "mock", "args": args, "kwargs": kwargs} + +def generate_code(*args, **kwargs): + """Generate code based on analysis.""" + return {"status": "mock", "args": args, "kwargs": kwargs} + +__all__ = [ + "Codebase", + "Function", + "ProgrammingLanguage", + "config", + "analyze_codebase", + "parse_code", + "generate_code" +] \ No newline at end of file diff --git a/codegen/sdk/_proxy.py b/codegen/sdk/_proxy.py new file mode 100644 index 000000000..290b73886 --- /dev/null +++ b/codegen/sdk/_proxy.py @@ -0,0 +1,30 @@ +import functools +from collections.abc import Callable +from typing import Generic, ParamSpec, TypeVar + +from lazy_object_proxy import Proxy +from lazy_object_proxy.simple import make_proxy_method + +try: + from codegen.sdk.compiled.utils import cached_property +except ModuleNotFoundError: + from functools import cached_property + +T = TypeVar("T") +P = ParamSpec("P") + + +class ProxyProperty(Proxy, Generic[P, T]): + """Lazy proxy that can behave like a method or a property depending on how its used. The class it's proxying should not implement __call__""" + + __factory__: Callable[P, T] + + def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T: + return self.__factory__(*args, **kwargs) + + __repr__ = make_proxy_method(repr) + + +def proxy_property(func: Callable[P, T]) -> cached_property[ProxyProperty[P, T]]: + """Proxy a property so it behaves like a method and property simultaneously. When invoked as a property, results are cached and invalidated using uncache_all""" + return cached_property(lambda obj: ProxyProperty(functools.partial(func, obj))) diff --git a/codegen/sdk/ai/client.py b/codegen/sdk/ai/client.py new file mode 100644 index 000000000..8902a2fa1 --- /dev/null +++ b/codegen/sdk/ai/client.py @@ -0,0 +1,5 @@ +from openai import OpenAI + + +def get_openai_client(key: str) -> OpenAI: + return OpenAI(api_key=key) diff --git a/codegen/sdk/ai/utils.py b/codegen/sdk/ai/utils.py new file mode 100644 index 000000000..b903a9a1a --- /dev/null +++ b/codegen/sdk/ai/utils.py @@ -0,0 +1,17 @@ +import tiktoken + +ENCODERS = { + "gpt-4o": tiktoken.encoding_for_model("gpt-4o"), +} + + +def count_tokens(s: str, model_name: str = "gpt-4o") -> int: + """Uses tiktoken""" + if s is None: + return 0 + enc = ENCODERS.get(model_name, None) + if not enc: + ENCODERS[model_name] = tiktoken.encoding_for_model(model_name) + enc = ENCODERS[model_name] + tokens = enc.encode(s) + return len(tokens) diff --git a/codegen/sdk/cli/README.md b/codegen/sdk/cli/README.md new file mode 100644 index 000000000..101f1b034 --- /dev/null +++ b/codegen/sdk/cli/README.md @@ -0,0 +1,15 @@ +# graph_sitter.cli + +A codegen module that handles all `codegen` CLI commands. + +### Dependencies + +- [codegen.sdk](https://github.com/codegen-sh/graph-sitter/tree/develop/src/codegen/sdk) +- [codegen.shared](https://github.com/codegen-sh/graph-sitter/tree/develop/src/codegen/shared) + +## Best Practices + +- Each folder in `cli` should correspond to a command group. The name of the folder should be the name of the command group. Ex: `task` for codegen task commands. +- The command group folder should have a file called `commands.py` where the CLI group (i.e. function decorated with `@click.group()`) and CLI commands are defined (i.e. functions decorated with ex: `@task.command()`) and if necessary a folder called `utils` (or a single `utils.py`) that holds any additional files with helpers/utilities that are specific to the command group. +- Store utils specific to a CLI command group within its folder. +- Store utils that can be shared across command groups in an appropriate file in cli/utils. If none exists, create a new appropriately named one! diff --git a/src/codegen/shared/__init__.py b/codegen/sdk/cli/__init__.py similarity index 100% rename from src/codegen/shared/__init__.py rename to codegen/sdk/cli/__init__.py diff --git a/codegen/sdk/cli/_env.py b/codegen/sdk/cli/_env.py new file mode 100644 index 000000000..5a12ba1d0 --- /dev/null +++ b/codegen/sdk/cli/_env.py @@ -0,0 +1 @@ +ENV = "" diff --git a/codegen/sdk/cli/auth/constants.py b/codegen/sdk/cli/auth/constants.py new file mode 100644 index 000000000..84849c81c --- /dev/null +++ b/codegen/sdk/cli/auth/constants.py @@ -0,0 +1,13 @@ +from pathlib import Path + +# Base directories +CONFIG_DIR = Path("~/.config/codegen-sh").expanduser() +CODEGEN_DIR = Path(".codegen") +PROMPTS_DIR = CODEGEN_DIR / "prompts" + +# Subdirectories +DOCS_DIR = CODEGEN_DIR / "docs" +EXAMPLES_DIR = CODEGEN_DIR / "examples" + +# Files +AUTH_FILE = CONFIG_DIR / "auth.json" diff --git a/codegen/sdk/cli/auth/session.py b/codegen/sdk/cli/auth/session.py new file mode 100644 index 000000000..650990d0c --- /dev/null +++ b/codegen/sdk/cli/auth/session.py @@ -0,0 +1,87 @@ +from pathlib import Path + +import click +import rich +from github import BadCredentialsException +from github.MainClass import Github + +from codegen.sdk.cli.git.repo import get_git_repo +from codegen.sdk.cli.rich.codeblocks import format_command +from codegen.sdk.configs.constants import CODEGEN_DIR_NAME, ENV_FILENAME +from codegen.sdk.configs.session_manager import session_manager +from codegen.sdk.configs.user_config import UserConfig +from codegen.sdk.git.repo_operator.local_git_repo import LocalGitRepo + + +class CliSession: + """Represents an authenticated codegen session with user and repository context""" + + repo_path: Path + local_git: LocalGitRepo + codegen_dir: Path + config: UserConfig + existing: bool + + def __init__(self, repo_path: Path, git_token: str | None = None) -> None: + if not repo_path.exists() or get_git_repo(repo_path) is None: + rich.print(f"\n[bold red]Error:[/bold red] Path to git repo does not exist at {self.repo_path}") + raise click.Abort() + + self.repo_path = repo_path + self.local_git = LocalGitRepo(repo_path=repo_path) + self.codegen_dir = repo_path / CODEGEN_DIR_NAME + self.config = UserConfig(env_filepath=repo_path / ENV_FILENAME) + self.config.secrets.github_token = git_token or self.config.secrets.github_token + self.existing = session_manager.get_session(repo_path) is not None + + self._initialize() + session_manager.set_active_session(repo_path) + + @classmethod + def from_active_session(cls) -> "CliSession | None": + active_session = session_manager.get_active_session() + if not active_session: + return None + + return cls(active_session) + + def _initialize(self) -> None: + """Initialize the codegen session""" + self._validate() + + self.config.repository.path = self.config.repository.path or str(self.local_git.repo_path) + self.config.repository.owner = self.config.repository.owner or self.local_git.owner + self.config.repository.user_name = self.config.repository.user_name or self.local_git.user_name + self.config.repository.user_email = self.config.repository.user_email or self.local_git.user_email + self.config.repository.language = self.config.repository.language or self.local_git.get_language(access_token=self.config.secrets.github_token).upper() + self.config.save() + + def _validate(self) -> None: + """Validates that the session configuration is correct, otherwise raises an error""" + if not self.codegen_dir.exists(): + self.codegen_dir.mkdir(parents=True, exist_ok=True) + + git_token = self.config.secrets.github_token + if git_token is None: + rich.print("\n[bold yellow]Warning:[/bold yellow] GitHub token not found") + rich.print("To enable full functionality, please set your GitHub token:") + rich.print(format_command("export GITHUB_TOKEN=")) + rich.print("Or pass in as a parameter:") + rich.print(format_command("gs init --token ")) + + if self.local_git.origin_remote is None: + rich.print("\n[bold yellow]Warning:[/bold yellow] No remote found for repository") + rich.print("[white]To enable full functionality, please add a remote to the repository[/white]") + rich.print("\n[dim]To add a remote to the repository:[/dim]") + rich.print(format_command("git remote add origin ")) + + try: + if git_token is not None: + Github(login_or_token=git_token).get_repo(self.local_git.full_name) + except BadCredentialsException: + rich.print(format_command(f"\n[bold red]Error:[/bold red] Invalid GitHub token={git_token} for repo={self.local_git.full_name}")) + rich.print("[white]Please provide a valid GitHub token for this repository.[/white]") + raise click.Abort() + + def __str__(self) -> str: + return f"CliSession(user={self.config.repository.user_name}, repo={self.config.repository.repo_name})" diff --git a/codegen/sdk/cli/cli.py b/codegen/sdk/cli/cli.py new file mode 100644 index 000000000..21b14c840 --- /dev/null +++ b/codegen/sdk/cli/cli.py @@ -0,0 +1,43 @@ +import rich_click as click +from rich.traceback import install + +# Removed reference to non-existent agent module +from codegen.sdk.cli.commands.config.main import config_command +from codegen.sdk.cli.commands.create.main import create_command +from codegen.sdk.cli.commands.init.main import init_command +from codegen.sdk.cli.commands.list.main import list_command +from codegen.sdk.cli.commands.lsp.lsp import lsp_command +from codegen.sdk.cli.commands.notebook.main import notebook_command +from codegen.sdk.cli.commands.reset.main import reset_command +from codegen.sdk.cli.commands.run.main import run_command +from codegen.sdk.cli.commands.start.main import start_command +from codegen.sdk.cli.commands.style_debug.main import style_debug_command +from codegen.sdk.cli.commands.update.main import update_command + +click.rich_click.USE_RICH_MARKUP = True +install(show_locals=True) + + +@click.group() +@click.version_option(prog_name="codegen", message="%(version)s") +def main(): + """codegen.sdk.cli - Transform your code with AI.""" + + +# Wrap commands with error handler +# Removed reference to non-existent agent_command +main.add_command(init_command) +main.add_command(run_command) +main.add_command(create_command) +main.add_command(list_command) +main.add_command(style_debug_command) +main.add_command(notebook_command) +main.add_command(reset_command) +main.add_command(update_command) +main.add_command(config_command) +main.add_command(lsp_command) +main.add_command(start_command) + + +if __name__ == "__main__": + main() diff --git a/codegen/sdk/cli/codemod/convert.py b/codegen/sdk/cli/codemod/convert.py new file mode 100644 index 000000000..f88d570f5 --- /dev/null +++ b/codegen/sdk/cli/codemod/convert.py @@ -0,0 +1,28 @@ +from textwrap import indent + + +def convert_to_cli(input: str, language: str, name: str) -> str: + return f""" +# Run this codemod using `gs run {name}` OR the `run_codemod` MCP tool. +# Important: if you run this as a regular python file, you MUST run it such that +# the base directory './' is the base of your codebase, otherwise it will not work. +import codegen.sdk +from codegen.sdk.core.codebase import Codebase + + +@codegen.sdk.function('{name}') +def run(codebase: Codebase): +{indent(input, " ")} + + +if __name__ == "__main__": + print('Parsing codebase...') + codebase = Codebase("./") + + print('Running function...') + codegen.run(run) +""" + + +def convert_to_ui(input: str) -> str: + return input diff --git a/codegen/sdk/cli/commands/config/main.py b/codegen/sdk/cli/commands/config/main.py new file mode 100644 index 000000000..f692be59b --- /dev/null +++ b/codegen/sdk/cli/commands/config/main.py @@ -0,0 +1,124 @@ +import logging + +import rich +import rich_click as click +from rich.table import Table + +from codegen.sdk.configs.constants import ENV_FILENAME, GLOBAL_ENV_FILE +from codegen.sdk.configs.user_config import UserConfig +from codegen.sdk.shared.path import get_git_root_path + + +@click.group(name="config") +def config_command(): + """Manage codegen configuration.""" + pass + + +@config_command.command(name="list") +def list_command(): + """List current configuration values.""" + + def flatten_dict(data: dict, prefix: str = "") -> dict: + items = {} + for key, value in data.items(): + full_key = f"{prefix}{key}" if prefix else key + if isinstance(value, dict): + # Always include dictionary fields, even if empty + if not value: + items[full_key] = "{}" + items.update(flatten_dict(value, f"{full_key}.")) + else: + items[full_key] = value + return items + + config = _get_user_config() + flat_config = flatten_dict(config.to_dict()) + sorted_items = sorted(flat_config.items(), key=lambda x: x[0]) + + # Create table + table = Table(title="Configuration Values", border_style="blue", show_header=True, title_justify="center") + table.add_column("Key", style="cyan", no_wrap=True) + table.add_column("Value", style="magenta") + + # Group items by prefix + codebase_items = [] + repository_items = [] + other_items = [] + + for key, value in sorted_items: + prefix = key.split("_")[0].lower() + if prefix == "codebase": + codebase_items.append((key, value)) + elif prefix == "repository": + repository_items.append((key, value)) + else: + other_items.append((key, value)) + + # Add codebase section + if codebase_items: + table.add_section() + table.add_row("[bold yellow]Codebase[/bold yellow]", "") + for key, value in codebase_items: + table.add_row(f" {key}", str(value)) + + # Add repository section + if repository_items: + table.add_section() + table.add_row("[bold yellow]Repository[/bold yellow]", "") + for key, value in repository_items: + table.add_row(f" {key}", str(value)) + + # Add other section + if other_items: + table.add_section() + table.add_row("[bold yellow]Other[/bold yellow]", "") + for key, value in other_items: + table.add_row(f" {key}", str(value)) + + rich.print(table) + + +@config_command.command(name="get") +@click.argument("key") +def get_command(key: str): + """Get a configuration value.""" + config = _get_user_config() + if not config.has_key(key): + rich.print(f"[red]Error: Configuration key '{key}' not found[/red]") + return + + value = config.get(key) + + rich.print(f"[cyan]{key}[/cyan]=[magenta]{value}[/magenta]") + + +@config_command.command(name="set") +@click.argument("key") +@click.argument("value") +def set_command(key: str, value: str): + """Set a configuration value and write to .env""" + config = _get_user_config() + if not config.has_key(key): + rich.print(f"[red]Error: Configuration key '{key}' not found[/red]") + return + + cur_value = config.get(key) + if cur_value is None or str(cur_value).lower() != value.lower(): + try: + config.set(key, value) + except Exception as e: + logging.exception(e) + rich.print(f"[red]{e}[/red]") + return + + rich.print(f"[green]Successfully set {key}=[magenta]{value}[/magenta] and saved to {ENV_FILENAME}[/green]") + + +def _get_user_config() -> UserConfig: + if (project_root := get_git_root_path()) is None: + env_filepath = GLOBAL_ENV_FILE + else: + env_filepath = project_root / ENV_FILENAME + + return UserConfig(env_filepath) diff --git a/codegen/sdk/cli/commands/create/main.py b/codegen/sdk/cli/commands/create/main.py new file mode 100644 index 000000000..ec9c4b73d --- /dev/null +++ b/codegen/sdk/cli/commands/create/main.py @@ -0,0 +1,93 @@ +from pathlib import Path + +import rich +import rich_click as click + +from codegen.sdk.cli.auth.session import CliSession +from codegen.sdk.cli.errors import ServerError +from codegen.sdk.cli.rich.codeblocks import format_command, format_path +from codegen.sdk.cli.rich.pretty_print import pretty_print_error +from codegen.sdk.cli.utils.default_code import DEFAULT_CODEMOD +from codegen.sdk.cli.workspace.decorators import requires_init + + +def get_target_paths(name: str, path: Path) -> tuple[Path, Path]: + """Get the target path for the new function file. + + Creates a directory structure like: + .codegen/codemods/function_name/function_name.py + """ + # Convert name to snake case for filename + name_snake = name.lower().replace("-", "_").replace(" ", "_") + + # If path points to a specific file, use its parent directory + if path.suffix == ".py": + base_dir = path.parent + else: + base_dir = path + + # Create path within .codegen/codemods + codemods_dir = base_dir / ".codegen" / "codemods" + function_dir = codemods_dir / name_snake + codemod_path = function_dir / f"{name_snake}.py" + prompt_path = function_dir / f"{name_snake}-system-prompt.txt" + return codemod_path, prompt_path + + +def make_relative(path: Path) -> str: + """Convert a path to a relative path from cwd, handling non-existent paths.""" + try: + return f"./{path.relative_to(Path.cwd())}" + except ValueError: + # If all else fails, just return the full path relative to .codegen + parts = path.parts + if ".codegen" in parts: + idx = parts.index(".codegen") + return "./" + str(Path(*parts[idx:])) + return f"./{path.name}" + + +@click.command(name="create") +@requires_init +@click.argument("name", type=str) +@click.argument("path", type=click.Path(path_type=Path), default=None) +@click.option("--overwrite", is_flag=True, help="Overwrites function if it already exists.") +def create_command(session: CliSession, name: str, path: Path | None, overwrite: bool = False): + """Create a new codegen function. + + NAME is the name/label for the function + PATH is where to create the function (default: current directory) + """ + # Get the target path for the function + codemod_path, prompt_path = get_target_paths(name, path or Path.cwd()) + + # Check if file exists + if codemod_path.exists() and not overwrite: + rel_path = make_relative(codemod_path) + pretty_print_error(f"File already exists at {format_path(rel_path)}\n\nTo overwrite the file:\n{format_command(f'gs create {name} {rel_path} --overwrite')}") + return + + code = None + try: + # Use default implementation + code = DEFAULT_CODEMOD.format(name=name) + + # Create the target directory if needed + codemod_path.parent.mkdir(parents=True, exist_ok=True) + + # Write the function code + codemod_path.write_text(code) + + except (ServerError, ValueError) as e: + raise click.ClickException(str(e)) + + # Success message + rich.print(f"\n✅ {'Overwrote' if overwrite and codemod_path.exists() else 'Created'} function '{name}'") + rich.print("") + rich.print("📁 Files Created:") + rich.print(f" [dim]Function:[/dim] {make_relative(codemod_path)}") + + # Next steps + rich.print("\n[bold]What's next?[/bold]\n") + rich.print("1. Review and edit the function to customize its behavior") + rich.print(f"2. Run it with: \n{format_command(f'gs run {name}')}") diff --git a/codegen/sdk/cli/commands/init/main.py b/codegen/sdk/cli/commands/init/main.py new file mode 100644 index 000000000..bb71caf73 --- /dev/null +++ b/codegen/sdk/cli/commands/init/main.py @@ -0,0 +1,50 @@ +import sys +from pathlib import Path + +import rich +import rich_click as click + +from codegen.sdk.cli.auth.session import CliSession +from codegen.sdk.cli.commands.init.render import get_success_message +from codegen.sdk.cli.rich.codeblocks import format_command +from codegen.sdk.cli.workspace.initialize_workspace import initialize_codegen +from codegen.sdk.shared.path import get_git_root_path + + +@click.command(name="init") +@click.option("--path", type=str, help="Path within a git repository. Defaults to the current directory.") +@click.option("--token", type=str, help="Access token for the git repository. Required for full functionality.") +@click.option("--language", type=click.Choice(["python", "typescript"], case_sensitive=False), help="Override automatic language detection") +def init_command(path: str | None = None, token: str | None = None, language: str | None = None): + """Initialize or update the Graph-sitter folder.""" + # Print a message if not in a git repo + path = Path.cwd() if path is None else Path(path) + repo_path = get_git_root_path(path) + rich.print(f"Found git repository at: {repo_path}") + + if repo_path is None: + rich.print(f"\n[bold red]Error:[/bold red] Path={path} is not in a git repository") + rich.print("[white]Please run this command from within a git repository.[/white]") + rich.print("\n[dim]To initialize a new git repository:[/dim]") + rich.print(format_command("git init")) + rich.print(format_command("gs init")) + sys.exit(1) + + session = CliSession(repo_path=repo_path, git_token=token) + if language: + session.config.repository.language = language.upper() + session.config.save() + + action = "Updating" if session.existing else "Initializing" + codegen_dir, docs_dir, examples_dir = initialize_codegen(status=action, session=session) + + # Print success message + rich.print(f"✅ {action} complete\n") + rich.print(get_success_message(codegen_dir, docs_dir, examples_dir)) + + # Print next steps + rich.print("\n[bold]What's next?[/bold]\n") + rich.print("1. Create a function:") + rich.print(format_command('gs create my-function . -d "describe what you want to do"')) + rich.print("2. Run it:") + rich.print(format_command("gs run my-function --apply-local")) diff --git a/codegen/sdk/cli/commands/init/render.py b/codegen/sdk/cli/commands/init/render.py new file mode 100644 index 000000000..7c7ee42ed --- /dev/null +++ b/codegen/sdk/cli/commands/init/render.py @@ -0,0 +1,9 @@ +from pathlib import Path + + +def get_success_message(codegen_dir: Path, docs_dir: Path, examples_dir: Path) -> str: + """Get the success message to display after initialization.""" + return """📁 .codegen configuration folder created: + [dim]codemods/[/dim] Your codemod implementations + [dim].venv/[/dim] Python virtual environment (gitignored) + [dim]codegen-system-prompt.txt[/dim] AI system prompt (gitignored)""" diff --git a/codegen/sdk/cli/commands/list/main.py b/codegen/sdk/cli/commands/list/main.py new file mode 100644 index 000000000..e03c998b5 --- /dev/null +++ b/codegen/sdk/cli/commands/list/main.py @@ -0,0 +1,39 @@ +from pathlib import Path + +import rich +import rich_click as click +from rich.table import Table + +from codegen.sdk.cli.rich.codeblocks import format_codeblock, format_command +from codegen.sdk.cli.utils.codemod_manager import CodemodManager + + +@click.command(name="list") +def list_command(): + """List available codegen functions.""" + functions = CodemodManager.get_decorated() + if functions: + table = Table(title="Graph-sitter Functions", border_style="blue") + table.add_column("Name", style="cyan") + table.add_column("Type", style="magenta") + table.add_column("Path", style="dim") + table.add_column("Subdirectories", style="dim") + table.add_column("Language", style="dim") + + for func in functions: + func_type = "Webhook" if func.lint_mode else "Function" + table.add_row( + func.name, + func_type, + str(func.filepath.relative_to(Path.cwd())) if func.filepath else "", + ", ".join(func.subdirectories) if func.subdirectories else "", + func.language or "", + ) + + rich.print(table) + rich.print("\nRun a function with:") + rich.print(format_command("gs run