From b90577c936c67a0e22f0bd15e6af1b62d44b322f Mon Sep 17 00:00:00 2001 From: Zeeeepa Date: Wed, 3 Sep 2025 14:52:45 +0100 Subject: [PATCH 01/23] d d --- pyproject.toml | 20 ++------------------ src/codegen/__main__.py | 5 +++++ 2 files changed, 7 insertions(+), 18 deletions(-) create mode 100644 src/codegen/__main__.py diff --git a/pyproject.toml b/pyproject.toml index 738e2d43f..d5212ecd6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,28 +35,22 @@ dependencies = [ "sentry-sdk==2.29.1", "humanize>=4.10.0", ] - # renovate: datasource=python-version depName=python license = { text = "Apache-2.0" } classifiers = [ "Development Status :: 4 - Beta", - "Environment :: Console", "Environment :: MacOS X", - "Intended Audience :: Developers", "Intended Audience :: Information Technology", - "License :: OSI Approved", "License :: OSI Approved :: Apache Software License", - "Operating System :: OS Independent", - + "Operating System :: Microsoft :: Windows", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", - "Topic :: Software Development", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Code Generators", @@ -75,7 +69,6 @@ keywords = [ [project.scripts] codegen = "codegen.cli.cli:main" cg = "codegen.cli.cli:main" - [project.optional-dependencies] types = [] [tool.uv] @@ -115,17 +108,14 @@ dev-dependencies = [ "pytest-lsp>=1.0.0b1", "codegen-api-client>=1.0.0", ] - [tool.uv.workspace] exclude = ["codegen-examples"] - [tool.coverage.run] branch = true concurrency = ["multiprocessing", "thread"] parallel = true sigterm = true - [tool.coverage.report] skip_covered = true skip_empty = true @@ -141,7 +131,6 @@ exclude_also = [ # Don't complain about abstract methods, they aren't run: "@(abc\\.)?abstractmethod", ] - [tool.coverage.html] show_contexts = true [tool.coverage.json] @@ -154,7 +143,6 @@ enableExperimentalFeatures = true pythonpath = "." norecursedirs = "repos expected" # addopts = -v --cov=app --cov-report=term - addopts = "--dist=loadgroup --junitxml=build/test-results/test/TEST.xml --strict-config --import-mode=importlib --cov-context=test --cov-config=pyproject.toml -p no:doctest" filterwarnings = """ ignore::DeprecationWarning:botocore.*: @@ -172,7 +160,6 @@ asyncio_default_fixture_loop_scope = "function" requires = ["hatchling>=1.26.3", "hatch-vcs>=0.4.0", "setuptools-scm>=8.0.0"] build-backend = "hatchling.build" - [tool.deptry] extend_exclude = [".*/eval/test_files/.*.py", ".*conftest.py"] pep621_dev_dependency_groups = ["types"] @@ -183,7 +170,6 @@ DEP002 = [ ] DEP003 = [] DEP004 = "pytest" - [tool.deptry.package_module_name_map] PyGithub = ["github"] GitPython = ["git"] @@ -192,7 +178,6 @@ pydantic-settings = ["pydantic_settings"] datamodel-code-generator = ["datamodel_code_generator"] sentry-sdk = ["sentry_sdk"] - [tool.semantic_release] assets = [] build_command_env = [] @@ -204,8 +189,7 @@ allow_zero_version = true repo_dir = "." no_git_verify = false tag_format = "v{version}" - [tool.semantic_release.branches.develop] match = "develop" prerelease_token = "rc" -prerelease = false +prerelease = false \ No newline at end of file diff --git a/src/codegen/__main__.py b/src/codegen/__main__.py new file mode 100644 index 000000000..527cfcb81 --- /dev/null +++ b/src/codegen/__main__.py @@ -0,0 +1,5 @@ +# C:\Programs\codegen\src\codegen\__main__.py +from codegen.cli.cli import main + +if __name__ == "__main__": + main() From 9f2f5fec929c72c1d41e38f158080dbbeb82132d Mon Sep 17 00:00:00 2001 From: Zeeeepa Date: Wed, 3 Sep 2025 15:52:55 +0100 Subject: [PATCH 02/23] up up --- src/codegen/__main__.py | 22 +- src/codegen/cli/cli.py | 73 +++- src/codegen/cli/commands/tui/main.py | 31 +- src/codegen/cli/tui/app.py | 405 +++++++++++++++--- src/codegen/cli/tui/widows_app.py | 130 ++++++ src/codegen/cli/utils/simple_selector.py | 166 ++++--- src/codegen/compat.py | 63 +++ .../git/repo_operator/local_git_repo.py | 11 +- 8 files changed, 756 insertions(+), 145 deletions(-) create mode 100644 src/codegen/cli/tui/widows_app.py create mode 100644 src/codegen/compat.py diff --git a/src/codegen/__main__.py b/src/codegen/__main__.py index 527cfcb81..07b1afa45 100644 --- a/src/codegen/__main__.py +++ b/src/codegen/__main__.py @@ -1,5 +1,25 @@ # C:\Programs\codegen\src\codegen\__main__.py -from codegen.cli.cli import main +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/cli/cli.py b/src/codegen/cli/cli.py index ab19f73ae..070798df3 100644 --- a/src/codegen/cli/cli.py +++ b/src/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/tui/main.py b/src/codegen/cli/commands/tui/main.py index 174d10634..ec41ed8f4 100644 --- a/src/codegen/cli/commands/tui/main.py +++ b/src/codegen/cli/commands/tui/main.py @@ -1,12 +1,33 @@ -"""TUI command for the Codegen CLI.""" +# C:\Programs\codegen\src\codegen\cli\commands\tui\main.py +import sys +import os -from codegen.cli.tui.app import run_tui +# 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(): - """Launch the Codegen TUI interface.""" + """Run the TUI interface.""" run_tui() -if __name__ == "__main__": - tui() +def tui_command(): + """Run the TUI interface.""" + run_tui() diff --git a/src/codegen/cli/tui/app.py b/src/codegen/cli/tui/app.py index b0f6acfc9..d47ffa559 100644 --- a/src/codegen/cli/tui/app.py +++ b/src/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/widows_app.py b/src/codegen/cli/tui/widows_app.py new file mode 100644 index 000000000..6a3b98e27 --- /dev/null +++ b/src/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/simple_selector.py b/src/codegen/cli/utils/simple_selector.py index 65ee04842..575a1149a 100644 --- a/src/codegen/cli/utils/simple_selector.py +++ b/src/codegen/cli/utils/simple_selector.py @@ -1,62 +1,71 @@ -"""Simple terminal-based selector utility.""" +"""Simple terminal-based selector utility for Windows.""" import signal import sys -import termios -import tty -from typing import Any +from typing import Any, Optional def _get_char(): - """Get a single character from stdin, handling arrow keys.""" + """Get a single character from stdin with Windows fallback.""" try: - 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" - + # Try to use msvcrt for Windows + import msvcrt -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: + 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 @@ -67,86 +76,107 @@ def signal_handler(signum, frame): print("\n") sys.exit(0) - signal.signal(signal.SIGINT, signal_handler) + 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" \033[37m→ {display_text}\033[0m") # White for selected + print(f" > {display_text}") # Simple arrow for selected else: - print(f" \033[90m {display_text}\033[0m") + print(f" {display_text}") if show_help: print() help_text = "[Enter] select • [↑↓] navigate" if allow_cancel: help_text += " • [q/Esc] cancel" - print(f"\033[90m{help_text}\033[0m") + print(f"{help_text}") while running: # Get input key = _get_char() - if key == "\x1b[A" or key.lower() == "w": # Up arrow or W + if key.lower() == "w" or key == "\x1b[A": # Up arrow or W selected = max(0, selected - 1) - # Redraw options only - lines_to_move = len(options) + (2 if show_help else 0) - print(f"\033[{lines_to_move}A", end="") # Move cursor up to start of options + # 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" \033[37m→ {display_text}\033[0m\033[K") # White for selected, clear to end of line + print(f" > {display_text}") else: - print(f" \033[90m {display_text}\033[0m\033[K") # Clear to end of line + print(f" {display_text}") + if show_help: - print("\033[K") # Clear help line - print(f"\033[90m{help_text}\033[0m\033[K") # Redraw help + print() + help_text = "[Enter] select • [↑↓] navigate" + if allow_cancel: + help_text += " • [q/Esc] cancel" + print(f"{help_text}") - elif key == "\x1b[B" or key.lower() == "s": # Down arrow or S + elif key.lower() == "s" or key == "\x1b[B": # Down arrow or S selected = min(len(options) - 1, selected + 1) - # Redraw options only - lines_to_move = len(options) + (2 if show_help else 0) - print(f"\033[{lines_to_move}A", end="") # Move cursor up to start of options + # 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" \033[37m→ {display_text}\033[0m\033[K") # White for selected, clear to end of line + print(f" > {display_text}") else: - print(f" \033[90m {display_text}\033[0m\033[K") # Clear to end of line + print(f" {display_text}") + if show_help: - print("\033[K") # Clear help line - print(f"\033[90m{help_text}\033[0m\033[K") # Redraw 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 + + 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 - signal.signal(signal.SIGINT, signal.SIG_DFL) - + 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: int | None = None, title: str = "Select Organization") -> dict | 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 """ @@ -159,13 +189,11 @@ def simple_org_selector(organizations: list[dict], current_org_id: int | None = 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 @@ -173,4 +201,10 @@ def simple_org_selector(organizations: list[dict], current_org_id: int | None = } ) - return simple_select(title=title, options=display_orgs, display_key="display_name", show_help=True, allow_cancel=True) + return simple_select( + title=title, + options=display_orgs, + display_key="display_name", + show_help=True, + allow_cancel=True, + ) diff --git a/src/codegen/compat.py b/src/codegen/compat.py new file mode 100644 index 000000000..89b36e93e --- /dev/null +++ b/src/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/git/repo_operator/local_git_repo.py b/src/codegen/git/repo_operator/local_git_repo.py index a5c4acea3..4a24bc62b 100644 --- a/src/codegen/git/repo_operator/local_git_repo.py +++ b/src/codegen/git/repo_operator/local_git_repo.py @@ -3,6 +3,13 @@ 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 @@ -74,7 +81,9 @@ def get_language(self, access_token: str | None = None) -> str: 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) + remote_git = GitRepoClient( + repo_config=repo_config, access_token=access_token + ) if (language := remote_git.repo.language) is not None: return language.upper() From bac9125440d5c6495308765e4a6932ca6a92a77e Mon Sep 17 00:00:00 2001 From: "codegen-sh[bot]" <131295404+codegen-sh[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 16:02:17 +0000 Subject: [PATCH 03/23] Integrate graph-sitter SDK with codemods and gsbuild - Cloned graph-sitter repository and integrated core modules - Added codemods and gsbuild folders to SDK structure - Moved integrated SDK to src/codegen/sdk/ - Updated all internal imports from graph_sitter to codegen.sdk - Removed type ignore comments from exports.py - SDK now provides Codebase and Function classes as expected Co-authored-by: Zeeeepa --- src/codegen.backup/__init__.py | 22 + src/codegen.backup/__main__.py | 25 + src/codegen.backup/agents/README.md | 124 + src/codegen.backup/agents/__init__.py | 5 + src/codegen.backup/agents/agent.py | 100 + src/codegen.backup/agents/constants.py | 6 + src/codegen.backup/cli/README.md | 15 + src/codegen.backup/cli/__init__.py | 0 src/codegen.backup/cli/_env.py | 1 + src/codegen.backup/cli/api/client.py | 89 + src/codegen.backup/cli/api/endpoints.py | 20 + src/codegen.backup/cli/api/modal.py | 25 + src/codegen.backup/cli/api/schemas.py | 257 + src/codegen.backup/cli/api/webapp_routes.py | 4 + src/codegen.backup/cli/auth/constants.py | 13 + src/codegen.backup/cli/auth/decorators.py | 46 + src/codegen.backup/cli/auth/login.py | 85 + src/codegen.backup/cli/auth/session.ipynb | 43 + src/codegen.backup/cli/auth/session.py | 93 + .../cli/auth/token_manager.ipynb | 38 + src/codegen.backup/cli/auth/token_manager.py | 460 + src/codegen.backup/cli/claude/__init__.py | 1 + src/codegen.backup/cli/cli.py | 154 + .../cli/commands/agent/__init__.py | 1 + src/codegen.backup/cli/commands/agent/main.py | 471 + .../cli/commands/agents/__init__.py | 1 + .../cli/commands/agents/main.py | 165 + .../cli/commands/claude/__init__.py | 2 + .../cli/commands/claude/claude_log_utils.py | 126 + .../cli/commands/claude/claude_log_watcher.py | 274 + .../cli/commands/claude/claude_session_api.py | 214 + .../config/claude_session_active_hook.py | 67 + .../claude/config/claude_session_hook.py | 84 + .../claude/config/claude_session_stop_hook.py | 68 + .../cli/commands/claude/config/mcp_setup.py | 67 + .../cli/commands/claude/hooks.py | 219 + .../cli/commands/claude/main.py | 403 + .../cli/commands/claude/quiet_console.py | 33 + .../cli/commands/claude/utils.py | 39 + .../cli/commands/config/main.py | 131 + .../cli/commands/config/telemetry.py | 156 + src/codegen.backup/cli/commands/init/main.py | 87 + .../cli/commands/init/render.py | 9 + .../cli/commands/integrations/__init__.py | 1 + .../cli/commands/integrations/main.py | 146 + src/codegen.backup/cli/commands/login/main.py | 37 + .../cli/commands/logout/main.py | 21 + .../cli/commands/org/__init__.py | 5 + src/codegen.backup/cli/commands/org/main.py | 129 + src/codegen.backup/cli/commands/org/tui.py | 325 + .../cli/commands/profile/main.py | 192 + .../cli/commands/repo/__init__.py | 5 + src/codegen.backup/cli/commands/repo/main.py | 159 + src/codegen.backup/cli/commands/repo/tui.py | 303 + .../cli/commands/style_debug/main.py | 19 + .../cli/commands/tools/__init__.py | 1 + src/codegen.backup/cli/commands/tools/main.py | 101 + .../cli/commands/tui/__init__.py | 1 + src/codegen.backup/cli/commands/tui/main.py | 33 + .../cli/commands/update/main.py | 62 + src/codegen.backup/cli/env/constants.py | 3 + src/codegen.backup/cli/env/enums.py | 7 + src/codegen.backup/cli/env/global_env.ipynb | 70 + src/codegen.backup/cli/env/global_env.py | 61 + src/codegen.backup/cli/errors.py | 60 + src/codegen.backup/cli/mcp/README.md | 40 + src/codegen.backup/cli/mcp/__init__.py | 1 + src/codegen.backup/cli/mcp/api_client.py | 54 + src/codegen.backup/cli/mcp/prompts.py | 14 + src/codegen.backup/cli/mcp/resources.py | 18 + src/codegen.backup/cli/mcp/runner.py | 43 + src/codegen.backup/cli/mcp/server.py | 18 + src/codegen.backup/cli/mcp/tools/__init__.py | 1 + src/codegen.backup/cli/mcp/tools/dynamic.py | 174 + src/codegen.backup/cli/mcp/tools/executor.py | 40 + src/codegen.backup/cli/mcp/tools/static.py | 176 + src/codegen.backup/cli/rich/codeblocks.py | 42 + src/codegen.backup/cli/rich/pretty_print.py | 66 + src/codegen.backup/cli/rich/spinners.py | 29 + src/codegen.backup/cli/telemetry/__init__.py | 17 + src/codegen.backup/cli/telemetry/consent.py | 105 + .../cli/telemetry/debug_exporter.py | 166 + .../cli/telemetry/exception_logger.py | 176 + .../cli/telemetry/otel_setup.py | 300 + src/codegen.backup/cli/telemetry/viewer.py | 129 + src/codegen.backup/cli/tui/__init__.py | 1 + src/codegen.backup/cli/tui/agent_detail.py | 309 + src/codegen.backup/cli/tui/app.py | 1372 ++ src/codegen.backup/cli/tui/codegen_theme.tcss | 185 + src/codegen.backup/cli/tui/codegen_tui.tcss | 74 + src/codegen.backup/cli/tui/widows_app.py | 130 + .../cli/utils/codemod_manager.py | 146 + src/codegen.backup/cli/utils/codemods.py | 41 + .../cli/utils/count_functions_2.py | 1 + src/codegen.backup/cli/utils/default_code.py | 20 + .../cli/utils/function_finder.py | 316 + src/codegen.backup/cli/utils/inplace_print.py | 27 + src/codegen.backup/cli/utils/json_schema.py | 41 + src/codegen.backup/cli/utils/notebooks.py | 216 + src/codegen.backup/cli/utils/org.py | 117 + src/codegen.backup/cli/utils/repo.py | 236 + src/codegen.backup/cli/utils/schema.ipynb | 74 + src/codegen.backup/cli/utils/schema.py | 29 + .../cli/utils/simple_selector.py | 210 + src/codegen.backup/cli/utils/url.py | 57 + src/codegen.backup/compat.py | 63 + src/codegen.backup/configs/constants.py | 20 + .../configs/models/base_config.py | 57 + src/codegen.backup/configs/models/codebase.py | 43 + .../configs/models/repository.py | 39 + src/codegen.backup/configs/models/secrets.py | 16 + .../configs/models/telemetry.py | 29 + src/codegen.backup/configs/models/utils.py | 9 + src/codegen.backup/configs/session_manager.py | 64 + src/codegen.backup/configs/user_config.py | 69 + src/codegen.backup/exports.py | 18 + src/codegen.backup/git/README.md | 8 + src/codegen.backup/git/__init__.py | 0 .../git/clients/git_repo_client.py | 463 + .../git/clients/github_client.py | 46 + src/codegen.backup/git/configs/constants.py | 5 + .../git/models/codemod_context.py | 36 + .../git/models/github_named_user_context.py | 12 + src/codegen.backup/git/models/pr_options.py | 20 + .../git/models/pr_part_context.py | 12 + .../git/models/pull_request_context.py | 49 + src/codegen.backup/git/py.typed | 0 .../git/repo_operator/local_git_repo.py | 93 + .../git/repo_operator/repo_operator.py | 920 ++ src/codegen.backup/git/schemas/enums.py | 35 + src/codegen.backup/git/schemas/repo_config.py | 56 + src/codegen.backup/git/utils/clone.py | 61 + src/codegen.backup/git/utils/clone_url.py | 25 + .../git/utils/codeowner_utils.py | 50 + src/codegen.backup/git/utils/file_utils.py | 73 + src/codegen.backup/git/utils/format.py | 24 + src/codegen.backup/git/utils/language.py | 183 + src/codegen.backup/git/utils/pr_review.py | 171 + .../git/utils/remote_progress.py | 32 + src/codegen.backup/py.typed | 0 src/codegen.backup/shared/README.md | 8 + src/codegen.backup/shared/__init__.py | 0 .../shared/compilation/README.md | 8 + .../compilation/codeblock_validation.py | 14 + .../shared/compilation/exception_utils.py | 51 + .../compilation/function_compilation.py | 67 + .../compilation/function_construction.py | 55 + .../shared/compilation/function_imports.py | 200 + .../shared/compilation/string_to_code.py | 109 + src/codegen.backup/shared/decorators/docs.py | 91 + .../shared/enums/programming_language.py | 8 + src/codegen.backup/shared/exceptions/api.py | 2 + .../shared/exceptions/compilation.py | 10 + .../shared/exceptions/control_flow.py | 28 + .../shared/logging/get_logger.py | 157 + src/codegen.backup/shared/network/port.py | 17 + src/codegen.backup/shared/path.py | 15 + .../shared/performance/memory_utils.py | 20 + .../shared/performance/stopwatch_utils.py | 52 + .../shared/performance/time_utils.py | 11 + src/codegen.backup/shared/string/csv_utils.py | 21 + src/codegen/exports.py | 4 +- src/codegen/sdk/_proxy.py | 30 + src/codegen/sdk/ai/client.py | 5 + src/codegen/sdk/ai/utils.py | 17 + src/codegen/sdk/cli/README.md | 15 + src/codegen/sdk/cli/__init__.py | 0 src/codegen/sdk/cli/_env.py | 1 + src/codegen/sdk/cli/auth/constants.py | 13 + src/codegen/sdk/cli/auth/session.py | 87 + src/codegen/sdk/cli/cli.py | 43 + src/codegen/sdk/cli/codemod/convert.py | 28 + src/codegen/sdk/cli/commands/config/main.py | 124 + src/codegen/sdk/cli/commands/create/main.py | 93 + src/codegen/sdk/cli/commands/init/main.py | 50 + src/codegen/sdk/cli/commands/init/render.py | 9 + src/codegen/sdk/cli/commands/list/main.py | 39 + src/codegen/sdk/cli/commands/lsp/lsp.py | 18 + src/codegen/sdk/cli/commands/notebook/main.py | 42 + src/codegen/sdk/cli/commands/reset/main.py | 102 + src/codegen/sdk/cli/commands/run/main.py | 60 + src/codegen/sdk/cli/commands/run/render.py | 49 + .../sdk/cli/commands/run/run_daemon.py | 87 + src/codegen/sdk/cli/commands/run/run_local.py | 125 + src/codegen/sdk/cli/commands/start/Dockerfile | 71 + .../cli/commands/start/docker_container.py | 75 + .../sdk/cli/commands/start/docker_fleet.py | 42 + src/codegen/sdk/cli/commands/start/main.py | 157 + .../sdk/cli/commands/style_debug/main.py | 21 + src/codegen/sdk/cli/commands/update/main.py | 67 + src/codegen/sdk/cli/errors.py | 60 + src/codegen/sdk/cli/git/folder.py | 14 + src/codegen/sdk/cli/git/patch.py | 41 + src/codegen/sdk/cli/git/repo.py | 16 + src/codegen/sdk/cli/mcp/README.md | 40 + .../sdk/cli/mcp/resources/system_prompt.py | 9911 +++++++++++++ .../resources/system_setup_instructions.py | 11 + src/codegen/sdk/cli/mcp/server.py | 58 + src/codegen/sdk/cli/rich/codeblocks.py | 42 + src/codegen/sdk/cli/rich/pretty_print.py | 49 + src/codegen/sdk/cli/rich/spinners.py | 29 + src/codegen/sdk/cli/sdk/__init__.py | 0 src/codegen/sdk/cli/sdk/decorator.py | 108 + src/codegen/sdk/cli/sdk/function.py | 10 + src/codegen/sdk/cli/sdk/models.py | 10 + src/codegen/sdk/cli/utils/codemod_manager.py | 142 + src/codegen/sdk/cli/utils/codemods.py | 34 + src/codegen/sdk/cli/utils/count_functions.py | 45 + .../sdk/cli/utils/count_functions_2.py | 1 + src/codegen/sdk/cli/utils/default_code.py | 20 + src/codegen/sdk/cli/utils/function_finder.py | 309 + src/codegen/sdk/cli/utils/json_schema.py | 38 + src/codegen/sdk/cli/utils/notebooks.py | 216 + src/codegen/sdk/cli/utils/schema.py | 26 + src/codegen/sdk/cli/workspace/decorators.py | 23 + .../sdk/cli/workspace/initialize_workspace.py | 103 + src/codegen/sdk/cli/workspace/venv_manager.py | 56 + src/codegen/sdk/code_generation/__init__.py | 0 .../code_generation/changelog_generation.py | 142 + .../code_generation/codegen_sdk_codebase.py | 22 + .../code_generation/current_code_codebase.py | 94 + .../sdk/code_generation/doc_utils/__init__.py | 0 .../doc_utils/generate_docs_json.py | 183 + .../doc_utils/parse_docstring.py | 68 + .../sdk/code_generation/doc_utils/schemas.py | 42 + .../sdk/code_generation/doc_utils/utils.py | 408 + src/codegen/sdk/code_generation/enums.py | 8 + .../code_generation/mdx_docs_generation.py | 204 + .../sdk/code_generation/prompts/__init__.py | 0 .../sdk/code_generation/prompts/api_docs.py | 235 + .../sdk/code_generation/prompts/utils.py | 110 + src/codegen/sdk/codebase/__init__.py | 0 src/codegen/sdk/codebase/codebase_ai.py | 208 + src/codegen/sdk/codebase/codebase_analysis.py | 87 + src/codegen/sdk/codebase/codebase_context.py | 848 ++ src/codegen/sdk/codebase/config.py | 68 + src/codegen/sdk/codebase/config_parser.py | 24 + src/codegen/sdk/codebase/diff_lite.py | 85 + .../sdk/codebase/factory/codebase_factory.py | 28 + .../sdk/codebase/factory/get_session.py | 122 + .../sdk/codebase/flagging/code_flag.py | 35 + src/codegen/sdk/codebase/flagging/enums.py | 36 + src/codegen/sdk/codebase/flagging/flags.py | 77 + src/codegen/sdk/codebase/flagging/group.py | 17 + .../codebase/flagging/groupers/all_grouper.py | 22 + .../codebase/flagging/groupers/app_grouper.py | 33 + .../flagging/groupers/base_grouper.py | 29 + .../flagging/groupers/codeowner_grouper.py | 41 + .../codebase/flagging/groupers/constants.py | 15 + .../sdk/codebase/flagging/groupers/enums.py | 11 + .../flagging/groupers/file_chunk_grouper.py | 46 + .../flagging/groupers/file_grouper.py | 32 + .../flagging/groupers/instance_grouper.py | 27 + .../sdk/codebase/flagging/groupers/utils.py | 14 + src/codegen/sdk/codebase/io/file_io.py | 68 + src/codegen/sdk/codebase/io/io.py | 46 + src/codegen/sdk/codebase/multigraph.py | 16 + .../sdk/codebase/node_classes/__init__.py | 0 .../node_classes/generic_node_classes.py | 22 + .../sdk/codebase/node_classes/node_classes.py | 49 + .../codebase/node_classes/py_node_classes.py | 130 + .../codebase/node_classes/ts_node_classes.py | 184 + src/codegen/sdk/codebase/progress/progress.py | 13 + .../sdk/codebase/progress/stub_progress.py | 7 + .../sdk/codebase/progress/stub_task.py | 9 + src/codegen/sdk/codebase/progress/task.py | 11 + src/codegen/sdk/codebase/range_index.py | 53 + src/codegen/sdk/codebase/resolution_stack.py | 3 + src/codegen/sdk/codebase/span.py | 83 + .../sdk/codebase/transaction_manager.py | 306 + src/codegen/sdk/codebase/transactions.py | 302 + src/codegen/sdk/codebase/validation.py | 151 + src/codegen/sdk/codemods/README.md | 44 + .../sdk/codemods/canonical/__init__.py | 0 .../__init__.py | 0 ...add_function_parameter_type_annotations.py | 51 + .../__init__.py | 0 ...add_internal_to_non_exported_components.py | 44 + .../bang_bang_to_boolean/__init__.py | 0 .../bang_bang_to_boolean.py | 37 + .../built_in_type_annotation.py | 44 + .../change_component_tag_names/__init__.py | 0 .../change_component_tag_names.py | 59 + .../canonical/classnames_to_backtick.py | 50 + .../__init__.py | 0 .../convert_array_type_to_square_bracket.py | 38 + .../__init__.py | 0 .../convert_attribute_to_decorator.py | 59 + .../__init__.py | 0 .../convert_comments_to_JSDoc_style.py | 45 + .../__init__.py | 0 .../convert_docstring_to_google_style.py | 30 + .../delete_unused_functions.py | 34 + .../emojify_py_files_codemod.py | 28 + .../canonical/enum_mover/enum_mover.py | 49 + .../insert_arguments_to_decorator/__init__.py | 0 .../insert_arguments_to_decorator.py | 45 + .../invite_factory_create_params/__init__.py | 0 .../invite_factory_create_params.py | 69 + .../js_to_esm_codemod/js_to_esm_codemod.py | 33 + .../mark_as_internal_codemod.py | 49 + .../mark_internal_to_module.py | 32 + .../canonical/mark_is_boolean/__init__.py | 0 .../mark_is_boolean/mark_is_boolean.py | 45 + .../migrate_class_attributes.py | 61 + .../move_enums_codemod/move_enums_codemod.py | 42 + .../move_functions_to_new_file.py | 39 + .../openapi_add_response_none.py | 75 + .../openapi_no_reference_request.py | 49 + .../canonical/pascal_case_symbols/__init__.py | 0 .../pascal_case_symbols.py | 41 + .../canonical/pivot_return_types/__init__.py | 0 .../pivot_return_types/pivot_return_types.py | 49 + ...or_react_components_into_separate_files.py | 48 + .../remove_indirect_imports.py | 53 + .../rename_function_parameters.py | 33 + .../rename_local_variables.py | 48 + .../replace_prop_values.py | 36 + .../return_none_type_annotation.py | 36 + .../canonical/split_decorators/__init__.py | 0 .../split_decorators/split_decorators.py | 52 + .../canonical/split_file/split_file.py | 38 + .../split_file_and_rename_symbols.py | 59 + .../split_large_files/split_large_files.py | 49 + .../swap_call_site_imports/__init__.py | 0 .../swap_call_site_imports.py | 63 + .../swap_class_attribute_usages.py | 63 + .../__init__.py | 0 .../update_optional_type_annotations.py | 55 + .../canonical/update_union_types/__init__.py | 0 .../update_union_types/update_union_types.py | 41 + .../use_named_kwargs/use_named_kwargs.py | 57 + .../wrap_with_component.py | 51 + .../wrap_with_statement.py | 44 + src/codegen/sdk/codemods/codemod.py | 10 + .../test_files/sample_py_1/expected/file.py | 6 + .../test_files/sample_py_1/original/file.py | 2 + .../test_files/sample_py_10/expected/file.py | 37 + .../test_files/sample_py_10/original/file.py | 34 + .../test_files/sample_py_2/expected/file.py | 5 + .../test_files/sample_py_2/expected/foo.py | 6 + .../test_files/sample_py_2/original/file.py | 10 + .../sample_py_3/expected/decorators.py | 2 + .../test_files/sample_py_3/expected/file.py | 15 + .../sample_py_3/original/decorators.py | 2 + .../test_files/sample_py_3/original/file.py | 10 + .../test_files/sample_py_4/expected/file.py | 10 + .../test_files/sample_py_4/expected/main.py | 5 + .../test_files/sample_py_4/original/file.py | 10 + .../test_files/sample_py_4/original/main.py | 5 + .../test_files/sample_py_5/expected/file.py | 10 + .../test_files/sample_py_5/original/file.py | 10 + .../test_files/sample_py_6/expected/file.py | 10 + .../test_files/sample_py_6/original/file.py | 14 + .../sample_py_6/original/unused_symbols.py | 5 + .../sample_py_7/expected/bar/enums.py | 11 + .../sample_py_7/expected/bar/file.py | 5 + .../sample_py_7/expected/foo/enums.py | 6 + .../sample_py_7/expected/foo/file.py | 17 + .../sample_py_7/original/bar/enums.py | 6 + .../sample_py_7/original/bar/file.py | 12 + .../sample_py_7/original/foo/file.py | 22 + .../test_files/sample_py_8/expected/file.py | 34 + .../test_files/sample_py_8/original/file.py | 34 + .../test_files/sample_py_9/expected/file.py | 34 + .../test_files/sample_py_9/original/file.py | 34 + .../test_files/sample_ts_1/expected/file.ts | 6 + .../test_files/sample_ts_1/original/file.ts | 4 + .../test_files/sample_ts_10/expected/types.ts | 11 + .../sample_ts_10/expected/userService.ts | 45 + .../test_files/sample_ts_10/original/types.ts | 11 + .../sample_ts_10/original/userService.ts | 45 + .../test_files/sample_ts_2/expected/App.tsx | 21 + .../sample_ts_2/expected/Footer.tsx | 5 + .../sample_ts_2/expected/Header.tsx | 5 + .../sample_ts_2/expected/MainContent.tsx | 5 + .../sample_ts_2/expected/Sidebar.tsx | 5 + .../sample_ts_2/expected/helpers.ts | 7 + .../test_files/sample_ts_2/original/App.tsx | 26 + .../sample_ts_2/original/Footer.tsx | 5 + .../sample_ts_2/original/Header.tsx | 5 + .../sample_ts_2/original/helpers.ts | 7 + .../test_files/sample_ts_3/expected/App.tsx | 29 + .../sample_ts_3/expected/Footer.tsx | 3 + .../sample_ts_3/expected/Header.tsx | 3 + .../test_files/sample_ts_3/original/App.tsx | 32 + .../sample_ts_3/original/Footer.tsx | 3 + .../sample_ts_3/original/Header.tsx | 3 + .../sample_ts_3/original/helpers.ts | 7 + .../test_files/sample_ts_4/expected/App.tsx | 26 + .../sample_ts_4/expected/Footer.tsx | 5 + .../sample_ts_4/expected/Header.tsx | 5 + .../sample_ts_4/expected/helpers.ts | 7 + .../test_files/sample_ts_4/original/App.tsx | 26 + .../sample_ts_4/original/Footer.tsx | 5 + .../sample_ts_4/original/Header.tsx | 5 + .../sample_ts_4/original/helpers.ts | 7 + .../test_files/sample_ts_5/expected/App.tsx | 15 + .../sample_ts_5/expected/user.router.js | 8 + .../sample_ts_5/expected/userHelpers.ts | 7 + .../test_files/sample_ts_5/original/App.tsx | 15 + .../sample_ts_5/original/user.router.js | 8 + .../sample_ts_5/original/userHelpers.ts | 7 + .../test_files/sample_ts_6/expected/App.tsx | 15 + .../sample_ts_6/expected/user.router.ts | 8 + .../sample_ts_6/expected/userHelpers.ts | 7 + .../test_files/sample_ts_6/original/App.tsx | 15 + .../sample_ts_6/original/user.router.js | 8 + .../sample_ts_6/original/userHelpers.ts | 7 + .../sample_ts_7/expected/app/App.tsx | 22 + .../sample_ts_7/expected/app/userHelpers.ts | 8 + .../sample_ts_7/expected/misc/Misc.tsx | 11 + .../sample_ts_7/original/app/App.tsx | 20 + .../sample_ts_7/original/app/userHelpers.ts | 7 + .../sample_ts_7/original/misc/Misc.tsx | 11 + .../test_files/sample_ts_8/expected/enums.ts | 10 + .../test_files/sample_ts_8/expected/types.ts | 6 + .../sample_ts_8/expected/userService.ts | 41 + .../test_files/sample_ts_8/original/types.ts | 11 + .../sample_ts_8/original/userService.ts | 45 + .../test_files/sample_ts_9/expected/types.ts | 11 + .../sample_ts_9/expected/userService.ts | 45 + .../test_files/sample_ts_9/original/types.ts | 11 + .../sample_ts_9/original/userService.ts | 45 + src/codegen/sdk/compiled/autocommit.pyi | 31 + src/codegen/sdk/compiled/autocommit.pyx | 217 + src/codegen/sdk/compiled/py.typed | 0 src/codegen/sdk/compiled/resolution.pyi | 48 + src/codegen/sdk/compiled/resolution.pyx | 107 + src/codegen/sdk/compiled/sort.pyx | 42 + src/codegen/sdk/compiled/utils.pyi | 27 + src/codegen/sdk/compiled/utils.pyx | 162 + src/codegen/sdk/configs/constants.py | 20 + src/codegen/sdk/configs/models/base_config.py | 57 + src/codegen/sdk/configs/models/codebase.py | 44 + src/codegen/sdk/configs/models/repository.py | 39 + src/codegen/sdk/configs/models/secrets.py | 16 + src/codegen/sdk/configs/models/utils.py | 9 + src/codegen/sdk/configs/session_manager.py | 64 + src/codegen/sdk/configs/user_config.py | 69 + src/codegen/sdk/core/__init__.py | 0 src/codegen/sdk/core/assignment.py | 287 + src/codegen/sdk/core/autocommit/__init__.py | 34 + src/codegen/sdk/core/autocommit/constants.py | 62 + src/codegen/sdk/core/autocommit/decorators.py | 115 + src/codegen/sdk/core/autocommit/manager.py | 294 + src/codegen/sdk/core/autocommit/ruff.toml | 2 + src/codegen/sdk/core/autocommit/utils.py | 27 + src/codegen/sdk/core/class_definition.py | 419 + src/codegen/sdk/core/codebase.py | 1613 +++ src/codegen/sdk/core/codeowner.py | 102 + src/codegen/sdk/core/dataclasses/usage.py | 92 + .../sdk/core/detached_symbols/__init__.py | 0 .../sdk/core/detached_symbols/argument.py | 163 + .../sdk/core/detached_symbols/code_block.py | 555 + .../sdk/core/detached_symbols/decorator.py | 57 + .../core/detached_symbols/function_call.py | 721 + .../sdk/core/detached_symbols/parameter.py | 232 + src/codegen/sdk/core/directory.py | 269 + src/codegen/sdk/core/export.py | 105 + src/codegen/sdk/core/expressions/__init__.py | 14 + .../sdk/core/expressions/await_expression.py | 33 + .../sdk/core/expressions/binary_expression.py | 132 + src/codegen/sdk/core/expressions/boolean.py | 31 + src/codegen/sdk/core/expressions/builtin.py | 28 + .../sdk/core/expressions/chained_attribute.py | 182 + .../core/expressions/comparison_expression.py | 59 + .../sdk/core/expressions/defined_name.py | 26 + .../sdk/core/expressions/expression.py | 39 + .../sdk/core/expressions/generic_type.py | 76 + .../sdk/core/expressions/multi_expression.py | 41 + src/codegen/sdk/core/expressions/name.py | 122 + .../sdk/core/expressions/named_type.py | 75 + src/codegen/sdk/core/expressions/none_type.py | 29 + src/codegen/sdk/core/expressions/number.py | 28 + .../expressions/parenthesized_expression.py | 88 + .../sdk/core/expressions/placeholder_type.py | 32 + src/codegen/sdk/core/expressions/string.py | 74 + .../core/expressions/subscript_expression.py | 62 + .../core/expressions/ternary_expression.py | 78 + .../sdk/core/expressions/tuple_type.py | 57 + src/codegen/sdk/core/expressions/type.py | 48 + .../sdk/core/expressions/unary_expression.py | 58 + .../sdk/core/expressions/union_type.py | 57 + src/codegen/sdk/core/expressions/unpack.py | 63 + src/codegen/sdk/core/expressions/value.py | 34 + .../sdk/core/external/dependency_manager.py | 38 + .../sdk/core/external/external_process.py | 78 + .../sdk/core/external/language_engine.py | 37 + src/codegen/sdk/core/external_module.py | 160 + src/codegen/sdk/core/file.py | 1192 ++ src/codegen/sdk/core/function.py | 417 + src/codegen/sdk/core/import_resolution.py | 722 + src/codegen/sdk/core/interface.py | 83 + src/codegen/sdk/core/interfaces/__init__.py | 0 src/codegen/sdk/core/interfaces/callable.py | 130 + src/codegen/sdk/core/interfaces/chainable.py | 76 + .../sdk/core/interfaces/conditional_block.py | 37 + src/codegen/sdk/core/interfaces/editable.py | 1174 ++ src/codegen/sdk/core/interfaces/exportable.py | 105 + .../sdk/core/interfaces/has_attribute.py | 15 + src/codegen/sdk/core/interfaces/has_block.py | 146 + src/codegen/sdk/core/interfaces/has_name.py | 96 + .../sdk/core/interfaces/has_symbols.py | 117 + src/codegen/sdk/core/interfaces/has_value.py | 35 + src/codegen/sdk/core/interfaces/importable.py | 129 + src/codegen/sdk/core/interfaces/inherits.py | 73 + src/codegen/sdk/core/interfaces/parseable.py | 11 + src/codegen/sdk/core/interfaces/resolvable.py | 20 + .../sdk/core/interfaces/supports_generic.py | 31 + src/codegen/sdk/core/interfaces/typeable.py | 51 + .../sdk/core/interfaces/unwrappable.py | 25 + src/codegen/sdk/core/interfaces/usable.py | 91 + .../sdk/core/interfaces/wrapper_expression.py | 55 + src/codegen/sdk/core/node_id_factory.py | 1 + src/codegen/sdk/core/parser.py | 300 + src/codegen/sdk/core/placeholder/__init__.py | 0 .../sdk/core/placeholder/placeholder.py | 73 + .../sdk/core/placeholder/placeholder_stub.py | 32 + .../sdk/core/placeholder/placeholder_type.py | 38 + src/codegen/sdk/core/plugins/__init__.py | 9 + src/codegen/sdk/core/plugins/axios.py | 52 + src/codegen/sdk/core/plugins/flask.py | 63 + src/codegen/sdk/core/plugins/modal.py | 26 + src/codegen/sdk/core/plugins/plugin.py | 17 + src/codegen/sdk/core/statements/__init__.py | 0 .../core/statements/assignment_statement.py | 82 + src/codegen/sdk/core/statements/attribute.py | 98 + .../sdk/core/statements/block_statement.py | 86 + .../sdk/core/statements/catch_statement.py | 36 + src/codegen/sdk/core/statements/comment.py | 130 + .../sdk/core/statements/export_statement.py | 115 + .../core/statements/expression_statement.py | 71 + .../sdk/core/statements/for_loop_statement.py | 60 + .../sdk/core/statements/if_block_statement.py | 308 + .../sdk/core/statements/import_statement.py | 51 + .../sdk/core/statements/raise_statement.py | 61 + .../sdk/core/statements/return_statement.py | 68 + src/codegen/sdk/core/statements/statement.py | 164 + .../sdk/core/statements/switch_case.py | 43 + .../sdk/core/statements/switch_statement.py | 84 + .../sdk/core/statements/symbol_statement.py | 69 + .../core/statements/try_catch_statement.py | 52 + .../sdk/core/statements/while_statement.py | 86 + src/codegen/sdk/core/symbol.py | 443 + src/codegen/sdk/core/symbol_group.py | 281 + .../sdk/core/symbol_groups/__init__.py | 0 .../sdk/core/symbol_groups/collection.py | 281 + .../sdk/core/symbol_groups/comment_group.py | 80 + src/codegen/sdk/core/symbol_groups/dict.py | 180 + .../core/symbol_groups/expression_group.py | 66 + src/codegen/sdk/core/symbol_groups/list.py | 30 + .../symbol_groups/multi_line_collection.py | 98 + src/codegen/sdk/core/symbol_groups/parents.py | 84 + src/codegen/sdk/core/symbol_groups/tuple.py | 30 + .../sdk/core/symbol_groups/type_parameters.py | 23 + src/codegen/sdk/core/type_alias.py | 76 + src/codegen/sdk/core/utils/cache_utils.py | 45 + src/codegen/sdk/enums.py | 85 + src/codegen/sdk/extensions/__init__.py | 6 + .../extensions/attribution/3pp/__init__.py | 1 + .../sdk/extensions/attribution/3pp/cursor.py | 593 + .../extensions/attribution/3pp/windsurf.py | 186 + src/codegen/sdk/extensions/attribution/cli.py | 153 + .../sdk/extensions/attribution/git_history.py | 433 + .../sdk/extensions/attribution/main.py | 87 + .../sdk/extensions/autogenlib/__init__.py | 67 + .../sdk/extensions/autogenlib/_cache.py | 100 + .../sdk/extensions/autogenlib/_caller.py | 127 + .../sdk/extensions/autogenlib/_context.py | 55 + .../autogenlib/_exception_handler.py | 638 + .../sdk/extensions/autogenlib/_finder.py | 239 + .../sdk/extensions/autogenlib/_generator.py | 356 + .../sdk/extensions/autogenlib/_state.py | 10 + src/codegen/sdk/extensions/clients/linear.py | 163 + src/codegen/sdk/extensions/github/__init__.py | 0 .../sdk/extensions/github/types/__init__.py | 0 .../sdk/extensions/github/types/author.py | 7 + .../sdk/extensions/github/types/base.py | 68 + .../sdk/extensions/github/types/commit.py | 17 + .../sdk/extensions/github/types/enterprise.py | 14 + .../github/types/events/pull_request.py | 73 + .../extensions/github/types/events/push.py | 27 + .../extensions/github/types/installation.py | 6 + .../sdk/extensions/github/types/label.py | 11 + .../extensions/github/types/organization.py | 16 + .../extensions/github/types/pull_request.py | 89 + .../sdk/extensions/github/types/push.py | 27 + .../sdk/extensions/github/types/pusher.py | 6 + src/codegen/sdk/extensions/graph/__init__.py | 0 .../sdk/extensions/graph/create_graph.py | 135 + src/codegen/sdk/extensions/graph/main.py | 42 + .../sdk/extensions/graph/neo4j_exporter.py | 49 + src/codegen/sdk/extensions/graph/utils.py | 78 + src/codegen/sdk/extensions/index/__init__.py | 0 .../sdk/extensions/index/code_index.py | 225 + .../sdk/extensions/index/file_index.py | 366 + .../sdk/extensions/index/symbol_index.py | 160 + src/codegen/sdk/extensions/linear/__init__.py | 3 + .../sdk/extensions/linear/linear_client.py | 294 + src/codegen/sdk/extensions/linear/types.py | 40 + .../sdk/extensions/lsp/codemods/__init__.py | 4 + .../sdk/extensions/lsp/codemods/base.py | 41 + .../lsp/codemods/move_symbol_to_file.py | 23 + .../extensions/lsp/codemods/split_tests.py | 33 + src/codegen/sdk/extensions/lsp/completion.py | 0 src/codegen/sdk/extensions/lsp/definition.py | 35 + .../sdk/extensions/lsp/document_symbol.py | 26 + src/codegen/sdk/extensions/lsp/execute.py | 39 + src/codegen/sdk/extensions/lsp/io.py | 152 + src/codegen/sdk/extensions/lsp/kind.py | 31 + src/codegen/sdk/extensions/lsp/lsp.py | 140 + src/codegen/sdk/extensions/lsp/progress.py | 60 + src/codegen/sdk/extensions/lsp/protocol.py | 40 + src/codegen/sdk/extensions/lsp/range.py | 32 + src/codegen/sdk/extensions/lsp/server.py | 100 + .../sdk/extensions/lsp/solidlsp/.gitignore | 1 + .../sdk/extensions/lsp/solidlsp/__init__.py | 2 + .../language_servers/bash_language_server.py | 230 + .../clangd_language_server.py | 210 + .../solidlsp/language_servers/clojure_lsp.py | 220 + .../lsp/solidlsp/language_servers/common.py | 114 + .../csharp_language_server.py | 741 + .../language_servers/dart_language_server.py | 158 + .../language_servers/eclipse_jdtls.py | 723 + .../language_servers/elixir_tools/README.md | 90 + .../language_servers/elixir_tools/__init__.py | 1 + .../elixir_tools/elixir_tools.py | 353 + .../erlang_language_server.py | 230 + .../lsp/solidlsp/language_servers/gopls.py | 159 + .../solidlsp/language_servers/intelephense.py | 191 + .../solidlsp/language_servers/jedi_server.py | 198 + .../kotlin_language_server.py | 469 + .../lsp/solidlsp/language_servers/lua_ls.py | 297 + .../lsp/solidlsp/language_servers/nixd_ls.py | 396 + .../solidlsp/language_servers/omnisharp.py | 380 + .../omnisharp/initialize_params.json | 631 + .../omnisharp/runtime_dependencies.json | 441 + .../workspace_did_change_configuration.json | 111 + .../language_servers/pyright_server.py | 202 + .../language_servers/r_language_server.py | 172 + .../lsp/solidlsp/language_servers/ruby_lsp.py | 442 + .../language_servers/rust_analyzer.py | 639 + .../solidlsp/language_servers/solargraph.py | 373 + .../language_servers/sourcekit_lsp.py | 358 + .../solidlsp/language_servers/terraform_ls.py | 214 + .../typescript_language_server.py | 253 + .../language_servers/vts_language_server.py | 235 + .../lsp/solidlsp/language_servers/zls.py | 252 + src/codegen/sdk/extensions/lsp/solidlsp/ls.py | 1738 +++ .../sdk/extensions/lsp/solidlsp/ls_config.py | 156 + .../extensions/lsp/solidlsp/ls_exceptions.py | 40 + .../sdk/extensions/lsp/solidlsp/ls_handler.py | 581 + .../sdk/extensions/lsp/solidlsp/ls_logger.py | 66 + .../sdk/extensions/lsp/solidlsp/ls_request.py | 383 + .../sdk/extensions/lsp/solidlsp/ls_types.py | 343 + .../sdk/extensions/lsp/solidlsp/ls_utils.py | 406 + .../lsp_protocol_handler/lsp_constants.py | 69 + .../lsp_protocol_handler/lsp_requests.py | 561 + .../lsp_protocol_handler/lsp_types.py | 5962 ++++++++ .../solidlsp/lsp_protocol_handler/server.py | 122 + .../sdk/extensions/lsp/solidlsp/settings.py | 29 + .../lsp/solidlsp/util/subprocess_util.py | 13 + .../sdk/extensions/lsp/solidlsp/util/zip.py | 128 + src/codegen/sdk/extensions/lsp/utils.py | 7 + src/codegen/sdk/extensions/mcp/README.md | 43 + .../sdk/extensions/mcp/codebase_mods.py | 47 + .../sdk/extensions/mcp/codebase_tools.py | 59 + src/codegen/sdk/extensions/slack/types.py | 79 + src/codegen/sdk/extensions/swebench/README.md | 29 + .../sdk/extensions/swebench/__init__.py | 0 src/codegen/sdk/extensions/swebench/enums.py | 13 + .../sdk/extensions/swebench/harness.py | 200 + src/codegen/sdk/extensions/swebench/report.py | 154 + .../sdk/extensions/swebench/subsets.py | 146 + .../sdk/extensions/swebench/success_rates.py | 302 + src/codegen/sdk/extensions/swebench/tests.py | 33 + src/codegen/sdk/extensions/swebench/utils.py | 135 + src/codegen/sdk/extensions/tools/README.md | 100 + src/codegen/sdk/extensions/tools/bash.py | 183 + .../extensions/tools/codegen_sdk_codebase.py | 22 + .../extensions/tools/current_code_codebase.py | 94 + .../extensions/tools/document_functions.py | 119 + .../extensions/tools/generate_docs_json.py | 183 + .../sdk/extensions/tools/list_directory.py | 232 + .../extensions/tools/mdx_docs_generation.py | 204 + .../sdk/extensions/tools/observation.py | 92 + .../sdk/extensions/tools/reflection.py | 225 + .../sdk/extensions/tools/reveal_symbol.py | 316 + .../sdk/extensions/tools/reveal_symbol_fn.py | 75 + .../sdk/extensions/tools/tool_output_types.py | 105 + src/codegen/sdk/extensions/tools/tools.py | 223 + src/codegen/sdk/extensions/tools/view_file.py | 191 + src/codegen/sdk/git/README.md | 8 + src/codegen/sdk/git/__init__.py | 0 .../sdk/git/clients/git_repo_client.py | 463 + src/codegen/sdk/git/clients/github_client.py | 46 + src/codegen/sdk/git/configs/constants.py | 5 + src/codegen/sdk/git/models/codemod_context.py | 36 + .../git/models/github_named_user_context.py | 12 + src/codegen/sdk/git/models/pr_options.py | 20 + src/codegen/sdk/git/models/pr_part_context.py | 12 + .../sdk/git/models/pull_request_context.py | 49 + src/codegen/sdk/git/py.typed | 0 .../sdk/git/repo_operator/local_git_repo.py | 84 + .../sdk/git/repo_operator/repo_operator.py | 920 ++ src/codegen/sdk/git/schemas/enums.py | 35 + src/codegen/sdk/git/schemas/repo_config.py | 56 + src/codegen/sdk/git/utils/clone.py | 61 + src/codegen/sdk/git/utils/clone_url.py | 25 + src/codegen/sdk/git/utils/codeowner_utils.py | 50 + src/codegen/sdk/git/utils/file_utils.py | 73 + src/codegen/sdk/git/utils/format.py | 24 + src/codegen/sdk/git/utils/language.py | 183 + src/codegen/sdk/git/utils/pr_review.py | 171 + src/codegen/sdk/git/utils/remote_progress.py | 32 + src/codegen/sdk/gsbuild/README.md | 3 + src/codegen/sdk/gsbuild/build.py | 24 + src/codegen/sdk/gscli/README.md | 3 + src/codegen/sdk/gscli/__init__.py | 0 src/codegen/sdk/gscli/backend/__init__.py | 0 .../sdk/gscli/backend/typestub_utils.py | 138 + src/codegen/sdk/gscli/backend/utils.py | 9 + src/codegen/sdk/gscli/cli.py | 18 + src/codegen/sdk/gscli/generate/__init__.py | 0 src/codegen/sdk/gscli/generate/commands.py | 242 + .../sdk/gscli/generate/runner_imports.py | 108 + .../sdk/gscli/generate/system_prompt.py | 29 + src/codegen/sdk/gscli/generate/utils.py | 55 + src/codegen/sdk/output/ast.py | 14 + src/codegen/sdk/output/constants.py | 3 + src/codegen/sdk/output/inspect.py | 25 + src/codegen/sdk/output/jsonable.py | 88 + src/codegen/sdk/output/placeholder.py | 12 + src/codegen/sdk/output/utils.py | 84 + src/codegen/sdk/py.typed | 0 src/codegen/sdk/python/__init__.py | 15 + src/codegen/sdk/python/assignment.py | 168 + src/codegen/sdk/python/class_definition.py | 118 + .../sdk/python/detached_symbols/code_block.py | 76 + .../sdk/python/detached_symbols/decorator.py | 58 + .../sdk/python/detached_symbols/parameter.py | 72 + .../python/expressions/chained_attribute.py | 20 + .../expressions/conditional_expression.py | 20 + .../sdk/python/expressions/generic_type.py | 45 + .../sdk/python/expressions/named_type.py | 21 + src/codegen/sdk/python/expressions/string.py | 23 + src/codegen/sdk/python/expressions/type.py | 2 + .../sdk/python/expressions/union_type.py | 17 + src/codegen/sdk/python/file.py | 276 + src/codegen/sdk/python/function.py | 265 + src/codegen/sdk/python/import_resolution.py | 370 + .../sdk/python/interfaces/has_block.py | 91 + .../placeholder/placeholder_return_type.py | 38 + src/codegen/sdk/python/statements/__init__.py | 0 .../python/statements/assignment_statement.py | 91 + .../sdk/python/statements/attribute.py | 111 + .../sdk/python/statements/block_statement.py | 26 + .../sdk/python/statements/break_statement.py | 24 + .../sdk/python/statements/catch_statement.py | 34 + src/codegen/sdk/python/statements/comment.py | 162 + .../python/statements/for_loop_statement.py | 71 + .../python/statements/if_block_statement.py | 106 + .../sdk/python/statements/import_statement.py | 33 + .../sdk/python/statements/match_case.py | 27 + .../sdk/python/statements/match_statement.py | 27 + .../sdk/python/statements/pass_statement.py | 24 + .../python/statements/try_catch_statement.py | 106 + .../sdk/python/statements/while_statement.py | 89 + .../sdk/python/statements/with_statement.py | 88 + src/codegen/sdk/python/symbol.py | 222 + .../sdk/python/symbol_groups/comment_group.py | 250 + src/codegen/sdk/runner/README.md | 9 + src/codegen/sdk/runner/__init__.py | 0 src/codegen/sdk/runner/clients/client.py | 55 + .../sdk/runner/clients/codebase_client.py | 83 + .../sdk/runner/clients/docker_client.py | 40 + src/codegen/sdk/runner/constants/envvars.py | 6 + src/codegen/sdk/runner/diff/get_raw_diff.py | 94 + src/codegen/sdk/runner/enums/warmup_state.py | 7 + src/codegen/sdk/runner/models/apis.py | 63 + src/codegen/sdk/runner/models/codemod.py | 51 + .../sdk/runner/sandbox/ephemeral_server.py | 56 + src/codegen/sdk/runner/sandbox/executor.py | 175 + src/codegen/sdk/runner/sandbox/middlewares.py | 51 + src/codegen/sdk/runner/sandbox/repo.py | 70 + src/codegen/sdk/runner/sandbox/runner.py | 87 + src/codegen/sdk/runner/sandbox/server.py | 84 + .../sdk/runner/servers/local_daemon.py | 97 + src/codegen/sdk/runner/utils/branch_name.py | 11 + .../sdk/runner/utils/exception_utils.py | 15 + src/codegen/sdk/shared/README.md | 8 + src/codegen/sdk/shared/__init__.py | 0 src/codegen/sdk/shared/compilation/README.md | 8 + .../compilation/codeblock_validation.py | 14 + .../sdk/shared/compilation/exception_utils.py | 51 + .../compilation/function_compilation.py | 67 + .../compilation/function_construction.py | 55 + .../shared/compilation/function_imports.py | 200 + .../sdk/shared/compilation/string_to_code.py | 109 + src/codegen/sdk/shared/decorators/docs.py | 91 + .../sdk/shared/enums/programming_language.py | 8 + src/codegen/sdk/shared/exceptions/api.py | 2 + .../sdk/shared/exceptions/compilation.py | 10 + .../sdk/shared/exceptions/control_flow.py | 28 + src/codegen/sdk/shared/logging/get_logger.py | 76 + src/codegen/sdk/shared/network/port.py | 17 + src/codegen/sdk/shared/path.py | 15 + .../sdk/shared/performance/memory_utils.py | 20 + .../sdk/shared/performance/stopwatch_utils.py | 48 + .../sdk/shared/performance/time_utils.py | 11 + src/codegen/sdk/shared/string/csv_utils.py | 21 + src/codegen/sdk/system-prompt.txt | 11684 ++++++++++++++++ src/codegen/sdk/topological_sort.py | 44 + src/codegen/sdk/tree_sitter_parser.py | 92 + src/codegen/sdk/types.py | 1 + src/codegen/sdk/typescript/__init__.py | 0 src/codegen/sdk/typescript/assignment.py | 83 + .../sdk/typescript/class_definition.py | 227 + src/codegen/sdk/typescript/config_parser.py | 63 + .../typescript/detached_symbols/code_block.py | 79 + .../typescript/detached_symbols/decorator.py | 54 + .../detached_symbols/jsx/element.py | 199 + .../detached_symbols/jsx/expression.py | 74 + .../typescript/detached_symbols/jsx/prop.py | 116 + .../typescript/detached_symbols/parameter.py | 187 + .../detached_symbols/promise_chain.py | 559 + src/codegen/sdk/typescript/enum_definition.py | 100 + src/codegen/sdk/typescript/enums.py | 36 + src/codegen/sdk/typescript/export.py | 705 + .../sdk/typescript/expressions/array_type.py | 19 + .../expressions/chained_attribute.py | 39 + .../expressions/conditional_type.py | 59 + .../typescript/expressions/expression_type.py | 29 + .../typescript/expressions/function_type.py | 94 + .../typescript/expressions/generic_type.py | 27 + .../sdk/typescript/expressions/lookup_type.py | 65 + .../sdk/typescript/expressions/named_type.py | 19 + .../sdk/typescript/expressions/object_type.py | 81 + .../sdk/typescript/expressions/query_type.py | 59 + .../typescript/expressions/readonly_type.py | 59 + .../sdk/typescript/expressions/string.py | 35 + .../expressions/ternary_expression.py | 20 + .../sdk/typescript/expressions/type.py | 2 + .../typescript/expressions/undefined_type.py | 29 + .../sdk/typescript/expressions/union_type.py | 17 + .../typescript/external/dependency_manager.py | 376 + .../sdk/typescript/external/mega_racer.py | 30 + .../typescript/external/ts_analyzer_engine.py | 250 + .../external/ts_declassify/ts_declassify.py | 94 + .../external/typescript_analyzer/.gitignore | 4 + .../external/typescript_analyzer/package.json | 30 + .../typescript_analyzer/rollup.config.js | 36 + .../typescript_analyzer/src/analyzer.ts | 410 + .../typescript_analyzer/src/fs_proxy.ts | 120 + .../external/typescript_analyzer/src/fsi.ts | 7 + .../src/get_type_at_position.ts | 57 + .../external/typescript_analyzer/src/index.ts | 3 + .../typescript_analyzer/src/run_full.ts | 140 + .../typescript_analyzer/src/test_fsi.ts | 103 + .../typescript_analyzer/tsconfig.json | 21 + src/codegen/sdk/typescript/file.py | 450 + src/codegen/sdk/typescript/function.py | 452 + .../sdk/typescript/import_resolution.py | 648 + src/codegen/sdk/typescript/interface.py | 98 + .../sdk/typescript/interfaces/has_block.py | 171 + src/codegen/sdk/typescript/namespace.py | 416 + .../placeholder/placeholder_return_type.py | 44 + .../sdk/typescript/statements/__init__.py | 0 .../statements/assignment_statement.py | 129 + .../sdk/typescript/statements/attribute.py | 86 + .../typescript/statements/block_statement.py | 17 + .../typescript/statements/catch_statement.py | 36 + .../sdk/typescript/statements/comment.py | 161 + .../statements/for_loop_statement.py | 120 + .../statements/if_block_statement.py | 139 + .../typescript/statements/import_statement.py | 45 + .../statements/labeled_statement.py | 63 + .../sdk/typescript/statements/switch_case.py | 28 + .../typescript/statements/switch_statement.py | 27 + .../statements/try_catch_statement.py | 104 + .../typescript/statements/while_statement.py | 32 + src/codegen/sdk/typescript/symbol.py | 512 + .../typescript/symbol_groups/comment_group.py | 127 + .../sdk/typescript/symbol_groups/dict.py | 144 + src/codegen/sdk/typescript/ts_config.py | 485 + src/codegen/sdk/typescript/type_alias.py | 73 + src/codegen/sdk/utils.py | 341 + src/codegen/sdk/visualizations/README.md | 175 + .../sdk/visualizations/blast_radius.py | 118 + src/codegen/sdk/visualizations/call_trace.py | 120 + .../sdk/visualizations/dependency_trace.py | 82 + src/codegen/sdk/visualizations/enums.py | 27 + .../visualizations/method_relationships.py | 106 + src/codegen/sdk/visualizations/py.typed | 0 .../visualizations/visualization_manager.py | 63 + src/codegen/sdk/visualizations/viz_utils.py | 70 + src/codegen/sdk/writer_decorators.py | 10 + 898 files changed, 112981 insertions(+), 2 deletions(-) create mode 100644 src/codegen.backup/__init__.py create mode 100644 src/codegen.backup/__main__.py create mode 100644 src/codegen.backup/agents/README.md create mode 100644 src/codegen.backup/agents/__init__.py create mode 100644 src/codegen.backup/agents/agent.py create mode 100644 src/codegen.backup/agents/constants.py create mode 100644 src/codegen.backup/cli/README.md create mode 100644 src/codegen.backup/cli/__init__.py create mode 100644 src/codegen.backup/cli/_env.py create mode 100644 src/codegen.backup/cli/api/client.py create mode 100644 src/codegen.backup/cli/api/endpoints.py create mode 100644 src/codegen.backup/cli/api/modal.py create mode 100644 src/codegen.backup/cli/api/schemas.py create mode 100644 src/codegen.backup/cli/api/webapp_routes.py create mode 100644 src/codegen.backup/cli/auth/constants.py create mode 100644 src/codegen.backup/cli/auth/decorators.py create mode 100644 src/codegen.backup/cli/auth/login.py create mode 100644 src/codegen.backup/cli/auth/session.ipynb create mode 100644 src/codegen.backup/cli/auth/session.py create mode 100644 src/codegen.backup/cli/auth/token_manager.ipynb create mode 100644 src/codegen.backup/cli/auth/token_manager.py create mode 100644 src/codegen.backup/cli/claude/__init__.py create mode 100644 src/codegen.backup/cli/cli.py create mode 100644 src/codegen.backup/cli/commands/agent/__init__.py create mode 100644 src/codegen.backup/cli/commands/agent/main.py create mode 100644 src/codegen.backup/cli/commands/agents/__init__.py create mode 100644 src/codegen.backup/cli/commands/agents/main.py create mode 100644 src/codegen.backup/cli/commands/claude/__init__.py create mode 100644 src/codegen.backup/cli/commands/claude/claude_log_utils.py create mode 100644 src/codegen.backup/cli/commands/claude/claude_log_watcher.py create mode 100644 src/codegen.backup/cli/commands/claude/claude_session_api.py create mode 100644 src/codegen.backup/cli/commands/claude/config/claude_session_active_hook.py create mode 100755 src/codegen.backup/cli/commands/claude/config/claude_session_hook.py create mode 100644 src/codegen.backup/cli/commands/claude/config/claude_session_stop_hook.py create mode 100644 src/codegen.backup/cli/commands/claude/config/mcp_setup.py create mode 100644 src/codegen.backup/cli/commands/claude/hooks.py create mode 100644 src/codegen.backup/cli/commands/claude/main.py create mode 100644 src/codegen.backup/cli/commands/claude/quiet_console.py create mode 100644 src/codegen.backup/cli/commands/claude/utils.py create mode 100644 src/codegen.backup/cli/commands/config/main.py create mode 100644 src/codegen.backup/cli/commands/config/telemetry.py create mode 100644 src/codegen.backup/cli/commands/init/main.py create mode 100644 src/codegen.backup/cli/commands/init/render.py create mode 100644 src/codegen.backup/cli/commands/integrations/__init__.py create mode 100644 src/codegen.backup/cli/commands/integrations/main.py create mode 100644 src/codegen.backup/cli/commands/login/main.py create mode 100644 src/codegen.backup/cli/commands/logout/main.py create mode 100644 src/codegen.backup/cli/commands/org/__init__.py create mode 100644 src/codegen.backup/cli/commands/org/main.py create mode 100644 src/codegen.backup/cli/commands/org/tui.py create mode 100644 src/codegen.backup/cli/commands/profile/main.py create mode 100644 src/codegen.backup/cli/commands/repo/__init__.py create mode 100644 src/codegen.backup/cli/commands/repo/main.py create mode 100644 src/codegen.backup/cli/commands/repo/tui.py create mode 100644 src/codegen.backup/cli/commands/style_debug/main.py create mode 100644 src/codegen.backup/cli/commands/tools/__init__.py create mode 100644 src/codegen.backup/cli/commands/tools/main.py create mode 100644 src/codegen.backup/cli/commands/tui/__init__.py create mode 100644 src/codegen.backup/cli/commands/tui/main.py create mode 100644 src/codegen.backup/cli/commands/update/main.py create mode 100644 src/codegen.backup/cli/env/constants.py create mode 100644 src/codegen.backup/cli/env/enums.py create mode 100644 src/codegen.backup/cli/env/global_env.ipynb create mode 100644 src/codegen.backup/cli/env/global_env.py create mode 100644 src/codegen.backup/cli/errors.py create mode 100644 src/codegen.backup/cli/mcp/README.md create mode 100644 src/codegen.backup/cli/mcp/__init__.py create mode 100644 src/codegen.backup/cli/mcp/api_client.py create mode 100644 src/codegen.backup/cli/mcp/prompts.py create mode 100644 src/codegen.backup/cli/mcp/resources.py create mode 100644 src/codegen.backup/cli/mcp/runner.py create mode 100644 src/codegen.backup/cli/mcp/server.py create mode 100644 src/codegen.backup/cli/mcp/tools/__init__.py create mode 100644 src/codegen.backup/cli/mcp/tools/dynamic.py create mode 100644 src/codegen.backup/cli/mcp/tools/executor.py create mode 100644 src/codegen.backup/cli/mcp/tools/static.py create mode 100644 src/codegen.backup/cli/rich/codeblocks.py create mode 100644 src/codegen.backup/cli/rich/pretty_print.py create mode 100644 src/codegen.backup/cli/rich/spinners.py create mode 100644 src/codegen.backup/cli/telemetry/__init__.py create mode 100644 src/codegen.backup/cli/telemetry/consent.py create mode 100644 src/codegen.backup/cli/telemetry/debug_exporter.py create mode 100644 src/codegen.backup/cli/telemetry/exception_logger.py create mode 100644 src/codegen.backup/cli/telemetry/otel_setup.py create mode 100644 src/codegen.backup/cli/telemetry/viewer.py create mode 100644 src/codegen.backup/cli/tui/__init__.py create mode 100644 src/codegen.backup/cli/tui/agent_detail.py create mode 100644 src/codegen.backup/cli/tui/app.py create mode 100644 src/codegen.backup/cli/tui/codegen_theme.tcss create mode 100644 src/codegen.backup/cli/tui/codegen_tui.tcss create mode 100644 src/codegen.backup/cli/tui/widows_app.py create mode 100644 src/codegen.backup/cli/utils/codemod_manager.py create mode 100644 src/codegen.backup/cli/utils/codemods.py create mode 100644 src/codegen.backup/cli/utils/count_functions_2.py create mode 100644 src/codegen.backup/cli/utils/default_code.py create mode 100644 src/codegen.backup/cli/utils/function_finder.py create mode 100644 src/codegen.backup/cli/utils/inplace_print.py create mode 100644 src/codegen.backup/cli/utils/json_schema.py create mode 100644 src/codegen.backup/cli/utils/notebooks.py create mode 100644 src/codegen.backup/cli/utils/org.py create mode 100644 src/codegen.backup/cli/utils/repo.py create mode 100644 src/codegen.backup/cli/utils/schema.ipynb create mode 100644 src/codegen.backup/cli/utils/schema.py create mode 100644 src/codegen.backup/cli/utils/simple_selector.py create mode 100644 src/codegen.backup/cli/utils/url.py create mode 100644 src/codegen.backup/compat.py create mode 100644 src/codegen.backup/configs/constants.py create mode 100644 src/codegen.backup/configs/models/base_config.py create mode 100644 src/codegen.backup/configs/models/codebase.py create mode 100644 src/codegen.backup/configs/models/repository.py create mode 100644 src/codegen.backup/configs/models/secrets.py create mode 100644 src/codegen.backup/configs/models/telemetry.py create mode 100644 src/codegen.backup/configs/models/utils.py create mode 100644 src/codegen.backup/configs/session_manager.py create mode 100644 src/codegen.backup/configs/user_config.py create mode 100644 src/codegen.backup/exports.py create mode 100644 src/codegen.backup/git/README.md create mode 100644 src/codegen.backup/git/__init__.py create mode 100644 src/codegen.backup/git/clients/git_repo_client.py create mode 100644 src/codegen.backup/git/clients/github_client.py create mode 100644 src/codegen.backup/git/configs/constants.py create mode 100644 src/codegen.backup/git/models/codemod_context.py create mode 100644 src/codegen.backup/git/models/github_named_user_context.py create mode 100644 src/codegen.backup/git/models/pr_options.py create mode 100644 src/codegen.backup/git/models/pr_part_context.py create mode 100644 src/codegen.backup/git/models/pull_request_context.py create mode 100644 src/codegen.backup/git/py.typed create mode 100644 src/codegen.backup/git/repo_operator/local_git_repo.py create mode 100644 src/codegen.backup/git/repo_operator/repo_operator.py create mode 100644 src/codegen.backup/git/schemas/enums.py create mode 100644 src/codegen.backup/git/schemas/repo_config.py create mode 100644 src/codegen.backup/git/utils/clone.py create mode 100644 src/codegen.backup/git/utils/clone_url.py create mode 100644 src/codegen.backup/git/utils/codeowner_utils.py create mode 100644 src/codegen.backup/git/utils/file_utils.py create mode 100644 src/codegen.backup/git/utils/format.py create mode 100644 src/codegen.backup/git/utils/language.py create mode 100644 src/codegen.backup/git/utils/pr_review.py create mode 100644 src/codegen.backup/git/utils/remote_progress.py create mode 100644 src/codegen.backup/py.typed create mode 100644 src/codegen.backup/shared/README.md create mode 100644 src/codegen.backup/shared/__init__.py create mode 100644 src/codegen.backup/shared/compilation/README.md create mode 100644 src/codegen.backup/shared/compilation/codeblock_validation.py create mode 100644 src/codegen.backup/shared/compilation/exception_utils.py create mode 100644 src/codegen.backup/shared/compilation/function_compilation.py create mode 100644 src/codegen.backup/shared/compilation/function_construction.py create mode 100644 src/codegen.backup/shared/compilation/function_imports.py create mode 100644 src/codegen.backup/shared/compilation/string_to_code.py create mode 100644 src/codegen.backup/shared/decorators/docs.py create mode 100644 src/codegen.backup/shared/enums/programming_language.py create mode 100644 src/codegen.backup/shared/exceptions/api.py create mode 100644 src/codegen.backup/shared/exceptions/compilation.py create mode 100644 src/codegen.backup/shared/exceptions/control_flow.py create mode 100644 src/codegen.backup/shared/logging/get_logger.py create mode 100644 src/codegen.backup/shared/network/port.py create mode 100644 src/codegen.backup/shared/path.py create mode 100644 src/codegen.backup/shared/performance/memory_utils.py create mode 100644 src/codegen.backup/shared/performance/stopwatch_utils.py create mode 100644 src/codegen.backup/shared/performance/time_utils.py create mode 100644 src/codegen.backup/shared/string/csv_utils.py create mode 100644 src/codegen/sdk/_proxy.py create mode 100644 src/codegen/sdk/ai/client.py create mode 100644 src/codegen/sdk/ai/utils.py create mode 100644 src/codegen/sdk/cli/README.md create mode 100644 src/codegen/sdk/cli/__init__.py create mode 100644 src/codegen/sdk/cli/_env.py create mode 100644 src/codegen/sdk/cli/auth/constants.py create mode 100644 src/codegen/sdk/cli/auth/session.py create mode 100644 src/codegen/sdk/cli/cli.py create mode 100644 src/codegen/sdk/cli/codemod/convert.py create mode 100644 src/codegen/sdk/cli/commands/config/main.py create mode 100644 src/codegen/sdk/cli/commands/create/main.py create mode 100644 src/codegen/sdk/cli/commands/init/main.py create mode 100644 src/codegen/sdk/cli/commands/init/render.py create mode 100644 src/codegen/sdk/cli/commands/list/main.py create mode 100644 src/codegen/sdk/cli/commands/lsp/lsp.py create mode 100644 src/codegen/sdk/cli/commands/notebook/main.py create mode 100644 src/codegen/sdk/cli/commands/reset/main.py create mode 100644 src/codegen/sdk/cli/commands/run/main.py create mode 100644 src/codegen/sdk/cli/commands/run/render.py create mode 100644 src/codegen/sdk/cli/commands/run/run_daemon.py create mode 100644 src/codegen/sdk/cli/commands/run/run_local.py create mode 100644 src/codegen/sdk/cli/commands/start/Dockerfile create mode 100644 src/codegen/sdk/cli/commands/start/docker_container.py create mode 100644 src/codegen/sdk/cli/commands/start/docker_fleet.py create mode 100644 src/codegen/sdk/cli/commands/start/main.py create mode 100644 src/codegen/sdk/cli/commands/style_debug/main.py create mode 100644 src/codegen/sdk/cli/commands/update/main.py create mode 100644 src/codegen/sdk/cli/errors.py create mode 100644 src/codegen/sdk/cli/git/folder.py create mode 100644 src/codegen/sdk/cli/git/patch.py create mode 100644 src/codegen/sdk/cli/git/repo.py create mode 100644 src/codegen/sdk/cli/mcp/README.md create mode 100644 src/codegen/sdk/cli/mcp/resources/system_prompt.py create mode 100644 src/codegen/sdk/cli/mcp/resources/system_setup_instructions.py create mode 100644 src/codegen/sdk/cli/mcp/server.py create mode 100644 src/codegen/sdk/cli/rich/codeblocks.py create mode 100644 src/codegen/sdk/cli/rich/pretty_print.py create mode 100644 src/codegen/sdk/cli/rich/spinners.py create mode 100644 src/codegen/sdk/cli/sdk/__init__.py create mode 100644 src/codegen/sdk/cli/sdk/decorator.py create mode 100644 src/codegen/sdk/cli/sdk/function.py create mode 100644 src/codegen/sdk/cli/sdk/models.py create mode 100644 src/codegen/sdk/cli/utils/codemod_manager.py create mode 100644 src/codegen/sdk/cli/utils/codemods.py create mode 100644 src/codegen/sdk/cli/utils/count_functions.py create mode 100644 src/codegen/sdk/cli/utils/count_functions_2.py create mode 100644 src/codegen/sdk/cli/utils/default_code.py create mode 100644 src/codegen/sdk/cli/utils/function_finder.py create mode 100644 src/codegen/sdk/cli/utils/json_schema.py create mode 100644 src/codegen/sdk/cli/utils/notebooks.py create mode 100644 src/codegen/sdk/cli/utils/schema.py create mode 100644 src/codegen/sdk/cli/workspace/decorators.py create mode 100644 src/codegen/sdk/cli/workspace/initialize_workspace.py create mode 100644 src/codegen/sdk/cli/workspace/venv_manager.py create mode 100644 src/codegen/sdk/code_generation/__init__.py create mode 100644 src/codegen/sdk/code_generation/changelog_generation.py create mode 100644 src/codegen/sdk/code_generation/codegen_sdk_codebase.py create mode 100644 src/codegen/sdk/code_generation/current_code_codebase.py create mode 100644 src/codegen/sdk/code_generation/doc_utils/__init__.py create mode 100644 src/codegen/sdk/code_generation/doc_utils/generate_docs_json.py create mode 100644 src/codegen/sdk/code_generation/doc_utils/parse_docstring.py create mode 100644 src/codegen/sdk/code_generation/doc_utils/schemas.py create mode 100644 src/codegen/sdk/code_generation/doc_utils/utils.py create mode 100644 src/codegen/sdk/code_generation/enums.py create mode 100644 src/codegen/sdk/code_generation/mdx_docs_generation.py create mode 100644 src/codegen/sdk/code_generation/prompts/__init__.py create mode 100644 src/codegen/sdk/code_generation/prompts/api_docs.py create mode 100644 src/codegen/sdk/code_generation/prompts/utils.py create mode 100644 src/codegen/sdk/codebase/__init__.py create mode 100644 src/codegen/sdk/codebase/codebase_ai.py create mode 100644 src/codegen/sdk/codebase/codebase_analysis.py create mode 100644 src/codegen/sdk/codebase/codebase_context.py create mode 100644 src/codegen/sdk/codebase/config.py create mode 100644 src/codegen/sdk/codebase/config_parser.py create mode 100644 src/codegen/sdk/codebase/diff_lite.py create mode 100644 src/codegen/sdk/codebase/factory/codebase_factory.py create mode 100644 src/codegen/sdk/codebase/factory/get_session.py create mode 100644 src/codegen/sdk/codebase/flagging/code_flag.py create mode 100644 src/codegen/sdk/codebase/flagging/enums.py create mode 100644 src/codegen/sdk/codebase/flagging/flags.py create mode 100644 src/codegen/sdk/codebase/flagging/group.py create mode 100644 src/codegen/sdk/codebase/flagging/groupers/all_grouper.py create mode 100644 src/codegen/sdk/codebase/flagging/groupers/app_grouper.py create mode 100644 src/codegen/sdk/codebase/flagging/groupers/base_grouper.py create mode 100644 src/codegen/sdk/codebase/flagging/groupers/codeowner_grouper.py create mode 100644 src/codegen/sdk/codebase/flagging/groupers/constants.py create mode 100644 src/codegen/sdk/codebase/flagging/groupers/enums.py create mode 100644 src/codegen/sdk/codebase/flagging/groupers/file_chunk_grouper.py create mode 100644 src/codegen/sdk/codebase/flagging/groupers/file_grouper.py create mode 100644 src/codegen/sdk/codebase/flagging/groupers/instance_grouper.py create mode 100644 src/codegen/sdk/codebase/flagging/groupers/utils.py create mode 100644 src/codegen/sdk/codebase/io/file_io.py create mode 100644 src/codegen/sdk/codebase/io/io.py create mode 100644 src/codegen/sdk/codebase/multigraph.py create mode 100644 src/codegen/sdk/codebase/node_classes/__init__.py create mode 100644 src/codegen/sdk/codebase/node_classes/generic_node_classes.py create mode 100644 src/codegen/sdk/codebase/node_classes/node_classes.py create mode 100644 src/codegen/sdk/codebase/node_classes/py_node_classes.py create mode 100644 src/codegen/sdk/codebase/node_classes/ts_node_classes.py create mode 100644 src/codegen/sdk/codebase/progress/progress.py create mode 100644 src/codegen/sdk/codebase/progress/stub_progress.py create mode 100644 src/codegen/sdk/codebase/progress/stub_task.py create mode 100644 src/codegen/sdk/codebase/progress/task.py create mode 100644 src/codegen/sdk/codebase/range_index.py create mode 100644 src/codegen/sdk/codebase/resolution_stack.py create mode 100644 src/codegen/sdk/codebase/span.py create mode 100644 src/codegen/sdk/codebase/transaction_manager.py create mode 100644 src/codegen/sdk/codebase/transactions.py create mode 100644 src/codegen/sdk/codebase/validation.py create mode 100644 src/codegen/sdk/codemods/README.md create mode 100644 src/codegen/sdk/codemods/canonical/__init__.py create mode 100644 src/codegen/sdk/codemods/canonical/add_function_parameter_type_annotations/__init__.py create mode 100644 src/codegen/sdk/codemods/canonical/add_function_parameter_type_annotations/add_function_parameter_type_annotations.py create mode 100644 src/codegen/sdk/codemods/canonical/add_internal_to_non_exported_components/__init__.py create mode 100644 src/codegen/sdk/codemods/canonical/add_internal_to_non_exported_components/add_internal_to_non_exported_components.py create mode 100644 src/codegen/sdk/codemods/canonical/bang_bang_to_boolean/__init__.py create mode 100644 src/codegen/sdk/codemods/canonical/bang_bang_to_boolean/bang_bang_to_boolean.py create mode 100644 src/codegen/sdk/codemods/canonical/built_in_type_annotation/built_in_type_annotation.py create mode 100644 src/codegen/sdk/codemods/canonical/change_component_tag_names/__init__.py create mode 100644 src/codegen/sdk/codemods/canonical/change_component_tag_names/change_component_tag_names.py create mode 100644 src/codegen/sdk/codemods/canonical/classnames_to_backtick.py create mode 100644 src/codegen/sdk/codemods/canonical/convert_array_type_to_square_bracket/__init__.py create mode 100644 src/codegen/sdk/codemods/canonical/convert_array_type_to_square_bracket/convert_array_type_to_square_bracket.py create mode 100644 src/codegen/sdk/codemods/canonical/convert_attribute_to_decorator/__init__.py create mode 100644 src/codegen/sdk/codemods/canonical/convert_attribute_to_decorator/convert_attribute_to_decorator.py create mode 100644 src/codegen/sdk/codemods/canonical/convert_comments_to_JSDoc_style/__init__.py create mode 100644 src/codegen/sdk/codemods/canonical/convert_comments_to_JSDoc_style/convert_comments_to_JSDoc_style.py create mode 100644 src/codegen/sdk/codemods/canonical/convert_docstring_to_google_style/__init__.py create mode 100644 src/codegen/sdk/codemods/canonical/convert_docstring_to_google_style/convert_docstring_to_google_style.py create mode 100644 src/codegen/sdk/codemods/canonical/delete_unused_functions/delete_unused_functions.py create mode 100644 src/codegen/sdk/codemods/canonical/emojify_py_files_codemod/emojify_py_files_codemod.py create mode 100644 src/codegen/sdk/codemods/canonical/enum_mover/enum_mover.py create mode 100644 src/codegen/sdk/codemods/canonical/insert_arguments_to_decorator/__init__.py create mode 100644 src/codegen/sdk/codemods/canonical/insert_arguments_to_decorator/insert_arguments_to_decorator.py create mode 100644 src/codegen/sdk/codemods/canonical/invite_factory_create_params/__init__.py create mode 100644 src/codegen/sdk/codemods/canonical/invite_factory_create_params/invite_factory_create_params.py create mode 100644 src/codegen/sdk/codemods/canonical/js_to_esm_codemod/js_to_esm_codemod.py create mode 100644 src/codegen/sdk/codemods/canonical/mark_as_internal_codemod/mark_as_internal_codemod.py create mode 100644 src/codegen/sdk/codemods/canonical/mark_internal_to_module/mark_internal_to_module.py create mode 100644 src/codegen/sdk/codemods/canonical/mark_is_boolean/__init__.py create mode 100644 src/codegen/sdk/codemods/canonical/mark_is_boolean/mark_is_boolean.py create mode 100644 src/codegen/sdk/codemods/canonical/migrate_class_attributes/migrate_class_attributes.py create mode 100644 src/codegen/sdk/codemods/canonical/move_enums_codemod/move_enums_codemod.py create mode 100644 src/codegen/sdk/codemods/canonical/move_functions_to_new_file/move_functions_to_new_file.py create mode 100644 src/codegen/sdk/codemods/canonical/openapi_add_response_none/openapi_add_response_none.py create mode 100644 src/codegen/sdk/codemods/canonical/openapi_no_reference_request/openapi_no_reference_request.py create mode 100644 src/codegen/sdk/codemods/canonical/pascal_case_symbols/__init__.py create mode 100644 src/codegen/sdk/codemods/canonical/pascal_case_symbols/pascal_case_symbols.py create mode 100644 src/codegen/sdk/codemods/canonical/pivot_return_types/__init__.py create mode 100644 src/codegen/sdk/codemods/canonical/pivot_return_types/pivot_return_types.py create mode 100644 src/codegen/sdk/codemods/canonical/refactor_react_components_into_separate_files/refactor_react_components_into_separate_files.py create mode 100644 src/codegen/sdk/codemods/canonical/remove_indirect_imports/remove_indirect_imports.py create mode 100644 src/codegen/sdk/codemods/canonical/rename_function_parameters/rename_function_parameters.py create mode 100644 src/codegen/sdk/codemods/canonical/rename_local_variables/rename_local_variables.py create mode 100644 src/codegen/sdk/codemods/canonical/replace_prop_values/replace_prop_values.py create mode 100644 src/codegen/sdk/codemods/canonical/return_none_type_annotation/return_none_type_annotation.py create mode 100644 src/codegen/sdk/codemods/canonical/split_decorators/__init__.py create mode 100644 src/codegen/sdk/codemods/canonical/split_decorators/split_decorators.py create mode 100644 src/codegen/sdk/codemods/canonical/split_file/split_file.py create mode 100644 src/codegen/sdk/codemods/canonical/split_file_and_rename_symbols/split_file_and_rename_symbols.py create mode 100644 src/codegen/sdk/codemods/canonical/split_large_files/split_large_files.py create mode 100644 src/codegen/sdk/codemods/canonical/swap_call_site_imports/__init__.py create mode 100644 src/codegen/sdk/codemods/canonical/swap_call_site_imports/swap_call_site_imports.py create mode 100644 src/codegen/sdk/codemods/canonical/swap_class_attribute_usages/swap_class_attribute_usages.py create mode 100644 src/codegen/sdk/codemods/canonical/update_optional_type_annotations/__init__.py create mode 100644 src/codegen/sdk/codemods/canonical/update_optional_type_annotations/update_optional_type_annotations.py create mode 100644 src/codegen/sdk/codemods/canonical/update_union_types/__init__.py create mode 100644 src/codegen/sdk/codemods/canonical/update_union_types/update_union_types.py create mode 100644 src/codegen/sdk/codemods/canonical/use_named_kwargs/use_named_kwargs.py create mode 100644 src/codegen/sdk/codemods/canonical/wrap_with_component/wrap_with_component.py create mode 100644 src/codegen/sdk/codemods/canonical/wrap_with_statement/wrap_with_statement.py create mode 100644 src/codegen/sdk/codemods/codemod.py create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_py_1/expected/file.py create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_py_1/original/file.py create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_py_10/expected/file.py create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_py_10/original/file.py create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_py_2/expected/file.py create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_py_2/expected/foo.py create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_py_2/original/file.py create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_py_3/expected/decorators.py create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_py_3/expected/file.py create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_py_3/original/decorators.py create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_py_3/original/file.py create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_py_4/expected/file.py create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_py_4/expected/main.py create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_py_4/original/file.py create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_py_4/original/main.py create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_py_5/expected/file.py create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_py_5/original/file.py create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_py_6/expected/file.py create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_py_6/original/file.py create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_py_6/original/unused_symbols.py create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_py_7/expected/bar/enums.py create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_py_7/expected/bar/file.py create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_py_7/expected/foo/enums.py create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_py_7/expected/foo/file.py create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_py_7/original/bar/enums.py create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_py_7/original/bar/file.py create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_py_7/original/foo/file.py create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_py_8/expected/file.py create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_py_8/original/file.py create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_py_9/expected/file.py create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_py_9/original/file.py create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_1/expected/file.ts create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_1/original/file.ts create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_10/expected/types.ts create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_10/expected/userService.ts create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_10/original/types.ts create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_10/original/userService.ts create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_2/expected/App.tsx create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_2/expected/Footer.tsx create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_2/expected/Header.tsx create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_2/expected/MainContent.tsx create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_2/expected/Sidebar.tsx create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_2/expected/helpers.ts create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_2/original/App.tsx create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_2/original/Footer.tsx create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_2/original/Header.tsx create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_2/original/helpers.ts create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_3/expected/App.tsx create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_3/expected/Footer.tsx create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_3/expected/Header.tsx create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_3/original/App.tsx create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_3/original/Footer.tsx create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_3/original/Header.tsx create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_3/original/helpers.ts create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_4/expected/App.tsx create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_4/expected/Footer.tsx create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_4/expected/Header.tsx create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_4/expected/helpers.ts create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_4/original/App.tsx create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_4/original/Footer.tsx create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_4/original/Header.tsx create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_4/original/helpers.ts create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_5/expected/App.tsx create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_5/expected/user.router.js create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_5/expected/userHelpers.ts create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_5/original/App.tsx create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_5/original/user.router.js create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_5/original/userHelpers.ts create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_6/expected/App.tsx create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_6/expected/user.router.ts create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_6/expected/userHelpers.ts create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_6/original/App.tsx create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_6/original/user.router.js create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_6/original/userHelpers.ts create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_7/expected/app/App.tsx create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_7/expected/app/userHelpers.ts create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_7/expected/misc/Misc.tsx create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_7/original/app/App.tsx create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_7/original/app/userHelpers.ts create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_7/original/misc/Misc.tsx create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_8/expected/enums.ts create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_8/expected/types.ts create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_8/expected/userService.ts create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_8/original/types.ts create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_8/original/userService.ts create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_9/expected/types.ts create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_9/expected/userService.ts create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_9/original/types.ts create mode 100644 src/codegen/sdk/codemods/eval/test_files/sample_ts_9/original/userService.ts create mode 100644 src/codegen/sdk/compiled/autocommit.pyi create mode 100644 src/codegen/sdk/compiled/autocommit.pyx create mode 100644 src/codegen/sdk/compiled/py.typed create mode 100644 src/codegen/sdk/compiled/resolution.pyi create mode 100644 src/codegen/sdk/compiled/resolution.pyx create mode 100644 src/codegen/sdk/compiled/sort.pyx create mode 100644 src/codegen/sdk/compiled/utils.pyi create mode 100644 src/codegen/sdk/compiled/utils.pyx create mode 100644 src/codegen/sdk/configs/constants.py create mode 100644 src/codegen/sdk/configs/models/base_config.py create mode 100644 src/codegen/sdk/configs/models/codebase.py create mode 100644 src/codegen/sdk/configs/models/repository.py create mode 100644 src/codegen/sdk/configs/models/secrets.py create mode 100644 src/codegen/sdk/configs/models/utils.py create mode 100644 src/codegen/sdk/configs/session_manager.py create mode 100644 src/codegen/sdk/configs/user_config.py create mode 100644 src/codegen/sdk/core/__init__.py create mode 100644 src/codegen/sdk/core/assignment.py create mode 100644 src/codegen/sdk/core/autocommit/__init__.py create mode 100644 src/codegen/sdk/core/autocommit/constants.py create mode 100644 src/codegen/sdk/core/autocommit/decorators.py create mode 100644 src/codegen/sdk/core/autocommit/manager.py create mode 100644 src/codegen/sdk/core/autocommit/ruff.toml create mode 100644 src/codegen/sdk/core/autocommit/utils.py create mode 100644 src/codegen/sdk/core/class_definition.py create mode 100644 src/codegen/sdk/core/codebase.py create mode 100644 src/codegen/sdk/core/codeowner.py create mode 100644 src/codegen/sdk/core/dataclasses/usage.py create mode 100644 src/codegen/sdk/core/detached_symbols/__init__.py create mode 100644 src/codegen/sdk/core/detached_symbols/argument.py create mode 100644 src/codegen/sdk/core/detached_symbols/code_block.py create mode 100644 src/codegen/sdk/core/detached_symbols/decorator.py create mode 100644 src/codegen/sdk/core/detached_symbols/function_call.py create mode 100644 src/codegen/sdk/core/detached_symbols/parameter.py create mode 100644 src/codegen/sdk/core/directory.py create mode 100644 src/codegen/sdk/core/export.py create mode 100644 src/codegen/sdk/core/expressions/__init__.py create mode 100644 src/codegen/sdk/core/expressions/await_expression.py create mode 100644 src/codegen/sdk/core/expressions/binary_expression.py create mode 100644 src/codegen/sdk/core/expressions/boolean.py create mode 100644 src/codegen/sdk/core/expressions/builtin.py create mode 100644 src/codegen/sdk/core/expressions/chained_attribute.py create mode 100644 src/codegen/sdk/core/expressions/comparison_expression.py create mode 100644 src/codegen/sdk/core/expressions/defined_name.py create mode 100644 src/codegen/sdk/core/expressions/expression.py create mode 100644 src/codegen/sdk/core/expressions/generic_type.py create mode 100644 src/codegen/sdk/core/expressions/multi_expression.py create mode 100644 src/codegen/sdk/core/expressions/name.py create mode 100644 src/codegen/sdk/core/expressions/named_type.py create mode 100644 src/codegen/sdk/core/expressions/none_type.py create mode 100644 src/codegen/sdk/core/expressions/number.py create mode 100644 src/codegen/sdk/core/expressions/parenthesized_expression.py create mode 100644 src/codegen/sdk/core/expressions/placeholder_type.py create mode 100644 src/codegen/sdk/core/expressions/string.py create mode 100644 src/codegen/sdk/core/expressions/subscript_expression.py create mode 100644 src/codegen/sdk/core/expressions/ternary_expression.py create mode 100644 src/codegen/sdk/core/expressions/tuple_type.py create mode 100644 src/codegen/sdk/core/expressions/type.py create mode 100644 src/codegen/sdk/core/expressions/unary_expression.py create mode 100644 src/codegen/sdk/core/expressions/union_type.py create mode 100644 src/codegen/sdk/core/expressions/unpack.py create mode 100644 src/codegen/sdk/core/expressions/value.py create mode 100644 src/codegen/sdk/core/external/dependency_manager.py create mode 100644 src/codegen/sdk/core/external/external_process.py create mode 100644 src/codegen/sdk/core/external/language_engine.py create mode 100644 src/codegen/sdk/core/external_module.py create mode 100644 src/codegen/sdk/core/file.py create mode 100644 src/codegen/sdk/core/function.py create mode 100644 src/codegen/sdk/core/import_resolution.py create mode 100644 src/codegen/sdk/core/interface.py create mode 100644 src/codegen/sdk/core/interfaces/__init__.py create mode 100644 src/codegen/sdk/core/interfaces/callable.py create mode 100644 src/codegen/sdk/core/interfaces/chainable.py create mode 100644 src/codegen/sdk/core/interfaces/conditional_block.py create mode 100644 src/codegen/sdk/core/interfaces/editable.py create mode 100644 src/codegen/sdk/core/interfaces/exportable.py create mode 100644 src/codegen/sdk/core/interfaces/has_attribute.py create mode 100644 src/codegen/sdk/core/interfaces/has_block.py create mode 100644 src/codegen/sdk/core/interfaces/has_name.py create mode 100644 src/codegen/sdk/core/interfaces/has_symbols.py create mode 100644 src/codegen/sdk/core/interfaces/has_value.py create mode 100644 src/codegen/sdk/core/interfaces/importable.py create mode 100644 src/codegen/sdk/core/interfaces/inherits.py create mode 100644 src/codegen/sdk/core/interfaces/parseable.py create mode 100644 src/codegen/sdk/core/interfaces/resolvable.py create mode 100644 src/codegen/sdk/core/interfaces/supports_generic.py create mode 100644 src/codegen/sdk/core/interfaces/typeable.py create mode 100644 src/codegen/sdk/core/interfaces/unwrappable.py create mode 100644 src/codegen/sdk/core/interfaces/usable.py create mode 100644 src/codegen/sdk/core/interfaces/wrapper_expression.py create mode 100644 src/codegen/sdk/core/node_id_factory.py create mode 100644 src/codegen/sdk/core/parser.py create mode 100644 src/codegen/sdk/core/placeholder/__init__.py create mode 100644 src/codegen/sdk/core/placeholder/placeholder.py create mode 100644 src/codegen/sdk/core/placeholder/placeholder_stub.py create mode 100644 src/codegen/sdk/core/placeholder/placeholder_type.py create mode 100644 src/codegen/sdk/core/plugins/__init__.py create mode 100644 src/codegen/sdk/core/plugins/axios.py create mode 100644 src/codegen/sdk/core/plugins/flask.py create mode 100644 src/codegen/sdk/core/plugins/modal.py create mode 100644 src/codegen/sdk/core/plugins/plugin.py create mode 100644 src/codegen/sdk/core/statements/__init__.py create mode 100644 src/codegen/sdk/core/statements/assignment_statement.py create mode 100644 src/codegen/sdk/core/statements/attribute.py create mode 100644 src/codegen/sdk/core/statements/block_statement.py create mode 100644 src/codegen/sdk/core/statements/catch_statement.py create mode 100644 src/codegen/sdk/core/statements/comment.py create mode 100644 src/codegen/sdk/core/statements/export_statement.py create mode 100644 src/codegen/sdk/core/statements/expression_statement.py create mode 100644 src/codegen/sdk/core/statements/for_loop_statement.py create mode 100644 src/codegen/sdk/core/statements/if_block_statement.py create mode 100644 src/codegen/sdk/core/statements/import_statement.py create mode 100644 src/codegen/sdk/core/statements/raise_statement.py create mode 100644 src/codegen/sdk/core/statements/return_statement.py create mode 100644 src/codegen/sdk/core/statements/statement.py create mode 100644 src/codegen/sdk/core/statements/switch_case.py create mode 100644 src/codegen/sdk/core/statements/switch_statement.py create mode 100644 src/codegen/sdk/core/statements/symbol_statement.py create mode 100644 src/codegen/sdk/core/statements/try_catch_statement.py create mode 100644 src/codegen/sdk/core/statements/while_statement.py create mode 100644 src/codegen/sdk/core/symbol.py create mode 100644 src/codegen/sdk/core/symbol_group.py create mode 100644 src/codegen/sdk/core/symbol_groups/__init__.py create mode 100644 src/codegen/sdk/core/symbol_groups/collection.py create mode 100644 src/codegen/sdk/core/symbol_groups/comment_group.py create mode 100644 src/codegen/sdk/core/symbol_groups/dict.py create mode 100644 src/codegen/sdk/core/symbol_groups/expression_group.py create mode 100644 src/codegen/sdk/core/symbol_groups/list.py create mode 100644 src/codegen/sdk/core/symbol_groups/multi_line_collection.py create mode 100644 src/codegen/sdk/core/symbol_groups/parents.py create mode 100644 src/codegen/sdk/core/symbol_groups/tuple.py create mode 100644 src/codegen/sdk/core/symbol_groups/type_parameters.py create mode 100644 src/codegen/sdk/core/type_alias.py create mode 100644 src/codegen/sdk/core/utils/cache_utils.py create mode 100644 src/codegen/sdk/enums.py create mode 100644 src/codegen/sdk/extensions/__init__.py create mode 100644 src/codegen/sdk/extensions/attribution/3pp/__init__.py create mode 100644 src/codegen/sdk/extensions/attribution/3pp/cursor.py create mode 100644 src/codegen/sdk/extensions/attribution/3pp/windsurf.py create mode 100644 src/codegen/sdk/extensions/attribution/cli.py create mode 100644 src/codegen/sdk/extensions/attribution/git_history.py create mode 100644 src/codegen/sdk/extensions/attribution/main.py create mode 100644 src/codegen/sdk/extensions/autogenlib/__init__.py create mode 100644 src/codegen/sdk/extensions/autogenlib/_cache.py create mode 100644 src/codegen/sdk/extensions/autogenlib/_caller.py create mode 100644 src/codegen/sdk/extensions/autogenlib/_context.py create mode 100644 src/codegen/sdk/extensions/autogenlib/_exception_handler.py create mode 100644 src/codegen/sdk/extensions/autogenlib/_finder.py create mode 100644 src/codegen/sdk/extensions/autogenlib/_generator.py create mode 100644 src/codegen/sdk/extensions/autogenlib/_state.py create mode 100644 src/codegen/sdk/extensions/clients/linear.py create mode 100644 src/codegen/sdk/extensions/github/__init__.py create mode 100644 src/codegen/sdk/extensions/github/types/__init__.py create mode 100644 src/codegen/sdk/extensions/github/types/author.py create mode 100644 src/codegen/sdk/extensions/github/types/base.py create mode 100644 src/codegen/sdk/extensions/github/types/commit.py create mode 100644 src/codegen/sdk/extensions/github/types/enterprise.py create mode 100644 src/codegen/sdk/extensions/github/types/events/pull_request.py create mode 100644 src/codegen/sdk/extensions/github/types/events/push.py create mode 100644 src/codegen/sdk/extensions/github/types/installation.py create mode 100644 src/codegen/sdk/extensions/github/types/label.py create mode 100644 src/codegen/sdk/extensions/github/types/organization.py create mode 100644 src/codegen/sdk/extensions/github/types/pull_request.py create mode 100644 src/codegen/sdk/extensions/github/types/push.py create mode 100644 src/codegen/sdk/extensions/github/types/pusher.py create mode 100644 src/codegen/sdk/extensions/graph/__init__.py create mode 100644 src/codegen/sdk/extensions/graph/create_graph.py create mode 100644 src/codegen/sdk/extensions/graph/main.py create mode 100644 src/codegen/sdk/extensions/graph/neo4j_exporter.py create mode 100644 src/codegen/sdk/extensions/graph/utils.py create mode 100644 src/codegen/sdk/extensions/index/__init__.py create mode 100644 src/codegen/sdk/extensions/index/code_index.py create mode 100644 src/codegen/sdk/extensions/index/file_index.py create mode 100644 src/codegen/sdk/extensions/index/symbol_index.py create mode 100644 src/codegen/sdk/extensions/linear/__init__.py create mode 100644 src/codegen/sdk/extensions/linear/linear_client.py create mode 100644 src/codegen/sdk/extensions/linear/types.py create mode 100644 src/codegen/sdk/extensions/lsp/codemods/__init__.py create mode 100644 src/codegen/sdk/extensions/lsp/codemods/base.py create mode 100644 src/codegen/sdk/extensions/lsp/codemods/move_symbol_to_file.py create mode 100644 src/codegen/sdk/extensions/lsp/codemods/split_tests.py create mode 100644 src/codegen/sdk/extensions/lsp/completion.py create mode 100644 src/codegen/sdk/extensions/lsp/definition.py create mode 100644 src/codegen/sdk/extensions/lsp/document_symbol.py create mode 100644 src/codegen/sdk/extensions/lsp/execute.py create mode 100644 src/codegen/sdk/extensions/lsp/io.py create mode 100644 src/codegen/sdk/extensions/lsp/kind.py create mode 100644 src/codegen/sdk/extensions/lsp/lsp.py create mode 100644 src/codegen/sdk/extensions/lsp/progress.py create mode 100644 src/codegen/sdk/extensions/lsp/protocol.py create mode 100644 src/codegen/sdk/extensions/lsp/range.py create mode 100644 src/codegen/sdk/extensions/lsp/server.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/.gitignore create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/__init__.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/language_servers/bash_language_server.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/language_servers/clangd_language_server.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/language_servers/clojure_lsp.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/language_servers/common.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/language_servers/csharp_language_server.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/language_servers/dart_language_server.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/language_servers/eclipse_jdtls.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/language_servers/elixir_tools/README.md create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/language_servers/elixir_tools/__init__.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/language_servers/elixir_tools/elixir_tools.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/language_servers/erlang_language_server.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/language_servers/gopls.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/language_servers/intelephense.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/language_servers/jedi_server.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/language_servers/kotlin_language_server.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/language_servers/lua_ls.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/language_servers/nixd_ls.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/language_servers/omnisharp.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/language_servers/omnisharp/initialize_params.json create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/language_servers/omnisharp/runtime_dependencies.json create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/language_servers/omnisharp/workspace_did_change_configuration.json create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/language_servers/pyright_server.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/language_servers/r_language_server.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/language_servers/ruby_lsp.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/language_servers/rust_analyzer.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/language_servers/solargraph.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/language_servers/sourcekit_lsp.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/language_servers/terraform_ls.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/language_servers/typescript_language_server.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/language_servers/vts_language_server.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/language_servers/zls.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/ls.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/ls_config.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/ls_exceptions.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/ls_handler.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/ls_logger.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/ls_request.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/ls_types.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/ls_utils.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/lsp_protocol_handler/lsp_constants.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/lsp_protocol_handler/lsp_requests.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/lsp_protocol_handler/lsp_types.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/lsp_protocol_handler/server.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/settings.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/util/subprocess_util.py create mode 100644 src/codegen/sdk/extensions/lsp/solidlsp/util/zip.py create mode 100644 src/codegen/sdk/extensions/lsp/utils.py create mode 100644 src/codegen/sdk/extensions/mcp/README.md create mode 100644 src/codegen/sdk/extensions/mcp/codebase_mods.py create mode 100644 src/codegen/sdk/extensions/mcp/codebase_tools.py create mode 100644 src/codegen/sdk/extensions/slack/types.py create mode 100644 src/codegen/sdk/extensions/swebench/README.md create mode 100644 src/codegen/sdk/extensions/swebench/__init__.py create mode 100644 src/codegen/sdk/extensions/swebench/enums.py create mode 100644 src/codegen/sdk/extensions/swebench/harness.py create mode 100755 src/codegen/sdk/extensions/swebench/report.py create mode 100644 src/codegen/sdk/extensions/swebench/subsets.py create mode 100644 src/codegen/sdk/extensions/swebench/success_rates.py create mode 100755 src/codegen/sdk/extensions/swebench/tests.py create mode 100644 src/codegen/sdk/extensions/swebench/utils.py create mode 100644 src/codegen/sdk/extensions/tools/README.md create mode 100644 src/codegen/sdk/extensions/tools/bash.py create mode 100644 src/codegen/sdk/extensions/tools/codegen_sdk_codebase.py create mode 100644 src/codegen/sdk/extensions/tools/current_code_codebase.py create mode 100644 src/codegen/sdk/extensions/tools/document_functions.py create mode 100644 src/codegen/sdk/extensions/tools/generate_docs_json.py create mode 100644 src/codegen/sdk/extensions/tools/list_directory.py create mode 100644 src/codegen/sdk/extensions/tools/mdx_docs_generation.py create mode 100644 src/codegen/sdk/extensions/tools/observation.py create mode 100644 src/codegen/sdk/extensions/tools/reflection.py create mode 100644 src/codegen/sdk/extensions/tools/reveal_symbol.py create mode 100644 src/codegen/sdk/extensions/tools/reveal_symbol_fn.py create mode 100644 src/codegen/sdk/extensions/tools/tool_output_types.py create mode 100644 src/codegen/sdk/extensions/tools/tools.py create mode 100644 src/codegen/sdk/extensions/tools/view_file.py create mode 100644 src/codegen/sdk/git/README.md create mode 100644 src/codegen/sdk/git/__init__.py create mode 100644 src/codegen/sdk/git/clients/git_repo_client.py create mode 100644 src/codegen/sdk/git/clients/github_client.py create mode 100644 src/codegen/sdk/git/configs/constants.py create mode 100644 src/codegen/sdk/git/models/codemod_context.py create mode 100644 src/codegen/sdk/git/models/github_named_user_context.py create mode 100644 src/codegen/sdk/git/models/pr_options.py create mode 100644 src/codegen/sdk/git/models/pr_part_context.py create mode 100644 src/codegen/sdk/git/models/pull_request_context.py create mode 100644 src/codegen/sdk/git/py.typed create mode 100644 src/codegen/sdk/git/repo_operator/local_git_repo.py create mode 100644 src/codegen/sdk/git/repo_operator/repo_operator.py create mode 100644 src/codegen/sdk/git/schemas/enums.py create mode 100644 src/codegen/sdk/git/schemas/repo_config.py create mode 100644 src/codegen/sdk/git/utils/clone.py create mode 100644 src/codegen/sdk/git/utils/clone_url.py create mode 100644 src/codegen/sdk/git/utils/codeowner_utils.py create mode 100644 src/codegen/sdk/git/utils/file_utils.py create mode 100644 src/codegen/sdk/git/utils/format.py create mode 100644 src/codegen/sdk/git/utils/language.py create mode 100644 src/codegen/sdk/git/utils/pr_review.py create mode 100644 src/codegen/sdk/git/utils/remote_progress.py create mode 100644 src/codegen/sdk/gsbuild/README.md create mode 100644 src/codegen/sdk/gsbuild/build.py create mode 100644 src/codegen/sdk/gscli/README.md create mode 100644 src/codegen/sdk/gscli/__init__.py create mode 100644 src/codegen/sdk/gscli/backend/__init__.py create mode 100644 src/codegen/sdk/gscli/backend/typestub_utils.py create mode 100644 src/codegen/sdk/gscli/backend/utils.py create mode 100644 src/codegen/sdk/gscli/cli.py create mode 100644 src/codegen/sdk/gscli/generate/__init__.py create mode 100644 src/codegen/sdk/gscli/generate/commands.py create mode 100644 src/codegen/sdk/gscli/generate/runner_imports.py create mode 100644 src/codegen/sdk/gscli/generate/system_prompt.py create mode 100644 src/codegen/sdk/gscli/generate/utils.py create mode 100644 src/codegen/sdk/output/ast.py create mode 100644 src/codegen/sdk/output/constants.py create mode 100644 src/codegen/sdk/output/inspect.py create mode 100644 src/codegen/sdk/output/jsonable.py create mode 100644 src/codegen/sdk/output/placeholder.py create mode 100644 src/codegen/sdk/output/utils.py create mode 100644 src/codegen/sdk/py.typed create mode 100644 src/codegen/sdk/python/__init__.py create mode 100644 src/codegen/sdk/python/assignment.py create mode 100644 src/codegen/sdk/python/class_definition.py create mode 100644 src/codegen/sdk/python/detached_symbols/code_block.py create mode 100644 src/codegen/sdk/python/detached_symbols/decorator.py create mode 100644 src/codegen/sdk/python/detached_symbols/parameter.py create mode 100644 src/codegen/sdk/python/expressions/chained_attribute.py create mode 100644 src/codegen/sdk/python/expressions/conditional_expression.py create mode 100644 src/codegen/sdk/python/expressions/generic_type.py create mode 100644 src/codegen/sdk/python/expressions/named_type.py create mode 100644 src/codegen/sdk/python/expressions/string.py create mode 100644 src/codegen/sdk/python/expressions/type.py create mode 100644 src/codegen/sdk/python/expressions/union_type.py create mode 100644 src/codegen/sdk/python/file.py create mode 100644 src/codegen/sdk/python/function.py create mode 100644 src/codegen/sdk/python/import_resolution.py create mode 100644 src/codegen/sdk/python/interfaces/has_block.py create mode 100644 src/codegen/sdk/python/placeholder/placeholder_return_type.py create mode 100644 src/codegen/sdk/python/statements/__init__.py create mode 100644 src/codegen/sdk/python/statements/assignment_statement.py create mode 100644 src/codegen/sdk/python/statements/attribute.py create mode 100644 src/codegen/sdk/python/statements/block_statement.py create mode 100644 src/codegen/sdk/python/statements/break_statement.py create mode 100644 src/codegen/sdk/python/statements/catch_statement.py create mode 100644 src/codegen/sdk/python/statements/comment.py create mode 100644 src/codegen/sdk/python/statements/for_loop_statement.py create mode 100644 src/codegen/sdk/python/statements/if_block_statement.py create mode 100644 src/codegen/sdk/python/statements/import_statement.py create mode 100644 src/codegen/sdk/python/statements/match_case.py create mode 100644 src/codegen/sdk/python/statements/match_statement.py create mode 100644 src/codegen/sdk/python/statements/pass_statement.py create mode 100644 src/codegen/sdk/python/statements/try_catch_statement.py create mode 100644 src/codegen/sdk/python/statements/while_statement.py create mode 100644 src/codegen/sdk/python/statements/with_statement.py create mode 100644 src/codegen/sdk/python/symbol.py create mode 100644 src/codegen/sdk/python/symbol_groups/comment_group.py create mode 100644 src/codegen/sdk/runner/README.md create mode 100644 src/codegen/sdk/runner/__init__.py create mode 100644 src/codegen/sdk/runner/clients/client.py create mode 100644 src/codegen/sdk/runner/clients/codebase_client.py create mode 100644 src/codegen/sdk/runner/clients/docker_client.py create mode 100644 src/codegen/sdk/runner/constants/envvars.py create mode 100644 src/codegen/sdk/runner/diff/get_raw_diff.py create mode 100644 src/codegen/sdk/runner/enums/warmup_state.py create mode 100644 src/codegen/sdk/runner/models/apis.py create mode 100644 src/codegen/sdk/runner/models/codemod.py create mode 100644 src/codegen/sdk/runner/sandbox/ephemeral_server.py create mode 100644 src/codegen/sdk/runner/sandbox/executor.py create mode 100644 src/codegen/sdk/runner/sandbox/middlewares.py create mode 100644 src/codegen/sdk/runner/sandbox/repo.py create mode 100644 src/codegen/sdk/runner/sandbox/runner.py create mode 100644 src/codegen/sdk/runner/sandbox/server.py create mode 100644 src/codegen/sdk/runner/servers/local_daemon.py create mode 100644 src/codegen/sdk/runner/utils/branch_name.py create mode 100644 src/codegen/sdk/runner/utils/exception_utils.py create mode 100644 src/codegen/sdk/shared/README.md create mode 100644 src/codegen/sdk/shared/__init__.py create mode 100644 src/codegen/sdk/shared/compilation/README.md create mode 100644 src/codegen/sdk/shared/compilation/codeblock_validation.py create mode 100644 src/codegen/sdk/shared/compilation/exception_utils.py create mode 100644 src/codegen/sdk/shared/compilation/function_compilation.py create mode 100644 src/codegen/sdk/shared/compilation/function_construction.py create mode 100644 src/codegen/sdk/shared/compilation/function_imports.py create mode 100644 src/codegen/sdk/shared/compilation/string_to_code.py create mode 100644 src/codegen/sdk/shared/decorators/docs.py create mode 100644 src/codegen/sdk/shared/enums/programming_language.py create mode 100644 src/codegen/sdk/shared/exceptions/api.py create mode 100644 src/codegen/sdk/shared/exceptions/compilation.py create mode 100644 src/codegen/sdk/shared/exceptions/control_flow.py create mode 100644 src/codegen/sdk/shared/logging/get_logger.py create mode 100644 src/codegen/sdk/shared/network/port.py create mode 100644 src/codegen/sdk/shared/path.py create mode 100644 src/codegen/sdk/shared/performance/memory_utils.py create mode 100644 src/codegen/sdk/shared/performance/stopwatch_utils.py create mode 100644 src/codegen/sdk/shared/performance/time_utils.py create mode 100644 src/codegen/sdk/shared/string/csv_utils.py create mode 100644 src/codegen/sdk/system-prompt.txt create mode 100644 src/codegen/sdk/topological_sort.py create mode 100644 src/codegen/sdk/tree_sitter_parser.py create mode 100644 src/codegen/sdk/types.py create mode 100644 src/codegen/sdk/typescript/__init__.py create mode 100644 src/codegen/sdk/typescript/assignment.py create mode 100644 src/codegen/sdk/typescript/class_definition.py create mode 100644 src/codegen/sdk/typescript/config_parser.py create mode 100644 src/codegen/sdk/typescript/detached_symbols/code_block.py create mode 100644 src/codegen/sdk/typescript/detached_symbols/decorator.py create mode 100644 src/codegen/sdk/typescript/detached_symbols/jsx/element.py create mode 100644 src/codegen/sdk/typescript/detached_symbols/jsx/expression.py create mode 100644 src/codegen/sdk/typescript/detached_symbols/jsx/prop.py create mode 100644 src/codegen/sdk/typescript/detached_symbols/parameter.py create mode 100644 src/codegen/sdk/typescript/detached_symbols/promise_chain.py create mode 100644 src/codegen/sdk/typescript/enum_definition.py create mode 100644 src/codegen/sdk/typescript/enums.py create mode 100644 src/codegen/sdk/typescript/export.py create mode 100644 src/codegen/sdk/typescript/expressions/array_type.py create mode 100644 src/codegen/sdk/typescript/expressions/chained_attribute.py create mode 100644 src/codegen/sdk/typescript/expressions/conditional_type.py create mode 100644 src/codegen/sdk/typescript/expressions/expression_type.py create mode 100644 src/codegen/sdk/typescript/expressions/function_type.py create mode 100644 src/codegen/sdk/typescript/expressions/generic_type.py create mode 100644 src/codegen/sdk/typescript/expressions/lookup_type.py create mode 100644 src/codegen/sdk/typescript/expressions/named_type.py create mode 100644 src/codegen/sdk/typescript/expressions/object_type.py create mode 100644 src/codegen/sdk/typescript/expressions/query_type.py create mode 100644 src/codegen/sdk/typescript/expressions/readonly_type.py create mode 100644 src/codegen/sdk/typescript/expressions/string.py create mode 100644 src/codegen/sdk/typescript/expressions/ternary_expression.py create mode 100644 src/codegen/sdk/typescript/expressions/type.py create mode 100644 src/codegen/sdk/typescript/expressions/undefined_type.py create mode 100644 src/codegen/sdk/typescript/expressions/union_type.py create mode 100644 src/codegen/sdk/typescript/external/dependency_manager.py create mode 100644 src/codegen/sdk/typescript/external/mega_racer.py create mode 100644 src/codegen/sdk/typescript/external/ts_analyzer_engine.py create mode 100644 src/codegen/sdk/typescript/external/ts_declassify/ts_declassify.py create mode 100644 src/codegen/sdk/typescript/external/typescript_analyzer/.gitignore create mode 100644 src/codegen/sdk/typescript/external/typescript_analyzer/package.json create mode 100644 src/codegen/sdk/typescript/external/typescript_analyzer/rollup.config.js create mode 100644 src/codegen/sdk/typescript/external/typescript_analyzer/src/analyzer.ts create mode 100644 src/codegen/sdk/typescript/external/typescript_analyzer/src/fs_proxy.ts create mode 100644 src/codegen/sdk/typescript/external/typescript_analyzer/src/fsi.ts create mode 100644 src/codegen/sdk/typescript/external/typescript_analyzer/src/get_type_at_position.ts create mode 100644 src/codegen/sdk/typescript/external/typescript_analyzer/src/index.ts create mode 100644 src/codegen/sdk/typescript/external/typescript_analyzer/src/run_full.ts create mode 100644 src/codegen/sdk/typescript/external/typescript_analyzer/src/test_fsi.ts create mode 100644 src/codegen/sdk/typescript/external/typescript_analyzer/tsconfig.json create mode 100644 src/codegen/sdk/typescript/file.py create mode 100644 src/codegen/sdk/typescript/function.py create mode 100644 src/codegen/sdk/typescript/import_resolution.py create mode 100644 src/codegen/sdk/typescript/interface.py create mode 100644 src/codegen/sdk/typescript/interfaces/has_block.py create mode 100644 src/codegen/sdk/typescript/namespace.py create mode 100644 src/codegen/sdk/typescript/placeholder/placeholder_return_type.py create mode 100644 src/codegen/sdk/typescript/statements/__init__.py create mode 100644 src/codegen/sdk/typescript/statements/assignment_statement.py create mode 100644 src/codegen/sdk/typescript/statements/attribute.py create mode 100644 src/codegen/sdk/typescript/statements/block_statement.py create mode 100644 src/codegen/sdk/typescript/statements/catch_statement.py create mode 100644 src/codegen/sdk/typescript/statements/comment.py create mode 100644 src/codegen/sdk/typescript/statements/for_loop_statement.py create mode 100644 src/codegen/sdk/typescript/statements/if_block_statement.py create mode 100644 src/codegen/sdk/typescript/statements/import_statement.py create mode 100644 src/codegen/sdk/typescript/statements/labeled_statement.py create mode 100644 src/codegen/sdk/typescript/statements/switch_case.py create mode 100644 src/codegen/sdk/typescript/statements/switch_statement.py create mode 100644 src/codegen/sdk/typescript/statements/try_catch_statement.py create mode 100644 src/codegen/sdk/typescript/statements/while_statement.py create mode 100644 src/codegen/sdk/typescript/symbol.py create mode 100644 src/codegen/sdk/typescript/symbol_groups/comment_group.py create mode 100644 src/codegen/sdk/typescript/symbol_groups/dict.py create mode 100644 src/codegen/sdk/typescript/ts_config.py create mode 100644 src/codegen/sdk/typescript/type_alias.py create mode 100644 src/codegen/sdk/utils.py create mode 100644 src/codegen/sdk/visualizations/README.md create mode 100644 src/codegen/sdk/visualizations/blast_radius.py create mode 100644 src/codegen/sdk/visualizations/call_trace.py create mode 100644 src/codegen/sdk/visualizations/dependency_trace.py create mode 100644 src/codegen/sdk/visualizations/enums.py create mode 100644 src/codegen/sdk/visualizations/method_relationships.py create mode 100644 src/codegen/sdk/visualizations/py.typed create mode 100644 src/codegen/sdk/visualizations/visualization_manager.py create mode 100644 src/codegen/sdk/visualizations/viz_utils.py create mode 100644 src/codegen/sdk/writer_decorators.py diff --git a/src/codegen.backup/__init__.py b/src/codegen.backup/__init__.py new file mode 100644 index 000000000..0099c79aa --- /dev/null +++ b/src/codegen.backup/__init__.py @@ -0,0 +1,22 @@ +# file generated by setuptools-scm +# don't change, don't track in version control + +from codegen.agents import Agent + +__all__ = ["__version__", "__version_tuple__", "version", "version_tuple", "Agent"] + +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import Tuple + + VERSION_TUPLE = tuple[int | str, ...] +else: + VERSION_TUPLE = object + +version: str +__version__: str +__version_tuple__: VERSION_TUPLE +version_tuple: VERSION_TUPLE + +__version__ = version = "0.55.8.dev50+gd8c9a1fa" +__version_tuple__ = version_tuple = (0, 55, 8, "dev50", "gd8c9a1fa") diff --git a/src/codegen.backup/__main__.py b/src/codegen.backup/__main__.py new file mode 100644 index 000000000..07b1afa45 --- /dev/null +++ b/src/codegen.backup/__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.backup/agents/README.md b/src/codegen.backup/agents/README.md new file mode 100644 index 000000000..254ed4bc9 --- /dev/null +++ b/src/codegen.backup/agents/README.md @@ -0,0 +1,124 @@ +# Codegen Agents - Python SDK + +This module provides a Python client for interacting with the Codegen AI agents API. + +## Installation + +The Codegen Agent SDK is included as part of the Codegen package. Ensure you have the latest version installed: + +```bash +pip install codegen +``` + +## Usage + +### Basic Example + +```python +from codegen.agents.agent import Agent + +# Initialize the Agent with your organization ID and API token +agent = Agent( + org_id="11", # Your organization ID + token="your_api_token_here", # Your API authentication token + base_url="https://codegen-sh-rest-api.modal.run", # Optional - defaults to this URL +) + +# Run an agent with a prompt +task = agent.run(prompt="Which github repos can you currently access?") + +# Check the initial status +print(task.status) # Returns the current status of the task (e.g., "queued", "in_progress", etc.) + +# Refresh the task to get updated status +task.refresh() + +# Check the updated status +print(task.status) + +# Once task is complete, you can access the result +if task.status == "completed": + print(task.result) +``` + +### Agent Class + +The `Agent` class is the main entry point for interacting with Codegen AI agents: + +```python +Agent(token: str, org_id: Optional[int] = None, base_url: Optional[str] = CODEGEN_BASE_API_URL) +``` + +Parameters: + +- `token` (required): Your API authentication token +- `org_id` (optional): Your organization ID. If not provided, defaults to environment variable `CODEGEN_ORG_ID` or "1" +- `base_url` (optional): API base URL. Defaults to "https://codegen-sh-rest-api.modal.run" + +### Methods + +#### run() + +```python +run(prompt: str) -> AgentTask +``` + +Runs an agent with the given prompt. + +Parameters: + +- `prompt` (required): The instruction for the agent to execute + +Returns: + +- An `AgentTask` object representing the running task + +#### get_status() + +```python +get_status() -> Optional[Dict[str, Any]] +``` + +Gets the status of the current task. + +Returns: + +- A dictionary containing task status information (`id`, `status`, `result`), or `None` if no task has been run + +### AgentTask Class + +The `AgentTask` class represents a running or completed agent task: + +#### Attributes + +- `id`: The unique identifier for the task +- `org_id`: The organization ID +- `status`: Current status of the task (e.g., "queued", "in_progress", "completed", "failed") +- `result`: The task result (available when status is "completed") + +#### Methods + +##### refresh() + +```python +refresh() -> None +``` + +Refreshes the task status from the API. + +## Environment Variables + +- `CODEGEN_ORG_ID`: Default organization ID (used if `org_id` is not provided) + +## Error Handling + +Handle potential API errors using standard try/except blocks: + +```python +try: + task = agent.run(prompt="Your prompt here") + task.refresh() + print(task.status) +except Exception as e: + print(f"Error: {e}") +``` diff --git a/src/codegen.backup/agents/__init__.py b/src/codegen.backup/agents/__init__.py new file mode 100644 index 000000000..d428226e3 --- /dev/null +++ b/src/codegen.backup/agents/__init__.py @@ -0,0 +1,5 @@ +"""Codegen Agent API module.""" + +from codegen.agents.agent import Agent + +__all__ = ["Agent"] diff --git a/src/codegen.backup/agents/agent.py b/src/codegen.backup/agents/agent.py new file mode 100644 index 000000000..78179396d --- /dev/null +++ b/src/codegen.backup/agents/agent.py @@ -0,0 +1,100 @@ +import os +from typing import Any + +from codegen_api_client.api.agents_api import AgentsApi +from codegen_api_client.api_client import ApiClient +from codegen_api_client.configuration import Configuration +from codegen_api_client.models.agent_run_response import AgentRunResponse +from codegen_api_client.models.create_agent_run_input import CreateAgentRunInput + +from codegen.agents.constants import CODEGEN_BASE_API_URL +from codegen.cli.utils.org import resolve_org_id + + +class AgentTask: + """Represents an agent run job.""" + + def __init__(self, task_data: AgentRunResponse, api_client: ApiClient, org_id: int): + self.id = task_data.id + self.org_id = org_id + self.status = task_data.status + self.result = task_data.result + self.web_url = task_data.web_url + self._api_client = api_client + self._agents_api = AgentsApi(api_client) + + def refresh(self) -> None: + """Refresh the job status from the API.""" + if self.id is None: + return + + job_data = self._agents_api.get_agent_run_v1_organizations_org_id_agent_run_agent_run_id_get( + agent_run_id=int(self.id), org_id=int(self.org_id), authorization=f"Bearer {self._api_client.configuration.access_token}" + ) + + # Convert API response to dict for attribute access + job_dict = {} + if hasattr(job_data, "__dict__"): + job_dict = job_data.__dict__ + elif isinstance(job_data, dict): + job_dict = job_data + + self.status = job_dict.get("status") + self.result = job_dict.get("result") + + +class Agent: + """API client for interacting with Codegen AI agents.""" + + def __init__(self, token: str | None, org_id: int | None = None, base_url: str | None = CODEGEN_BASE_API_URL): + """Initialize a new Agent client. + + Args: + token: API authentication token + org_id: Optional organization ID. If not provided, default org will be used. + """ + self.token = token + resolved_org = resolve_org_id(org_id) + if resolved_org is None: + # Keep previous behavior only as last resort to avoid exceptions in legacy paths + resolved_org = int(os.environ.get("CODEGEN_ORG_ID", "1")) + self.org_id = resolved_org + + # Configure API client + config = Configuration(host=base_url, access_token=token) + self.api_client = ApiClient(configuration=config) + self.agents_api = AgentsApi(self.api_client) + + # Current job + self.current_job: AgentTask | None = None + + def run(self, prompt: str) -> AgentTask: + """Run an agent with the given prompt. + + Args: + prompt: The instruction for the agent to execute + + Returns: + Job: A job object representing the agent run + """ + run_input = CreateAgentRunInput(prompt=prompt) + agent_run_response = self.agents_api.create_agent_run_v1_organizations_org_id_agent_run_post( + org_id=int(self.org_id), create_agent_run_input=run_input, authorization=f"Bearer {self.token}", _headers={"Content-Type": "application/json"} + ) + # Convert API response to dict for Job initialization + + job = AgentTask(agent_run_response, self.api_client, self.org_id) + self.current_job = job + return job + + def get_status(self) -> dict[str, Any] | None: + """Get the status of the current job. + + Returns: + dict: A dictionary containing job status information, + or None if no job has been run. + """ + if self.current_job: + self.current_job.refresh() + return {"id": self.current_job.id, "status": self.current_job.status, "result": self.current_job.result, "web_url": self.current_job.web_url} + return None diff --git a/src/codegen.backup/agents/constants.py b/src/codegen.backup/agents/constants.py new file mode 100644 index 000000000..243ab197b --- /dev/null +++ b/src/codegen.backup/agents/constants.py @@ -0,0 +1,6 @@ +import os + +from codegen.cli.api.endpoints import API_ENDPOINT + +# Prefer explicit override; fall back to the CLI's unified API endpoint +CODEGEN_BASE_API_URL = os.environ.get("CODEGEN_API_BASE_URL", API_ENDPOINT.rstrip("/")) diff --git a/src/codegen.backup/cli/README.md b/src/codegen.backup/cli/README.md new file mode 100644 index 000000000..1f09650d8 --- /dev/null +++ b/src/codegen.backup/cli/README.md @@ -0,0 +1,15 @@ +# Codegen CLI + +A codegen module that handles all `codegen` CLI commands. + +### Dependencies + +- [codegen.sdk](https://github.com/codegen-sh/codegen-sdk/tree/develop/src/codegen/sdk) +- [codegen.shared](https://github.com/codegen-sh/codegen-sdk/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.backup/cli/__init__.py b/src/codegen.backup/cli/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/codegen.backup/cli/_env.py b/src/codegen.backup/cli/_env.py new file mode 100644 index 000000000..5a12ba1d0 --- /dev/null +++ b/src/codegen.backup/cli/_env.py @@ -0,0 +1 @@ +ENV = "" diff --git a/src/codegen.backup/cli/api/client.py b/src/codegen.backup/cli/api/client.py new file mode 100644 index 000000000..3854a0b31 --- /dev/null +++ b/src/codegen.backup/cli/api/client.py @@ -0,0 +1,89 @@ +import json +from typing import ClassVar, TypeVar + +import requests +from pydantic import BaseModel +from rich import print as rprint + +from codegen.cli.env.global_env import global_env +from codegen.cli.errors import InvalidTokenError, ServerError + +InputT = TypeVar("InputT", bound=BaseModel) +OutputT = TypeVar("OutputT", bound=BaseModel) + + +class AuthContext(BaseModel): + """Authentication context model.""" + + status: str + + +class Identity(BaseModel): + """User identity model.""" + + auth_context: AuthContext + + +class RestAPI: + """Handles auth + validation with the codegen API.""" + + _session: ClassVar[requests.Session] = requests.Session() + + auth_token: str + + def __init__(self, auth_token: str): + self.auth_token = auth_token + + def _get_headers(self) -> dict[str, str]: + """Get headers with authentication token.""" + return {"Authorization": f"Bearer {self.auth_token}"} + + def _make_request( + self, + method: str, + endpoint: str, + input_data: InputT | None, + output_model: type[OutputT], + ) -> OutputT: + """Make an API request with input validation and response handling.""" + if global_env.DEBUG: + rprint(f"[purple]{method}[/purple] {endpoint}") + if input_data: + rprint(f"{json.dumps(input_data.model_dump(), indent=4)}") + + try: + headers = self._get_headers() + + json_data = input_data.model_dump() if input_data else None + + response = self._session.request( + method, + endpoint, + json=json_data, + headers=headers, + ) + + if response.status_code == 200: + try: + return output_model.model_validate(response.json()) + except ValueError as e: + msg = f"Invalid response format: {e}" + raise ServerError(msg) + elif response.status_code == 401: + msg = "Invalid or expired authentication token" + raise InvalidTokenError(msg) + elif response.status_code == 500: + msg = "The server encountered an error while processing your request" + raise ServerError(msg) + else: + try: + error_json = response.json() + error_msg = error_json.get("detail", error_json) + except Exception: + error_msg = response.text + msg = f"Error ({response.status_code}): {error_msg}" + raise ServerError(msg) + + except requests.RequestException as e: + msg = f"Network error: {e!s}" + raise ServerError(msg) diff --git a/src/codegen.backup/cli/api/endpoints.py b/src/codegen.backup/cli/api/endpoints.py new file mode 100644 index 000000000..9bdb69695 --- /dev/null +++ b/src/codegen.backup/cli/api/endpoints.py @@ -0,0 +1,20 @@ +import os + +from codegen.cli.api.modal import MODAL_PREFIX + +RUN_ENDPOINT = f"https://{MODAL_PREFIX}--cli-run.modal.run" +DOCS_ENDPOINT = f"https://{MODAL_PREFIX}--cli-docs.modal.run" +EXPERT_ENDPOINT = f"https://{MODAL_PREFIX}--cli-ask-expert.modal.run" +IDENTIFY_ENDPOINT = f"https://{MODAL_PREFIX}--cli-identify.modal.run" +CREATE_ENDPOINT = f"https://{MODAL_PREFIX}--cli-create.modal.run" +DEPLOY_ENDPOINT = f"https://{MODAL_PREFIX}--cli-deploy.modal.run" +LOOKUP_ENDPOINT = f"https://{MODAL_PREFIX}--cli-lookup.modal.run" +RUN_ON_PR_ENDPOINT = f"https://{MODAL_PREFIX}--cli-run-on-pull-request.modal.run" +PR_LOOKUP_ENDPOINT = f"https://{MODAL_PREFIX}--cli-pr-lookup.modal.run" +CODEGEN_SYSTEM_PROMPT_URL = "https://gist.githubusercontent.com/jayhack/15681a2ceaccd726f19e6fdb3a44738b/raw/17c08054e3931b3b7fdf424458269c9e607541e8/codegen-system-prompt.txt" +IMPROVE_ENDPOINT = f"https://{MODAL_PREFIX}--cli-improve.modal.run" +MCP_SERVER_ENDPOINT = f"https://{MODAL_PREFIX}--codegen-mcp-server.modal.run/mcp" + +# API ENDPOINT +# Prefer explicit override via CODEGEN_API_BASE_URL; fallback to Modal-derived URL for current ENV +API_ENDPOINT = os.environ.get("CODEGEN_API_BASE_URL", f"https://{MODAL_PREFIX}--rest-api.modal.run/") diff --git a/src/codegen.backup/cli/api/modal.py b/src/codegen.backup/cli/api/modal.py new file mode 100644 index 000000000..16b4e3d62 --- /dev/null +++ b/src/codegen.backup/cli/api/modal.py @@ -0,0 +1,25 @@ +from codegen.cli.env.enums import Environment +from codegen.cli.env.global_env import global_env + + +def get_modal_workspace(): + match global_env.ENV: + case Environment.PRODUCTION: + return "codegen-sh" + case Environment.STAGING: + return "codegen-sh-staging" + case Environment.DEVELOP: + return "codegen-sh-develop" + case _: + msg = f"Invalid environment: {global_env.ENV}" + raise ValueError(msg) + + +def get_modal_prefix(): + workspace = get_modal_workspace() + if global_env.ENV == Environment.DEVELOP and global_env.MODAL_ENVIRONMENT: + return f"{workspace}-{global_env.MODAL_ENVIRONMENT}" + return workspace + + +MODAL_PREFIX = get_modal_prefix() diff --git a/src/codegen.backup/cli/api/schemas.py b/src/codegen.backup/cli/api/schemas.py new file mode 100644 index 000000000..0cb20a619 --- /dev/null +++ b/src/codegen.backup/cli/api/schemas.py @@ -0,0 +1,257 @@ +from enum import Enum +from typing import TypeVar + +from pydantic import BaseModel +from pydantic.fields import Field + +from codegen.cli.utils.schema import SafeBaseModel +from codegen.shared.enums.programming_language import ProgrammingLanguage + +T = TypeVar("T") + + +########################################################################### +# RUN +########################################################################### + + +class CodemodRunType(str, Enum): + """Type of codemod run.""" + + DIFF = "diff" + PR = "pr" + + +class RunCodemodInput(SafeBaseModel): + class BaseRunCodemodInput(SafeBaseModel): + repo_full_name: str + codemod_id: int | None = None + codemod_name: str | None = None + codemod_source: str | None = None + codemod_run_type: CodemodRunType = CodemodRunType.DIFF + template_context: dict[str, str] = Field(default_factory=dict) + + input: BaseRunCodemodInput + + +class RunCodemodOutput(SafeBaseModel): + success: bool = False + web_link: str | None = None + logs: str | None = None + observation: str | None = None + error: str | None = None + + +########################################################################### +# EXPERT +########################################################################### + + +class AskExpertInput(SafeBaseModel): + class BaseAskExpertInput(SafeBaseModel): + query: str + + input: BaseAskExpertInput + + +class AskExpertResponse(SafeBaseModel): + response: str + success: bool + + +########################################################################### +# DOCS +########################################################################### + + +class SerializedExample(SafeBaseModel): + name: str | None = None + description: str | None = None + source: str + language: ProgrammingLanguage + docstring: str = "" + + +class DocsInput(SafeBaseModel): + class BaseDocsInput(SafeBaseModel): + repo_full_name: str + + docs_input: BaseDocsInput + + +class DocsResponse(SafeBaseModel): + docs: dict[str, str] + examples: list[SerializedExample] + language: ProgrammingLanguage + + +########################################################################### +# CREATE +########################################################################### + + +class CreateInput(SafeBaseModel): + class BaseCreateInput(SafeBaseModel): + name: str + query: str + language: ProgrammingLanguage + + input: BaseCreateInput + + +class CreateResponse(SafeBaseModel): + success: bool + response: str + code: str + context: str + + +########################################################################### +# IDENTIFY +########################################################################### + + +class IdentifyResponse(SafeBaseModel): + class AuthContext(SafeBaseModel): + token_id: int + expires_at: str + status: str + user_id: int + + class User(SafeBaseModel): + github_user_id: str + avatar_url: str + auth_user_id: str + created_at: str + email: str + is_contractor: str | None = None + github_username: str + full_name: str | None = None + id: int + last_updated_at: str | None = None + + auth_context: AuthContext + user: User + + +########################################################################### +# DEPLOY +########################################################################### + + +class DeployInput(BaseModel): + """Input for deploying a codemod.""" + + class BaseDeployInput(BaseModel): + codemod_name: str = Field(..., description="Name of the codemod to deploy") + codemod_source: str = Field(..., description="Source code of the codemod") + repo_full_name: str = Field(..., description="Full name of the repository") + lint_mode: bool = Field(default=False, description="Whether this is a PR check/lint mode function") + lint_user_whitelist: list[str] = Field(default_factory=list, description="List of GitHub usernames to notify") + message: str | None = Field(default=None, description="Optional message describing the codemod being deployed.") + arguments_schema: dict | None = Field(default=None, description="Schema of the arguments parameter") + + input: BaseDeployInput = Field(..., description="Input data for deployment") + + +class DeployResponse(BaseModel): + """Response from deploying a codemod.""" + + success: bool = Field(..., description="Whether the deployment was successful") + new: bool = Field(..., description="Whether the codemod is newly created") + codemod_id: int = Field(..., description="ID of the deployed codemod") + version_id: int = Field(..., description="Version ID of the deployed codemod") + url: str = Field(..., description="URL of the deployed codemod") + + +########################################################################### +# LOOKUP +########################################################################### + + +class LookupInput(BaseModel): + """Input for looking up a codemod.""" + + class BaseLookupInput(BaseModel): + codemod_name: str = Field(..., description="Name of the codemod to look up") + repo_full_name: str = Field(..., description="Full name of the repository") + + input: BaseLookupInput = Field(..., description="Input data for lookup") + + +class LookupOutput(BaseModel): + """Response from looking up a codemod.""" + + codemod_id: int = Field(..., description="ID of the codemod") + version_id: int = Field(..., description="Version ID of the codemod") + + +########################################################################### +# PR LOOKUP +########################################################################### + + +class PRSchema(BaseModel): + url: str + title: str + body: str + github_pr_number: int + codegen_pr_id: int + + +class PRLookupInput(BaseModel): + class BasePRLookupInput(BaseModel): + repo_full_name: str + github_pr_number: int + + input: BasePRLookupInput + + +class PRLookupResponse(BaseModel): + pr: PRSchema + + +########################################################################### +# TEST WEBHOOK +########################################################################### + + +class RunOnPRInput(BaseModel): + """Input for testing a webhook against a PR.""" + + class BaseRunOnPRInput(BaseModel): + codemod_name: str = Field(..., description="Name of the codemod to test") + repo_full_name: str = Field(..., description="Full name of the repository") + github_pr_number: int = Field(..., description="GitHub PR number to test against") + language: str | None = Field(..., description="Language of the codemod") + + input: BaseRunOnPRInput = Field(..., description="Input data for webhook test") + + +class RunOnPRResponse(BaseModel): + """Response from testing a webhook.""" + + codemod_id: int = Field(..., description="ID of the codemod") + codemod_run_id: int = Field(..., description="ID of the codemod run") + web_url: str = Field(..., description="URL to view the test results") + + +########################################################################### +# IMPROVE +########################################################################### + + +class ImproveCodemodInput(BaseModel): + class BaseImproveCodemodInput(BaseModel): + codemod: str = Field(..., description="Source code of the codemod to improve") + task: str = Field(..., description="Task to which the codemod should implement to solve") + concerns: list[str] = Field(..., description="A list of issues that were discovered with the current codemod that need to be considered in the next iteration") + context: dict[str, str] = Field(..., description="Additional context for the codemod this can be a list of files that are related, additional information about the task, etc.") + language: ProgrammingLanguage = Field(..., description="Language of the codemod") + + input: BaseImproveCodemodInput = Field(..., description="Input data for improvement") + + +class ImproveCodemodResponse(BaseModel): + success: bool = Field(..., description="Whether the improvement was successful") + codemod_source: str = Field(..., description="Source code of the improved codemod") diff --git a/src/codegen.backup/cli/api/webapp_routes.py b/src/codegen.backup/cli/api/webapp_routes.py new file mode 100644 index 000000000..3588fcc0f --- /dev/null +++ b/src/codegen.backup/cli/api/webapp_routes.py @@ -0,0 +1,4 @@ +# Urls linking to the webapp +from codegen.cli.utils.url import generate_webapp_url + +USER_SECRETS_ROUTE = generate_webapp_url(path="cli-token") diff --git a/src/codegen.backup/cli/auth/constants.py b/src/codegen.backup/cli/auth/constants.py new file mode 100644 index 000000000..5d394bd96 --- /dev/null +++ b/src/codegen.backup/cli/auth/constants.py @@ -0,0 +1,13 @@ +from pathlib import Path + +# Base directories +CONFIG_DIR = Path("~/.codegen").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/src/codegen.backup/cli/auth/decorators.py b/src/codegen.backup/cli/auth/decorators.py new file mode 100644 index 000000000..df4c5781f --- /dev/null +++ b/src/codegen.backup/cli/auth/decorators.py @@ -0,0 +1,46 @@ +import functools +import inspect +from collections.abc import Callable + +import rich +import typer + +from codegen.cli.auth.login import login_routine +from codegen.cli.auth.session import CodegenSession +from codegen.cli.auth.token_manager import TokenManager, get_current_token +from codegen.cli.errors import AuthError +from codegen.cli.rich.pretty_print import pretty_print_error + + +def requires_auth(f: Callable) -> Callable: + """Decorator that ensures a user is authenticated and injects a CodegenSession.""" + + @functools.wraps(f) + def wrapper(*args, **kwargs): + session = CodegenSession.from_active_session() + + # Check for valid session + if session is None: + pretty_print_error("There is currently no active session.\nPlease run 'codegen init' to initialize the project.") + raise typer.Abort() + + if (token := get_current_token()) is None: + rich.print("[yellow]Not authenticated. Let's get you logged in first![/yellow]\n") + login_routine() + else: + try: + token_manager = TokenManager() + token_manager.authenticate_token(token) + except AuthError: + rich.print("[yellow]Authentication token is invalid or expired. Let's get you logged in again![/yellow]\n") + login_routine() + + return f(*args, session=session, **kwargs) + + # Remove the session parameter from the wrapper's signature so Typer doesn't see it + sig = inspect.signature(f) + new_params = [param for name, param in sig.parameters.items() if name != "session"] + new_sig = sig.replace(parameters=new_params) + wrapper.__signature__ = new_sig # type: ignore[attr-defined] + + return wrapper diff --git a/src/codegen.backup/cli/auth/login.py b/src/codegen.backup/cli/auth/login.py new file mode 100644 index 000000000..6905330ed --- /dev/null +++ b/src/codegen.backup/cli/auth/login.py @@ -0,0 +1,85 @@ +import webbrowser + +import rich +import typer + +from codegen.cli.api.webapp_routes import USER_SECRETS_ROUTE +from codegen.cli.auth.token_manager import TokenManager, get_cached_organizations, set_default_organization +from codegen.cli.env.global_env import global_env +from codegen.cli.errors import AuthError +from codegen.cli.utils.simple_selector import simple_org_selector + + +def login_routine(token: str | None = None) -> str: + """Guide user through login flow and return authenticated session. + + Args: + token: Codegen user access token associated with github account + + Returns: + str: The authenticated token + + Raises: + typer.Exit: If login fails + + """ + # Display header like in the main TUI + print("\033[38;2;82;19;217m" + "/" * 20 + " Codegen\033[0m") + print() + + # Try environment variable first + token = token or global_env.CODEGEN_USER_ACCESS_TOKEN + + # If no token provided, guide user through browser flow + if not token: + webbrowser.open_new(USER_SECRETS_ROUTE) + token = typer.prompt(f"Enter your token from {USER_SECRETS_ROUTE}", hide_input=False) + + if not token: + rich.print("[red]Error:[/red] Token must be provided via CODEGEN_USER_ACCESS_TOKEN environment variable or manual input") + raise typer.Exit(1) + + # Validate and store token + try: + token_manager = TokenManager() + token_manager.authenticate_token(token) + rich.print(f"[dim]✓ Stored token and profile to:[/dim] [#ffca85]{token_manager.token_file}[/#ffca85]") + + # Show organization selector if multiple organizations available + organizations = get_cached_organizations() + if organizations and len(organizations) > 1: + rich.print("\n[blue]Multiple organizations found. Please select your default:[/blue]") + selected_org = simple_org_selector(organizations, title="🏢 Select Default Organization") + + if selected_org: + org_id = selected_org.get("id") + org_name = selected_org.get("name") + try: + set_default_organization(org_id, org_name) + rich.print(f"[green]✓ Set default organization:[/green] {org_name}") + except Exception as e: + rich.print(f"[yellow]Warning: Could not set default organization: {e}[/yellow]") + rich.print("[yellow]You can set it later with 'codegen profile'[/yellow]") + else: + rich.print("[yellow]No organization selected. You can set it later with 'codegen profile'[/yellow]") + elif organizations and len(organizations) == 1: + # Single organization - set it automatically + org = organizations[0] + org_id = org.get("id") + org_name = org.get("name") + try: + set_default_organization(org_id, org_name) + rich.print(f"[green]✓ Set default organization:[/green] {org_name}") + except Exception as e: + rich.print(f"[yellow]Warning: Could not set default organization: {e}[/yellow]") + + # After successful login, launch the TUI + print() # Add some space + from codegen.cli.tui.app import run_tui + + run_tui() + + return token + except AuthError as e: + rich.print(f"[red]Error:[/red] {e!s}") + raise typer.Exit(1) diff --git a/src/codegen.backup/cli/auth/session.ipynb b/src/codegen.backup/cli/auth/session.ipynb new file mode 100644 index 000000000..8a90952d2 --- /dev/null +++ b/src/codegen.backup/cli/auth/session.ipynb @@ -0,0 +1,43 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "from codegen.cli.auth.session import CodegenSession\n", + "\n", + "\n", + "# Create a session with the current directory as repo_path\n", + "session = CodegenSession(repo_path=Path(\".\"))\n", + "print(f\"Session: {session}\")\n", + "print(f\"Repo path: {session.repo_path}\")\n", + "print(f\"Config: {session.config}\")\n", + "print(f\"Existing session: {session.existing}\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.0" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/codegen.backup/cli/auth/session.py b/src/codegen.backup/cli/auth/session.py new file mode 100644 index 000000000..65c0407d8 --- /dev/null +++ b/src/codegen.backup/cli/auth/session.py @@ -0,0 +1,93 @@ +from pathlib import Path + +import rich +import typer +from github import BadCredentialsException +from github.MainClass import Github + +from codegen.cli.rich.codeblocks import format_command +from codegen.configs.constants import CODEGEN_DIR_NAME, ENV_FILENAME +from codegen.configs.session_manager import session_manager +from codegen.configs.user_config import UserConfig +from codegen.git.repo_operator.local_git_repo import LocalGitRepo + + +class CodegenSession: + """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(): + rich.print(f"\n[bold red]Error:[/bold red] Path to git repo does not exist at {repo_path}") + raise typer.Abort() + + # Check if it's a valid git repository + try: + LocalGitRepo(repo_path=repo_path) + except Exception: + rich.print(f"\n[bold red]Error:[/bold red] Path {repo_path} is not a valid git repository") + raise typer.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) -> "CodegenSession | 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("codegen 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 and self.local_git.full_name 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 typer.Abort() + + def __str__(self) -> str: + return f"CodegenSession(user={self.config.repository.user_name}, repo={self.config.repository.name})" diff --git a/src/codegen.backup/cli/auth/token_manager.ipynb b/src/codegen.backup/cli/auth/token_manager.ipynb new file mode 100644 index 000000000..145d47608 --- /dev/null +++ b/src/codegen.backup/cli/auth/token_manager.ipynb @@ -0,0 +1,38 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from codegen.cli.auth.token_manager import TokenManager\n", + "\n", + "\n", + "token_manager = TokenManager()\n", + "print(token_manager.get_token())" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.0" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/codegen.backup/cli/auth/token_manager.py b/src/codegen.backup/cli/auth/token_manager.py new file mode 100644 index 000000000..3b8bf4418 --- /dev/null +++ b/src/codegen.backup/cli/auth/token_manager.py @@ -0,0 +1,460 @@ +import json +import os +from pathlib import Path + +from codegen.cli.auth.constants import AUTH_FILE, CONFIG_DIR + +# Simple cache to avoid repeated file I/O +_token_cache = None +_cache_mtime = None + + +class TokenManager: + # Simple token manager to store and retrieve tokens. + # This manager checks if the token is expired before retrieval. + # TODO: add support for refreshing token and re authorization via supabase oauth + def __init__(self): + self.config_dir = CONFIG_DIR + self.token_file = AUTH_FILE + self._ensure_config_dir() + + def _ensure_config_dir(self): + """Create config directory if it doesn't exist.""" + if not os.path.exists(self.config_dir): + Path(self.config_dir).mkdir(parents=True, exist_ok=True) + + def authenticate_token(self, token: str) -> None: + """Store the token locally and fetch organization info.""" + self.save_token_with_org_info(token) + + def save_token_with_org_info(self, token: str) -> None: + """Save api token to disk along with organization info.""" + global _token_cache, _cache_mtime + + # First fetch organization info using the token + try: + import requests + + from codegen.cli.api.endpoints import API_ENDPOINT + + headers = {"Authorization": f"Bearer {token}"} + + # Test token by getting user info + user_response = requests.get(f"{API_ENDPOINT.rstrip('/')}/v1/users/me", headers=headers, timeout=10) + user_response.raise_for_status() + user_data = user_response.json() + + # Get organizations + org_response = requests.get(f"{API_ENDPOINT.rstrip('/')}/v1/organizations", headers=headers, timeout=10) + org_response.raise_for_status() + org_data = org_response.json() + + # Prepare auth data with org info + auth_data = { + "token": token, + "user": {"id": user_data.get("id"), "email": user_data.get("email"), "full_name": user_data.get("full_name"), "github_username": user_data.get("github_username")}, + } + + # Add organization info if available + orgs = org_data.get("items", []) + if orgs and len(orgs) > 0: + # Store ALL organizations in cache for local resolution + all_orgs = [{"id": org.get("id"), "name": org.get("name")} for org in orgs] + primary_org = orgs[0] # Use first org as primary/default + auth_data["organization"] = {"id": primary_org.get("id"), "name": primary_org.get("name"), "all_orgs": all_orgs} + auth_data["organizations_cache"] = all_orgs # Separate cache for easy access + + except requests.RequestException as e: + # If we can't fetch org info, still save the token but without org data + print(f"Warning: Could not fetch organization info: {e}") + auth_data = {"token": token} + except Exception as e: + print(f"Warning: Error fetching user/org info: {e}") + auth_data = {"token": token} + + # Save to file + try: + with open(self.token_file, "w") as f: + json.dump(auth_data, f, indent=2) + + # Secure the file permissions (read/write for owner only) + os.chmod(self.token_file, 0o600) + + # Invalidate cache + _token_cache = None + _cache_mtime = None + except Exception as e: + print(f"Error saving token: {e!s}") + raise + + def save_token(self, token: str) -> None: + """Save api token to disk (legacy method - just saves token).""" + global _token_cache, _cache_mtime + try: + with open(self.token_file, "w") as f: + json.dump({"token": token}, f) + + # Secure the file permissions (read/write for owner only) + os.chmod(self.token_file, 0o600) + + # Invalidate cache + _token_cache = None + _cache_mtime = None + except Exception as e: + print(f"Error saving token: {e!s}") + raise + + def get_token(self) -> str | None: + """Retrieve token from disk if it exists and is valid.""" + try: + if not os.access(self.config_dir, os.R_OK): + return None + + if not os.path.exists(self.token_file): + return None + + with open(self.token_file) as f: + data = json.load(f) + token = data.get("token") + if not token: + return None + + return token + + except (KeyError, OSError) as e: + print(e) + return None + + def clear_token(self) -> None: + """Remove stored token.""" + global _token_cache, _cache_mtime + if os.path.exists(self.token_file): + os.remove(self.token_file) + # Invalidate cache + _token_cache = None + _cache_mtime = None + + def get_auth_data(self) -> dict | None: + """Retrieve complete auth data from disk.""" + try: + if not os.access(self.config_dir, os.R_OK): + return None + + if not os.path.exists(self.token_file): + return None + + with open(self.token_file) as f: + return json.load(f) + except Exception: + return None + + def get_org_id(self) -> int | None: + """Get the stored organization ID.""" + auth_data = self.get_auth_data() + if auth_data and "organization" in auth_data: + org_id = auth_data["organization"].get("id") + if org_id: + try: + return int(org_id) + except (ValueError, TypeError): + return None + return None + + def get_org_name(self) -> str | None: + """Get the stored organization name.""" + auth_data = self.get_auth_data() + if auth_data and "organization" in auth_data: + return auth_data["organization"].get("name") + return None + + def get_user_info(self) -> dict | None: + """Get the stored user info.""" + auth_data = self.get_auth_data() + if auth_data and "user" in auth_data: + return auth_data["user"] + return None + + def get_cached_organizations(self) -> list[dict] | None: + """Get all cached organizations. + + Returns: + List of organization dictionaries with 'id' and 'name' keys, or None if no cache. + """ + auth_data = self.get_auth_data() + if auth_data and "organizations_cache" in auth_data: + return auth_data["organizations_cache"] + # Fallback to legacy format + if auth_data and "organization" in auth_data and "all_orgs" in auth_data["organization"]: + return auth_data["organization"]["all_orgs"] + return None + + def is_org_id_in_cache(self, org_id: int) -> bool: + """Check if an organization ID exists in the local cache. + + Args: + org_id: The organization ID to check + + Returns: + True if the organization ID is found in cache, False otherwise. + """ + cached_orgs = self.get_cached_organizations() + if not cached_orgs: + return False + + return any(org.get("id") == org_id for org in cached_orgs) + + def get_org_name_from_cache(self, org_id: int) -> str | None: + """Get organization name from cache by ID. + + Args: + org_id: The organization ID to look up + + Returns: + Organization name if found in cache, None otherwise. + """ + cached_orgs = self.get_cached_organizations() + if not cached_orgs: + return None + + for org in cached_orgs: + if org.get("id") == org_id: + return org.get("name") + return None + + def set_default_organization(self, org_id: int, org_name: str) -> None: + """Set the default organization in auth.json. + + Args: + org_id: The organization ID to set as default + org_name: The organization name + """ + auth_data = self.get_auth_data() + if not auth_data: + msg = "No authentication data found. Please run 'codegen login' first." + raise ValueError(msg) + + # Verify the org exists in cache + if not self.is_org_id_in_cache(org_id): + msg = f"Organization {org_id} not found in cache. Please run 'codegen login' to refresh." + raise ValueError(msg) + + # Update the organization info + auth_data["organization"] = {"id": org_id, "name": org_name, "all_orgs": auth_data.get("organization", {}).get("all_orgs", [])} + + # Save to file + try: + import json + + with open(self.token_file, "w") as f: + json.dump(auth_data, f, indent=2) + + # Secure the file permissions (read/write for owner only) + os.chmod(self.token_file, 0o600) + + # Invalidate cache + global _token_cache, _cache_mtime + _token_cache = None + _cache_mtime = None + except Exception as e: + msg = f"Error saving default organization: {e}" + raise ValueError(msg) + + +def get_current_token() -> str | None: + """Get the current authentication token if one exists. + + This is a helper function that creates a TokenManager instance and retrieves + the stored token. The token is validated before being returned. + Uses a simple cache to avoid repeated file I/O. + + Returns: + Optional[str]: The current valid api token if one exists. + Returns None if no token exists. + + """ + global _token_cache, _cache_mtime + + try: + # Check if token file exists + if not os.path.exists(AUTH_FILE): + return None + + # Get file modification time + current_mtime = os.path.getmtime(AUTH_FILE) + + # Use cache if file hasn't changed + if _token_cache is not None and _cache_mtime == current_mtime: + return _token_cache + + # Read token from file + token_manager = TokenManager() + token = token_manager.get_token() + + # Update cache + _token_cache = token + _cache_mtime = current_mtime + + return token + except Exception: + # Fall back to uncached version on any error + token_manager = TokenManager() + return token_manager.get_token() + + +def get_current_org_id() -> int | None: + """Get the stored organization ID if available. + + Returns: + Optional[int]: The organization ID if stored, None otherwise. + """ + token_manager = TokenManager() + return token_manager.get_org_id() + + +def get_current_org_name() -> str | None: + """Get the stored organization name if available. + + Returns: + Optional[str]: The organization name if stored, None otherwise. + """ + token_manager = TokenManager() + return token_manager.get_org_name() + + +def get_cached_organizations() -> list[dict] | None: + """Get all cached organizations. + + Returns: + List of organization dictionaries with 'id' and 'name' keys, or None if no cache. + """ + token_manager = TokenManager() + return token_manager.get_cached_organizations() + + +def is_org_id_cached(org_id: int) -> bool: + """Check if an organization ID exists in the local cache. + + Args: + org_id: The organization ID to check + + Returns: + True if the organization ID is found in cache, False otherwise. + """ + token_manager = TokenManager() + return token_manager.is_org_id_in_cache(org_id) + + +def get_org_name_from_cache(org_id: int) -> str | None: + """Get organization name from cache by ID. + + Args: + org_id: The organization ID to look up + + Returns: + Organization name if found in cache, None otherwise. + """ + token_manager = TokenManager() + return token_manager.get_org_name_from_cache(org_id) + + +def get_current_user_info() -> dict | None: + """Get the stored user info if available. + + Returns: + Optional[dict]: The user info if stored, None otherwise. + """ + token_manager = TokenManager() + return token_manager.get_user_info() + + +# Repository caching functions (similar to organization caching) + + +def get_cached_repositories() -> list[dict] | None: + """Get all cached repositories. + + Returns: + List of repository dictionaries with 'id' and 'name' keys, or None if no cache. + """ + token_manager = TokenManager() + auth_data = token_manager.get_auth_data() + if auth_data and "repositories_cache" in auth_data: + return auth_data["repositories_cache"] + return None + + +def cache_repositories(repositories: list[dict]) -> None: + """Cache repositories to local storage. + + Args: + repositories: List of repository dictionaries to cache + """ + token_manager = TokenManager() + auth_data = token_manager.get_auth_data() + if auth_data: + auth_data["repositories_cache"] = repositories + # Save back to file + try: + import json + + with open(token_manager.token_file, "w") as f: + json.dump(auth_data, f, indent=2) + except Exception: + pass # Fail silently + + +def is_repo_id_cached(repo_id: int) -> bool: + """Check if a repository ID exists in the local cache. + + Args: + repo_id: The repository ID to check + + Returns: + True if the repository ID is found in cache, False otherwise. + """ + cached_repos = get_cached_repositories() + if not cached_repos: + return False + + return any(repo.get("id") == repo_id for repo in cached_repos) + + +def get_repo_name_from_cache(repo_id: int) -> str | None: + """Get repository name from cache by ID. + + Args: + repo_id: The repository ID to look up + + Returns: + Repository name if found in cache, None otherwise. + """ + cached_repos = get_cached_repositories() + if not cached_repos: + return None + + for repo in cached_repos: + if repo.get("id") == repo_id: + return repo.get("name") + + return None + + +def get_current_repo_name() -> str | None: + """Get the current repository name from environment or cache.""" + from codegen.cli.utils.repo import get_current_repo_id + + repo_id = get_current_repo_id() + if repo_id: + return get_repo_name_from_cache(repo_id) + return None + + +def set_default_organization(org_id: int, org_name: str) -> None: + """Set the default organization in auth.json. + + Args: + org_id: The organization ID to set as default + org_name: The organization name + """ + token_manager = TokenManager() + return token_manager.set_default_organization(org_id, org_name) diff --git a/src/codegen.backup/cli/claude/__init__.py b/src/codegen.backup/cli/claude/__init__.py new file mode 100644 index 000000000..f16cc633c --- /dev/null +++ b/src/codegen.backup/cli/claude/__init__.py @@ -0,0 +1 @@ +"""Claude Code proxy server and utilities.""" diff --git a/src/codegen.backup/cli/cli.py b/src/codegen.backup/cli/cli.py new file mode 100644 index 000000000..070798df3 --- /dev/null +++ b/src/codegen.backup/cli/cli.py @@ -0,0 +1,154 @@ +import atexit + +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 +from codegen.cli.commands.claude.main import claude +from codegen.cli.commands.config.main import config_command +from codegen.cli.commands.init.main import init +from codegen.cli.commands.integrations.main import integrations_app +from codegen.cli.commands.login.main import login +from codegen.cli.commands.logout.main import logout +from codegen.cli.commands.org.main import org +from codegen.cli.commands.profile.main import profile_app +from codegen.cli.commands.repo.main import repo +from codegen.cli.commands.style_debug.main import style_debug +from codegen.cli.commands.tools.main import tools +from codegen.cli.commands.tui.main import tui +from codegen.cli.commands.update.main import update +from codegen.shared.logging.get_logger import get_logger + +# Initialize logger for CLI command tracking +logger = get_logger(__name__) + +# Set up global exception logging early +try: + from codegen.cli.telemetry.exception_logger import setup_global_exception_logging + + setup_global_exception_logging() +except ImportError: + # Exception logging dependencies not available - continue without it + pass + + +install(show_locals=True) + +# Register telemetry shutdown on exit +try: + from codegen.cli.telemetry.exception_logger import teardown_global_exception_logging + from codegen.cli.telemetry.otel_setup import shutdown_otel_logging + + atexit.register(shutdown_otel_logging) + atexit.register(teardown_global_exception_logging) +except ImportError: + # OTel dependencies not available + pass + + +def version_callback(value: bool): + """Print version and exit.""" + if value: + 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", +) + +# 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("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("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) + +# Add Typer apps as sub-applications (these will handle their own sub-command logging) +main.add_typer(agents_app, name="agents") +main.add_typer(config_command, name="config") +main.add_typer(integrations_app, name="integrations") +main.add_typer(profile_app, name="profile") + + +@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", + ), +): + """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", + }, + ) + 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}", + }, + ) + + +if __name__ == "__main__": + main() diff --git a/src/codegen.backup/cli/commands/agent/__init__.py b/src/codegen.backup/cli/commands/agent/__init__.py new file mode 100644 index 000000000..3e51d8df8 --- /dev/null +++ b/src/codegen.backup/cli/commands/agent/__init__.py @@ -0,0 +1 @@ +"""Agent command module.""" diff --git a/src/codegen.backup/cli/commands/agent/main.py b/src/codegen.backup/cli/commands/agent/main.py new file mode 100644 index 000000000..99b95b7d5 --- /dev/null +++ b/src/codegen.backup/cli/commands/agent/main.py @@ -0,0 +1,471 @@ +"""Agent command for creating remote agent runs.""" + +import json +from pathlib import Path + +import requests +import typer +from rich import box +from rich.console import Console +from rich.panel import Panel +from rich.syntax import Syntax + +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.rich.spinners import create_spinner +from codegen.cli.utils.org import resolve_org_id +from codegen.git.repo_operator.local_git_repo import LocalGitRepo +from codegen.git.repo_operator.repo_operator import RepoOperator +from codegen.git.schemas.repo_config import RepoConfig + +console = Console() + +# Create the agent app +agent_app = typer.Typer(help="Create and manage individual agent runs") + + +@agent_app.command() +def create( + prompt: str = typer.Option(..., "--prompt", "-p", help="The prompt to send to the agent"), + org_id: int | None = typer.Option(None, help="Organization ID (defaults to CODEGEN_ORG_ID/REPOSITORY_ORG_ID or auto-detect)"), + model: str | None = typer.Option(None, help="Model to use for this agent run (optional)"), + repo_id: int | None = typer.Option(None, help="Repository ID to use for this agent run (optional)"), +): + """Create a new agent run with the given prompt.""" + # Get the current token + token = get_current_token() + if not token: + console.print("[red]Error:[/red] Not authenticated. Please run 'codegen login' first.") + raise typer.Exit(1) + + try: + # Resolve org id + resolved_org_id = resolve_org_id(org_id) + if resolved_org_id is None: + console.print("[red]Error:[/red] Organization ID not provided. Pass --org-id, set CODEGEN_ORG_ID, or REPOSITORY_ORG_ID.") + raise typer.Exit(1) + + # Prepare the request payload + payload = { + "prompt": prompt, + } + + if model: + payload["model"] = model + if repo_id: + payload["repo_id"] = repo_id + + # Make API request to create agent run with spinner + spinner = create_spinner("Creating agent run...") + spinner.start() + + try: + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + url = f"{API_ENDPOINT.rstrip('/')}/v1/organizations/{resolved_org_id}/agent/run" + response = requests.post(url, headers=headers, json=payload) + response.raise_for_status() + agent_run_data = response.json() + finally: + spinner.stop() + + # Extract agent run information + run_id = agent_run_data.get("id", "Unknown") + status = agent_run_data.get("status", "Unknown") + web_url = agent_run_data.get("web_url", "") + created_at = agent_run_data.get("created_at", "") + + # Format created date + if created_at: + try: + from datetime import datetime + + dt = datetime.fromisoformat(created_at.replace("Z", "+00:00")) + created_display = dt.strftime("%B %d, %Y at %H:%M") + except (ValueError, TypeError): + created_display = created_at + else: + created_display = "Unknown" + + # Status with emoji + status_display = status + if status == "COMPLETE": + status_display = "✅ Complete" + elif status == "RUNNING": + status_display = "🏃 Running" + elif status == "FAILED": + status_display = "❌ Failed" + elif status == "STOPPED": + status_display = "⏹️ Stopped" + elif status == "PENDING": + status_display = "⏳ Pending" + + # Create result display + result_info = [] + result_info.append(f"[cyan]Agent Run ID:[/cyan] {run_id}") + result_info.append(f"[cyan]Status:[/cyan] {status_display}") + result_info.append(f"[cyan]Created:[/cyan] {created_display}") + if web_url: + result_info.append(f"[cyan]Web URL:[/cyan] {web_url}") + + result_text = "\n".join(result_info) + + console.print( + Panel( + result_text, + title="🤖 [bold]Agent Run Created[/bold]", + border_style="green", + box=box.ROUNDED, + padding=(1, 2), + ) + ) + + # Show next steps + console.print("\n[dim]💡 Track progress with:[/dim] [cyan]codegen agents[/cyan]") + if web_url: + console.print(f"[dim]🌐 View in browser:[/dim] [link]{web_url}[/link]") + + except requests.RequestException as e: + console.print(f"[red]Error creating agent run:[/red] {e}", style="bold red") + if hasattr(e, "response") and e.response is not None: + try: + error_detail = e.response.json().get("detail", "Unknown error") + console.print(f"[red]Details:[/red] {error_detail}") + except (ValueError, KeyError): + pass + raise typer.Exit(1) + except Exception as e: + console.print(f"[red]Unexpected error:[/red] {e}", style="bold red") + raise typer.Exit(1) + + +# Default callback for the agent app +@agent_app.callback(invoke_without_command=True) +def agent_callback(ctx: typer.Context): + """Create and manage individual agent runs.""" + if ctx.invoked_subcommand is None: + # If no subcommand is provided, show help + print(ctx.get_help()) + raise typer.Exit() + + +# For backward compatibility, also allow `codegen agent --prompt "..."`, `codegen agent --id X --json`, and `codegen agent --id X pull` +def agent( + action: str = typer.Argument(None, help="Action to perform: 'pull' to checkout PR branch"), + prompt: str | None = typer.Option(None, "--prompt", "-p", help="The prompt to send to the agent"), + agent_id: int | None = typer.Option(None, "--id", help="Agent run ID to fetch or pull"), + as_json: bool = typer.Option(False, "--json", help="Output raw JSON response"), + org_id: int | None = typer.Option(None, help="Organization ID (defaults to CODEGEN_ORG_ID/REPOSITORY_ORG_ID or auto-detect)"), + model: str | None = typer.Option(None, help="Model to use for this agent run (optional)"), + repo_id: int | None = typer.Option(None, help="Repository ID to use for this agent run (optional)"), +): + """Create a new agent run with the given prompt, fetch an existing agent run by ID, or pull PR branch.""" + if prompt: + # If prompt is provided, create the agent run + create(prompt=prompt, org_id=org_id, model=model, repo_id=repo_id) + elif agent_id and action == "pull": + # If agent ID and pull action provided, pull the PR branch + pull(agent_id=agent_id, org_id=org_id) + elif agent_id: + # If agent ID is provided, fetch the agent run + get(agent_id=agent_id, as_json=as_json, org_id=org_id) + else: + # If none of the above, show help + console.print("[red]Error:[/red] Either --prompt or --id is required") + console.print("Usage:") + console.print(" [cyan]codegen agent --prompt 'Your prompt here'[/cyan] # Create agent run") + console.print(" [cyan]codegen agent --id 123 --json[/cyan] # Fetch agent run as JSON") + console.print(" [cyan]codegen agent --id 123 pull[/cyan] # Pull PR branch") + raise typer.Exit(1) + + +@agent_app.command() +def get( + agent_id: int = typer.Option(..., "--id", help="Agent run ID to fetch"), + as_json: bool = typer.Option(False, "--json", help="Output raw JSON response"), + org_id: int | None = typer.Option(None, help="Organization ID (defaults to CODEGEN_ORG_ID/REPOSITORY_ORG_ID or auto-detect)"), +): + """Fetch and display details for a specific agent run.""" + # Get the current token + token = get_current_token() + if not token: + console.print("[red]Error:[/red] Not authenticated. Please run 'codegen login' first.") + raise typer.Exit(1) + + try: + # Resolve org id (fast, uses stored data) + resolved_org_id = resolve_org_id(org_id) + if resolved_org_id is None: + console.print("[red]Error:[/red] Organization ID not provided. Pass --org-id, set CODEGEN_ORG_ID, or REPOSITORY_ORG_ID.") + raise typer.Exit(1) + + spinner = create_spinner(f"Fetching agent run {agent_id}...") + spinner.start() + + try: + headers = {"Authorization": f"Bearer {token}"} + # Fixed: Use /agent/run/{id} not /agent/runs/{id} + url = f"{API_ENDPOINT.rstrip('/')}/v1/organizations/{resolved_org_id}/agent/run/{agent_id}" + response = requests.get(url, headers=headers) + response.raise_for_status() + agent_data = response.json() + finally: + spinner.stop() + + # Output the data + if as_json: + # Pretty print JSON with syntax highlighting + formatted_json = json.dumps(agent_data, indent=2, sort_keys=True) + syntax = Syntax(formatted_json, "json", theme="monokai", line_numbers=False) + console.print(syntax) + else: + # Display formatted information (fallback for future enhancement) + formatted_json = json.dumps(agent_data, indent=2, sort_keys=True) + syntax = Syntax(formatted_json, "json", theme="monokai", line_numbers=False) + console.print(syntax) + + except requests.HTTPError as e: + # Get organization name for better error messages + org_name = get_current_org_name() + org_display = f"{org_name} ({resolved_org_id})" if org_name else f"organization {resolved_org_id}" + + if e.response.status_code == 404: + console.print(f"[red]Error:[/red] Agent run {agent_id} not found in {org_display}.") + elif e.response.status_code == 403: + console.print(f"[red]Error:[/red] Access denied to agent run {agent_id} in {org_display}. Check your permissions.") + else: + console.print(f"[red]Error:[/red] HTTP {e.response.status_code}: {e}") + raise typer.Exit(1) + except requests.RequestException as e: + console.print(f"[red]Error fetching agent run:[/red] {e}") + raise typer.Exit(1) + except Exception as e: + console.print(f"[red]Unexpected error:[/red] {e}") + raise typer.Exit(1) + + +@agent_app.command() +def pull( + agent_id: int = typer.Option(..., "--id", help="Agent run ID to pull PR branch for"), + org_id: int | None = typer.Option(None, help="Organization ID (defaults to CODEGEN_ORG_ID/REPOSITORY_ORG_ID or auto-detect)"), +): + """Fetch and checkout the PR branch for an agent run.""" + token = get_current_token() + if not token: + console.print("[red]Error:[/red] Not authenticated. Please run 'codegen login' first.") + raise typer.Exit(1) + + resolved_org_id = resolve_org_id(org_id) + if resolved_org_id is None: + console.print("[red]Error:[/red] Organization ID not provided. Pass --org-id, set CODEGEN_ORG_ID, or REPOSITORY_ORG_ID.") + raise typer.Exit(1) + + # Check if we're in a git repository + try: + current_repo = LocalGitRepo(Path.cwd()) + if not current_repo.has_remote(): + console.print("[red]Error:[/red] Current directory is not a git repository with remotes.") + raise typer.Exit(1) + except Exception: + console.print("[red]Error:[/red] Current directory is not a valid git repository.") + raise typer.Exit(1) + + # Fetch agent run data + spinner = create_spinner(f"Fetching agent run {agent_id}...") + spinner.start() + + try: + headers = {"Authorization": f"Bearer {token}"} + url = f"{API_ENDPOINT.rstrip('/')}/v1/organizations/{resolved_org_id}/agent/run/{agent_id}" + response = requests.get(url, headers=headers) + response.raise_for_status() + agent_data = response.json() + except requests.HTTPError as e: + org_name = get_current_org_name() + org_display = f"{org_name} ({resolved_org_id})" if org_name else f"organization {resolved_org_id}" + + if e.response.status_code == 404: + console.print(f"[red]Error:[/red] Agent run {agent_id} not found in {org_display}.") + elif e.response.status_code == 403: + console.print(f"[red]Error:[/red] Access denied to agent run {agent_id} in {org_display}. Check your permissions.") + else: + console.print(f"[red]Error:[/red] HTTP {e.response.status_code}: {e}") + raise typer.Exit(1) + except requests.RequestException as e: + console.print(f"[red]Error fetching agent run:[/red] {e}") + raise typer.Exit(1) + finally: + spinner.stop() + + # Check if agent run has PRs + github_prs = agent_data.get("github_pull_requests", []) + if not github_prs: + console.print(f"[yellow]Warning:[/yellow] Agent run {agent_id} has no associated pull requests.") + raise typer.Exit(1) + + if len(github_prs) > 1: + console.print(f"[yellow]Warning:[/yellow] Agent run {agent_id} has multiple PRs. Using the first one.") + + pr = github_prs[0] + pr_url = pr.get("url") + head_branch_name = pr.get("head_branch_name") + + if not pr_url: + console.print("[red]Error:[/red] PR URL not found in agent run data.") + raise typer.Exit(1) + + if not head_branch_name: + # Try to extract branch name from PR URL as fallback + # GitHub PR URLs often follow patterns like: + # https://github.com/owner/repo/pull/123 + # We can use GitHub API to get the branch name + console.print("[yellow]Info:[/yellow] HEAD branch name not in API response, attempting to fetch from GitHub...") + try: + # Extract owner, repo, and PR number from PR URL manually + # Expected format: https://github.com/owner/repo/pull/123 + if not pr_url.startswith("https://github.com/"): + msg = f"Only GitHub URLs are supported, got: {pr_url}" + raise ValueError(msg) + + # Remove the GitHub base and split the path + path_parts = pr_url.replace("https://github.com/", "").split("/") + if len(path_parts) < 4 or path_parts[2] != "pull": + msg = f"Invalid GitHub PR URL format: {pr_url}" + raise ValueError(msg) + + owner = path_parts[0] + repo = path_parts[1] + pr_number = path_parts[3] + + # Use GitHub API to get PR details + import requests as github_requests + + github_api_url = f"https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}" + + github_response = github_requests.get(github_api_url) + if github_response.status_code == 200: + pr_data = github_response.json() + head_branch_name = pr_data.get("head", {}).get("ref") + if head_branch_name: + console.print(f"[green]✓ Found branch name from GitHub API:[/green] {head_branch_name}") + else: + console.print("[red]Error:[/red] Could not extract branch name from GitHub API response.") + raise typer.Exit(1) + else: + console.print(f"[red]Error:[/red] Failed to fetch PR details from GitHub API (status: {github_response.status_code})") + console.print("[yellow]Tip:[/yellow] The PR may be private or the GitHub API rate limit may be exceeded.") + raise typer.Exit(1) + except Exception as e: + console.print(f"[red]Error:[/red] Could not fetch branch name from GitHub: {e}") + console.print("[yellow]Tip:[/yellow] The backend may need to be updated to include branch information.") + raise typer.Exit(1) + + # Parse PR URL to get repository information + try: + # Extract owner and repo from PR URL manually + # Expected format: https://github.com/owner/repo/pull/123 + if not pr_url.startswith("https://github.com/"): + msg = f"Only GitHub URLs are supported, got: {pr_url}" + raise ValueError(msg) + + # Remove the GitHub base and split the path + path_parts = pr_url.replace("https://github.com/", "").split("/") + if len(path_parts) < 4 or path_parts[2] != "pull": + msg = f"Invalid GitHub PR URL format: {pr_url}" + raise ValueError(msg) + + owner = path_parts[0] + repo = path_parts[1] + pr_repo_full_name = f"{owner}/{repo}" + except Exception as e: + console.print(f"[red]Error:[/red] Could not parse PR URL: {pr_url} - {e}") + raise typer.Exit(1) + + # Check if current repository matches PR repository + current_repo_full_name = current_repo.full_name + if not current_repo_full_name: + console.print("[red]Error:[/red] Could not determine current repository name.") + raise typer.Exit(1) + + if current_repo_full_name.lower() != pr_repo_full_name.lower(): + console.print("[red]Error:[/red] Repository mismatch!") + console.print(f" Current repo: [cyan]{current_repo_full_name}[/cyan]") + console.print(f" PR repo: [cyan]{pr_repo_full_name}[/cyan]") + console.print("[yellow]Tip:[/yellow] Navigate to the correct repository directory first.") + raise typer.Exit(1) + + # Perform git operations with safety checks + try: + repo_config = RepoConfig.from_repo_path(str(Path.cwd())) + repo_operator = RepoOperator(repo_config) + + # Safety check: warn if repository has uncommitted changes + if repo_operator.git_cli.is_dirty(): + console.print("[yellow]⚠️ Warning:[/yellow] You have uncommitted changes in your repository.") + console.print("These changes may be lost when switching branches.") + + # Get user confirmation + confirm = typer.confirm("Do you want to continue? Your changes may be lost.") + if not confirm: + console.print("[yellow]Operation cancelled.[/yellow]") + raise typer.Exit(0) + + console.print("[blue]Proceeding with branch checkout...[/blue]") + + console.print(f"[blue]Repository match confirmed:[/blue] {current_repo_full_name}") + console.print(f"[blue]Fetching and checking out branch:[/blue] {head_branch_name}") + + # Fetch the branch from remote + fetch_spinner = create_spinner("Fetching latest changes from remote...") + fetch_spinner.start() + try: + fetch_result = repo_operator.fetch_remote("origin") + if fetch_result.name != "SUCCESS": + console.print(f"[yellow]Warning:[/yellow] Fetch result: {fetch_result.name}") + except Exception as e: + console.print(f"[red]Error during fetch:[/red] {e}") + raise + finally: + fetch_spinner.stop() + + # Check if the branch already exists locally + local_branches = [b.name for b in repo_operator.git_cli.branches] + if head_branch_name in local_branches: + console.print(f"[yellow]Info:[/yellow] Local branch '{head_branch_name}' already exists. It will be reset to match the remote.") + + # Checkout the remote branch + checkout_spinner = create_spinner(f"Checking out branch {head_branch_name}...") + checkout_spinner.start() + try: + checkout_result = repo_operator.checkout_remote_branch(head_branch_name) + if checkout_result.name == "SUCCESS": + console.print(f"[green]✓ Successfully checked out branch:[/green] {head_branch_name}") + elif checkout_result.name == "NOT_FOUND": + console.print(f"[red]Error:[/red] Branch {head_branch_name} not found on remote.") + console.print("[yellow]Tip:[/yellow] The branch may have been deleted or renamed.") + raise typer.Exit(1) + else: + console.print(f"[yellow]Warning:[/yellow] Checkout result: {checkout_result.name}") + except Exception as e: + console.print(f"[red]Error during checkout:[/red] {e}") + raise + finally: + checkout_spinner.stop() + + # Display success info + console.print( + Panel( + f"[green]✓ Successfully pulled PR branch![/green]\n\n" + f"[cyan]Agent Run:[/cyan] {agent_id}\n" + f"[cyan]Repository:[/cyan] {current_repo_full_name}\n" + f"[cyan]Branch:[/cyan] {head_branch_name}\n" + f"[cyan]PR URL:[/cyan] {pr_url}", + title="🌿 [bold]Branch Checkout Complete[/bold]", + border_style="green", + box=box.ROUNDED, + padding=(1, 2), + ) + ) + + except Exception as e: + console.print(f"[red]Error during git operations:[/red] {e}") + raise typer.Exit(1) diff --git a/src/codegen.backup/cli/commands/agents/__init__.py b/src/codegen.backup/cli/commands/agents/__init__.py new file mode 100644 index 000000000..14f40ba0c --- /dev/null +++ b/src/codegen.backup/cli/commands/agents/__init__.py @@ -0,0 +1 @@ +"""Agents command module.""" diff --git a/src/codegen.backup/cli/commands/agents/main.py b/src/codegen.backup/cli/commands/agents/main.py new file mode 100644 index 000000000..2be9a13df --- /dev/null +++ b/src/codegen.backup/cli/commands/agents/main.py @@ -0,0 +1,165 @@ +"""Agents command for the Codegen CLI.""" + +import requests +import typer +from rich.console import Console +from rich.table import Table + +from codegen.cli.api.endpoints import API_ENDPOINT +from codegen.cli.auth.token_manager import get_current_token +from codegen.cli.rich.spinners import create_spinner +from codegen.cli.utils.org import resolve_org_id +from codegen.shared.logging.get_logger import get_logger + +# Initialize logger +logger = get_logger(__name__) + +console = Console() + +# Create the agents app +agents_app = typer.Typer(help="Manage Codegen agents") + + +@agents_app.command("list") +def list_agents(org_id: int | None = typer.Option(None, help="Organization ID (defaults to CODEGEN_ORG_ID/REPOSITORY_ORG_ID or auto-detect)")): + """List agent runs from the Codegen API.""" + logger.info("Agents list command invoked", extra={"operation": "agents.list", "org_id": org_id, "command": "codegen agents list"}) + + # Get the current token + token = get_current_token() + if not token: + logger.error("Agents list failed - not authenticated", extra={"operation": "agents.list", "error_type": "not_authenticated"}) + console.print("[red]Error:[/red] Not authenticated. Please run 'codegen login' first.") + raise typer.Exit(1) + + try: + # Resolve org id (now fast, uses stored data) + resolved_org_id = resolve_org_id(org_id) + if resolved_org_id is None: + console.print("[red]Error:[/red] Organization ID not provided. Pass --org-id, set CODEGEN_ORG_ID, or REPOSITORY_ORG_ID.") + raise typer.Exit(1) + + # Start spinner for API calls only + spinner = create_spinner("Fetching your recent API agent runs...") + spinner.start() + + try: + headers = {"Authorization": f"Bearer {token}"} + + # Filter to only API source type and current user's agent runs + params = { + "source_type": "API", + # We'll get the user_id from the /users/me endpoint + } + + # First get the current user ID + 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") + + if user_id: + params["user_id"] = user_id + + url = f"{API_ENDPOINT.rstrip('/')}/v1/organizations/{resolved_org_id}/agent/runs" + response = requests.get(url, headers=headers, params=params) + response.raise_for_status() + response_data = response.json() + finally: + spinner.stop() + + # Extract agent runs from the response structure + agent_runs = response_data.get("items", []) + total = response_data.get("total", 0) + page = response_data.get("page", 1) + page_size = response_data.get("page_size", 10) + + if not agent_runs: + console.print("[yellow]No API agent runs found for your user.[/yellow]") + return + + # Create a table to display agent runs + table = Table( + title=f"Your Recent API Agent Runs (Page {page}, Total: {total})", + border_style="blue", + show_header=True, + title_justify="center", + ) + table.add_column("Created", style="dim") + table.add_column("Status", style="white", justify="center") + table.add_column("Summary", style="green") + table.add_column("Link", style="blue") + + # Add agent runs to table + for agent_run in agent_runs: + run_id = str(agent_run.get("id", "Unknown")) + status = agent_run.get("status", "Unknown") + source_type = agent_run.get("source_type", "Unknown") + created_at = agent_run.get("created_at", "Unknown") + + # Use summary from API response (backend now handles extraction) + summary = agent_run.get("summary", "") or "No summary" + + # Status with colored circles + if status == "COMPLETE": + status_display = "[green]●[/green] Complete" + elif status == "ACTIVE": + status_display = "[dim]●[/dim] Active" + elif status == "RUNNING": + status_display = "[dim]●[/dim] Running" + elif status == "CANCELLED": + status_display = "[yellow]●[/yellow] Cancelled" + elif status == "ERROR": + status_display = "[red]●[/red] Error" + elif status == "FAILED": + status_display = "[red]●[/red] Failed" + elif status == "STOPPED": + status_display = "[yellow]●[/yellow] Stopped" + elif status == "PENDING": + status_display = "[dim]●[/dim] Pending" + else: + status_display = "[dim]●[/dim] " + status + + # Format created date (just show date and time, not full timestamp) + if created_at and created_at != "Unknown": + try: + # Parse and format the timestamp to be more readable + from datetime import datetime + + dt = datetime.fromisoformat(created_at.replace("Z", "+00:00")) + created_display = dt.strftime("%m/%d %H:%M") + except (ValueError, TypeError): + created_display = created_at[:16] if len(created_at) > 16 else created_at + else: + created_display = created_at + + # Truncate summary if too long + summary_display = summary[:50] + "..." if summary and len(summary) > 50 else summary or "No summary" + + # Create web link for the agent run + web_url = agent_run.get("web_url") + if not web_url: + # Construct URL if not provided + web_url = f"https://codegen.com/traces/{run_id}" + link_display = web_url + + table.add_row(created_display, status_display, summary_display, link_display) + + console.print(table) + console.print(f"\n[green]Showing {len(agent_runs)} of {total} API agent runs[/green]") + + except requests.RequestException as e: + console.print(f"[red]Error fetching agent runs:[/red] {e}", style="bold red") + raise typer.Exit(1) + except Exception as e: + console.print(f"[red]Unexpected error:[/red] {e}", style="bold red") + raise typer.Exit(1) + + +# Default callback for the agents app +@agents_app.callback(invoke_without_command=True) +def agents_callback(ctx: typer.Context): + """Manage Codegen agents.""" + if ctx.invoked_subcommand is None: + # If no subcommand is provided, run list by default + list_agents(org_id=None) diff --git a/src/codegen.backup/cli/commands/claude/__init__.py b/src/codegen.backup/cli/commands/claude/__init__.py new file mode 100644 index 000000000..82ffa52a8 --- /dev/null +++ b/src/codegen.backup/cli/commands/claude/__init__.py @@ -0,0 +1,2 @@ +"""Claude Code integration commands.""" + diff --git a/src/codegen.backup/cli/commands/claude/claude_log_utils.py b/src/codegen.backup/cli/commands/claude/claude_log_utils.py new file mode 100644 index 000000000..5268726de --- /dev/null +++ b/src/codegen.backup/cli/commands/claude/claude_log_utils.py @@ -0,0 +1,126 @@ +"""Utilities for Claude Code session log management.""" + +import json +import os +import re +from pathlib import Path +from typing import Dict, Any, Optional + + +def get_hyphenated_cwd() -> str: + """Convert current working directory to hyphenated format for Claude log path. + + Returns: + Hyphenated directory name (e.g., "/Users/john/project" -> "users-john-project") + """ + cwd = os.getcwd() + # Remove leading slash and replace slashes and spaces with hyphens + hyphenated = cwd.replace("/", "-").replace(" ", "-").replace("_", "-") + # Remove any double hyphens + hyphenated = re.sub(r"-+", "-", hyphenated) + return hyphenated + + +def get_claude_session_log_path(session_id: str) -> Path: + """Get the path to the Claude session log file. + + Args: + session_id: The Claude session ID + + Returns: + Path to the session log file + """ + claude_dir = Path.home() / ".claude" + projects_dir = claude_dir / "projects" + hyphenated_cwd = get_hyphenated_cwd() + project_dir = projects_dir / hyphenated_cwd + + log_file = project_dir / f"{session_id}.jsonl" + return log_file + + +def parse_jsonl_line(line: str) -> Optional[Dict[str, Any]]: + """Parse a single line from a JSONL file. + + Args: + line: Raw line from JSONL file + + Returns: + Parsed JSON object or None if parsing fails + """ + line = line.strip() + if not line: + return None + + try: + return json.loads(line) + except json.JSONDecodeError: + return None + + +def ensure_log_directory(session_id: str) -> Path: + """Ensure the log directory exists and return the log file path. + + Args: + session_id: The Claude session ID + + Returns: + Path to the session log file + """ + log_path = get_claude_session_log_path(session_id) + log_path.parent.mkdir(parents=True, exist_ok=True) + return log_path + + +def read_existing_log_lines(log_path: Path) -> int: + """Count existing lines in a log file. + + Args: + log_path: Path to the log file + + Returns: + Number of existing lines + """ + if not log_path.exists(): + return 0 + + try: + with open(log_path, "r", encoding="utf-8") as f: + return sum(1 for _ in f) + except (OSError, UnicodeDecodeError): + return 0 + + +def validate_log_entry(log_entry: Dict[str, Any]) -> bool: + """Validate a log entry before sending to API. + + Args: + log_entry: The log entry to validate + + Returns: + True if valid, False otherwise + """ + if not isinstance(log_entry, dict): + return False + + # Basic validation - ensure it has some content + if not log_entry: + return False + + # Optionally validate specific fields that Claude Code uses + # This can be expanded based on actual Claude log format + return True + + +def format_log_for_api(log_entry: Dict[str, Any]) -> Dict[str, Any]: + """Format a log entry for sending to the API. + + Args: + log_entry: Raw log entry from Claude + + Returns: + Formatted log entry ready for API + """ + # For now, pass through as-is since API expects dict[str, Any] + # This can be enhanced to transform or filter fields as needed + return log_entry diff --git a/src/codegen.backup/cli/commands/claude/claude_log_watcher.py b/src/codegen.backup/cli/commands/claude/claude_log_watcher.py new file mode 100644 index 000000000..5c6416702 --- /dev/null +++ b/src/codegen.backup/cli/commands/claude/claude_log_watcher.py @@ -0,0 +1,274 @@ +"""Claude Code session log watcher implementation.""" + +import time +import threading +from pathlib import Path +from typing import Optional, Callable, Dict, Any + +from .quiet_console import console + +from .claude_log_utils import get_claude_session_log_path, parse_jsonl_line, read_existing_log_lines, validate_log_entry, format_log_for_api +from .claude_session_api import send_claude_session_log + + +class ClaudeLogWatcher: + """Watches Claude Code session log files for new entries and sends them to the API.""" + + def __init__(self, session_id: str, org_id: Optional[int] = None, poll_interval: float = 1.0, on_log_entry: Optional[Callable[[Dict[str, Any]], None]] = None): + """Initialize the log watcher. + + Args: + session_id: The Claude session ID to watch + org_id: Organization ID for API calls + poll_interval: How often to check for new entries (seconds) + on_log_entry: Optional callback for each new log entry + """ + self.session_id = session_id + self.org_id = org_id + self.poll_interval = poll_interval + self.on_log_entry = on_log_entry + + self.log_path = get_claude_session_log_path(session_id) + self.last_line_count = 0 + self.is_running = False + self.watcher_thread: Optional[threading.Thread] = None + + # Stats + self.total_entries_processed = 0 + self.total_entries_sent = 0 + self.total_send_failures = 0 + + def start(self) -> bool: + """Start the log watcher in a background thread. + + Returns: + True if started successfully, False otherwise + """ + if self.is_running: + console.print(f"⚠️ Log watcher for session {self.session_id[:8]}... is already running", style="yellow") + return False + + # Initialize line count + self.last_line_count = read_existing_log_lines(self.log_path) + + self.is_running = True + self.watcher_thread = threading.Thread(target=self._watch_loop, daemon=True) + self.watcher_thread.start() + + console.print(f"📋 Started log watcher for session {self.session_id[:8]}...", style="green") + console.print(f" Log file: {self.log_path}", style="dim") + console.print(f" Starting from line: {self.last_line_count + 1}", style="dim") + + return True + + def stop(self) -> None: + """Stop the log watcher.""" + if not self.is_running: + return + + self.is_running = False + + if self.watcher_thread and self.watcher_thread.is_alive(): + self.watcher_thread.join(timeout=2.0) + + console.print(f"📋 Stopped log watcher for session {self.session_id[:8]}...", style="dim") + console.print(f" Processed: {self.total_entries_processed} entries", style="dim") + console.print(f" Sent: {self.total_entries_sent} entries", style="dim") + if self.total_send_failures > 0: + console.print(f" Failures: {self.total_send_failures} entries", style="yellow") + + def _watch_loop(self) -> None: + """Main watching loop that runs in a background thread.""" + while self.is_running: + try: + self._check_for_new_entries() + time.sleep(self.poll_interval) + except Exception as e: + console.print(f"⚠️ Error in log watcher: {e}", style="yellow") + time.sleep(self.poll_interval * 2) # Back off on errors + + def _check_for_new_entries(self) -> None: + """Check for new log entries and process them.""" + if not self.log_path.exists(): + return + + try: + current_line_count = read_existing_log_lines(self.log_path) + + if current_line_count > self.last_line_count: + new_entries = self._read_new_lines(self.last_line_count, current_line_count) + + for entry in new_entries: + self._process_log_entry(entry) + + self.last_line_count = current_line_count + + except Exception as e: + console.print(f"⚠️ Error reading log file: {e}", style="yellow") + + def _read_new_lines(self, start_line: int, end_line: int) -> list[Dict[str, Any]]: + """Read new lines from the log file. + + Args: + start_line: Line number to start from (0-indexed) + end_line: Line number to end at (0-indexed, exclusive) + + Returns: + List of parsed log entries + """ + entries = [] + + try: + with open(self.log_path, "r", encoding="utf-8") as f: + lines = f.readlines() + + # Read only the new lines + for i in range(start_line, min(end_line, len(lines))): + line = lines[i] + entry = parse_jsonl_line(line) + + if entry is not None: + entries.append(entry) + + except (OSError, UnicodeDecodeError) as e: + console.print(f"⚠️ Error reading log file: {e}", style="yellow") + + return entries + + def _process_log_entry(self, log_entry: Dict[str, Any]) -> None: + """Process a single log entry. + + Args: + log_entry: The parsed log entry + """ + self.total_entries_processed += 1 + + # Validate the entry + if not validate_log_entry(log_entry): + console.print(f"⚠️ Invalid log entry skipped: {log_entry}", style="yellow") + return + + # Format for API + formatted_entry = format_log_for_api(log_entry) + + # Call optional callback + if self.on_log_entry: + try: + self.on_log_entry(formatted_entry) + except Exception as e: + console.print(f"⚠️ Error in log entry callback: {e}", style="yellow") + + # Send to API + self._send_log_entry(formatted_entry) + + def _send_log_entry(self, log_entry: Dict[str, Any]) -> None: + """Send a log entry to the API. + + Args: + log_entry: The formatted log entry + """ + try: + success = send_claude_session_log(self.session_id, log_entry, self.org_id) + + if success: + self.total_entries_sent += 1 + # Only show verbose output in debug mode + console.print(f"📤 Sent log entry: {log_entry.get('type', 'unknown')}", style="dim") + else: + self.total_send_failures += 1 + + except Exception as e: + self.total_send_failures += 1 + console.print(f"⚠️ Failed to send log entry: {e}", style="yellow") + + def get_stats(self) -> Dict[str, Any]: + """Get watcher statistics. + + Returns: + Dictionary with watcher stats + """ + return { + "session_id": self.session_id, + "is_running": self.is_running, + "log_path": str(self.log_path), + "log_file_exists": self.log_path.exists(), + "last_line_count": self.last_line_count, + "total_entries_processed": self.total_entries_processed, + "total_entries_sent": self.total_entries_sent, + "total_send_failures": self.total_send_failures, + "success_rate": (self.total_entries_sent / max(1, self.total_entries_processed) * 100 if self.total_entries_processed > 0 else 0), + } + + +class ClaudeLogWatcherManager: + """Manages multiple log watchers for different sessions.""" + + def __init__(self): + self.watchers: Dict[str, ClaudeLogWatcher] = {} + + def start_watcher(self, session_id: str, org_id: Optional[int] = None, poll_interval: float = 1.0, on_log_entry: Optional[Callable[[Dict[str, Any]], None]] = None) -> bool: + """Start a log watcher for a session. + + Args: + session_id: The Claude session ID + org_id: Organization ID for API calls + poll_interval: How often to check for new entries (seconds) + on_log_entry: Optional callback for each new log entry + + Returns: + True if started successfully, False otherwise + """ + if session_id in self.watchers: + console.print(f"⚠️ Watcher for session {session_id[:8]}... already exists", style="yellow") + return False + + watcher = ClaudeLogWatcher(session_id=session_id, org_id=org_id, poll_interval=poll_interval, on_log_entry=on_log_entry) + + if watcher.start(): + self.watchers[session_id] = watcher + return True + return False + + def stop_watcher(self, session_id: str) -> None: + """Stop a log watcher for a session. + + Args: + session_id: The Claude session ID + """ + if session_id in self.watchers: + self.watchers[session_id].stop() + del self.watchers[session_id] + + def stop_all_watchers(self) -> None: + """Stop all active watchers.""" + for session_id in list(self.watchers.keys()): + self.stop_watcher(session_id) + + def get_active_sessions(self) -> list[str]: + """Get list of active session IDs being watched. + + Returns: + List of session IDs + """ + return list(self.watchers.keys()) + + def get_watcher_stats(self, session_id: str) -> Optional[Dict[str, Any]]: + """Get stats for a specific watcher. + + Args: + session_id: The Claude session ID + + Returns: + Watcher stats or None if not found + """ + if session_id in self.watchers: + return self.watchers[session_id].get_stats() + return None + + def get_all_stats(self) -> Dict[str, Dict[str, Any]]: + """Get stats for all active watchers. + + Returns: + Dictionary mapping session IDs to their stats + """ + return {session_id: watcher.get_stats() for session_id, watcher in self.watchers.items()} diff --git a/src/codegen.backup/cli/commands/claude/claude_session_api.py b/src/codegen.backup/cli/commands/claude/claude_session_api.py new file mode 100644 index 000000000..6ff5e5566 --- /dev/null +++ b/src/codegen.backup/cli/commands/claude/claude_session_api.py @@ -0,0 +1,214 @@ +"""API client for Claude Code session management.""" + +import json +import uuid +from typing import Optional + +import requests +from .quiet_console import console + +from codegen.cli.api.endpoints import API_ENDPOINT +from codegen.cli.auth.token_manager import get_current_token +from codegen.cli.utils.org import resolve_org_id + + +class ClaudeSessionAPIError(Exception): + """Exception raised for Claude session API errors.""" + + pass + + +def generate_session_id() -> str: + """Generate a unique session ID for Claude Code session tracking.""" + return str(uuid.uuid4()) + + +def create_claude_session(session_id: str, org_id: Optional[int] = None) -> Optional[str]: + """Create a new Claude Code session in the backend. + + Args: + session_id: The session ID to register + org_id: Organization ID (will be resolved if None) + + Returns: + Agent run ID if successful, None if failed + + Raises: + ClaudeSessionAPIError: If the API call fails + """ + try: + # Resolve org_id + resolved_org_id = resolve_org_id(org_id) + if resolved_org_id is None: + console.print("⚠️ Could not resolve organization ID for session creation", style="yellow") + return None + + # Get authentication token + token = get_current_token() + if not token: + console.print("⚠️ No authentication token found for session creation", style="yellow") + return None + + # Prepare API request + url = f"{API_ENDPOINT.rstrip('/')}/v1/organizations/{resolved_org_id}/claude_code/session" + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + payload = {"session_id": session_id} + + # Make API request + response = requests.post(url, json=payload, headers=headers, timeout=30) + + if response.status_code == 200: + try: + result = response.json() + agent_run_id = result.get("agent_run_id") + return agent_run_id + except (json.JSONDecodeError, KeyError) as e: + console.print(f"⚠️ Invalid response format from session creation: {e}", style="yellow") + return None + else: + error_msg = f"HTTP {response.status_code}" + try: + error_detail = response.json().get("detail", response.text) + error_msg = f"{error_msg}: {error_detail}" + except Exception: + error_msg = f"{error_msg}: {response.text}" + + console.print(f"⚠️ Failed to create Claude session: {error_msg}", style="yellow") + return None + + except requests.RequestException as e: + console.print(f"⚠️ Network error creating Claude session: {e}", style="yellow") + return None + except Exception as e: + console.print(f"⚠️ Unexpected error creating Claude session: {e}", style="yellow") + return None + + +def update_claude_session_status(session_id: str, status: str, org_id: Optional[int] = None) -> bool: + """Update a Claude Code session status in the backend. + + Args: + session_id: The session ID to update + status: Session status ("COMPLETE", "ERROR", "ACTIVE", etc.) + org_id: Organization ID (will be resolved if None) + + Returns: + True if successful, False if failed + """ + try: + # Resolve org_id + resolved_org_id = resolve_org_id(org_id) + if resolved_org_id is None: + console.print("⚠️ Could not resolve organization ID for session status update", style="yellow") + return False + + # Get authentication token + token = get_current_token() + if not token: + console.print("⚠️ No authentication token found for session status update", style="yellow") + return False + + # Prepare API request + url = f"{API_ENDPOINT.rstrip('/')}/v1/organizations/{resolved_org_id}/claude_code/session/{session_id}/status" + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + payload = {"status": status} + + # Make API request + response = requests.post(url, json=payload, headers=headers, timeout=30) + + if response.status_code == 200: + status_emoji = "✅" if status == "COMPLETE" else "🔄" if status == "ACTIVE" else "❌" + console.print(f"{status_emoji} Updated Claude session {session_id[:8]}... status to {status}", style="green") + return True + else: + error_msg = f"HTTP {response.status_code}" + try: + error_detail = response.json().get("detail", response.text) + error_msg = f"{error_msg}: {error_detail}" + except Exception: + error_msg = f"{error_msg}: {response.text}" + + console.print(f"⚠️ Failed to update Claude session status: {error_msg}", style="yellow") + return False + + except requests.RequestException as e: + console.print(f"⚠️ Network error updating Claude session status: {e}", style="yellow") + return False + except Exception as e: + console.print(f"⚠️ Unexpected error updating Claude session status: {e}", style="yellow") + return False + + +def send_claude_session_log(session_id: str, log_entry: dict, org_id: Optional[int] = None) -> bool: + """Send a log entry to the Claude Code session log endpoint. + + Args: + session_id: The session ID + log_entry: The log entry to send (dict) + org_id: Organization ID (will be resolved if None) + + Returns: + True if successful, False if failed + """ + try: + # Resolve org_id + resolved_org_id = resolve_org_id(org_id) + if resolved_org_id is None: + console.print("⚠️ Could not resolve organization ID for log sending", style="yellow") + return False + + # Get authentication token + token = get_current_token() + if not token: + console.print("⚠️ No authentication token found for log sending", style="yellow") + return False + + # Prepare API request + url = f"{API_ENDPOINT.rstrip('/')}/v1/organizations/{resolved_org_id}/claude_code/session/{session_id}/log" + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + payload = {"log": log_entry} + + # Make API request + response = requests.post(url, json=payload, headers=headers, timeout=30) + + if response.status_code == 200: + return True + else: + error_msg = f"HTTP {response.status_code}" + try: + error_detail = response.json().get("detail", response.text) + error_msg = f"{error_msg}: {error_detail}" + except Exception: + error_msg = f"{error_msg}: {response.text}" + + console.print(f"⚠️ Failed to send log entry: {error_msg}", style="yellow") + return False + + except requests.RequestException as e: + console.print(f"⚠️ Network error sending log entry: {e}", style="yellow") + return False + except Exception as e: + console.print(f"⚠️ Unexpected error sending log entry: {e}", style="yellow") + return False + + +def write_session_hook_data(session_id: str, org_id: Optional[int] = None) -> str: + """Write session data for Claude hook and create session via API. + + This function is called by the Claude hook to both write session data locally + and create the session in the backend API. + + Args: + session_id: The session ID + org_id: Organization ID + + Returns: + JSON string to write to the session file + """ + # Create session in backend API + agent_run_id = create_claude_session(session_id, org_id) + + # Prepare session data + session_data = {"session_id": session_id, "agent_run_id": agent_run_id, "org_id": resolve_org_id(org_id)} + + return json.dumps(session_data, indent=2) diff --git a/src/codegen.backup/cli/commands/claude/config/claude_session_active_hook.py b/src/codegen.backup/cli/commands/claude/config/claude_session_active_hook.py new file mode 100644 index 000000000..f48767242 --- /dev/null +++ b/src/codegen.backup/cli/commands/claude/config/claude_session_active_hook.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +"""Claude Code user prompt submit hook for API integration. + +This script is called by Claude Code on UserPromptSubmit to: +1. Read the session context (session_id, org_id) +2. Send an ACTIVE status to the backend API +""" + +import json +import os +import sys +from pathlib import Path + +# Add the codegen CLI to the path so we can import from it +script_dir = Path(__file__).parent +codegen_cli_dir = script_dir.parent.parent.parent.parent +sys.path.insert(0, str(codegen_cli_dir)) + +try: + from codegen.cli.commands.claude.claude_session_api import update_claude_session_status +except ImportError: + update_claude_session_status = None + + +def read_session_file() -> dict: + """Read session data written by the SessionStart hook, if available.""" + session_path = Path.home() / ".codegen" / "claude-session.json" + if not session_path.exists(): + return {} + try: + with open(session_path) as f: + return json.load(f) + except Exception: + return {} + + +def main(): + try: + # Prefer environment variables set by the CLI wrapper + session_id = os.environ.get("CODEGEN_CLAUDE_SESSION_ID") + org_id = os.environ.get("CODEGEN_CLAUDE_ORG_ID") + + # Fallback to reading the session file + if not session_id or not org_id: + data = read_session_file() + session_id = session_id or data.get("session_id") + org_id = org_id or data.get("org_id") + + # Normalize org_id type + if isinstance(org_id, str): + try: + org_id = int(org_id) + except ValueError: + org_id = None + + if update_claude_session_status and session_id: + update_claude_session_status(session_id, "ACTIVE", org_id) + + # Print minimal output + print(json.dumps({"session_id": session_id, "status": "ACTIVE"})) + + except Exception as e: + print(json.dumps({"error": str(e)})) + + +if __name__ == "__main__": + main() diff --git a/src/codegen.backup/cli/commands/claude/config/claude_session_hook.py b/src/codegen.backup/cli/commands/claude/config/claude_session_hook.py new file mode 100755 index 000000000..e9a1f138d --- /dev/null +++ b/src/codegen.backup/cli/commands/claude/config/claude_session_hook.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Claude Code session hook script for API integration. + +This script is called by Claude Code on SessionStart to: +1. Create a session in the backend API +2. Write session data to local file for tracking +""" + +import json +import os +import sys +from pathlib import Path + +# Add the codegen CLI to the path so we can import from it +script_dir = Path(__file__).parent +codegen_cli_dir = script_dir.parent.parent.parent.parent +sys.path.insert(0, str(codegen_cli_dir)) + +try: + from codegen.cli.commands.claude.claude_session_api import create_claude_session + from codegen.cli.utils.org import resolve_org_id +except ImportError: + create_claude_session = None + + +def main(): + """Main hook function called by Claude Code.""" + try: + # Read hook input from stdin (Claude passes JSON data) + input_data = {} + try: + if not sys.stdin.isatty(): + input_text = sys.stdin.read().strip() + if input_text: + input_data = json.loads(input_text) + except (json.JSONDecodeError, Exception): + # If we can't read the input, continue with empty data + pass + + # Get session ID from environment variable (set by main.py) + session_id = os.environ.get("CODEGEN_CLAUDE_SESSION_ID") + if not session_id: + # Fallback: try to extract from input data + session_id = input_data.get("session_id") + + if not session_id: + # Generate a basic session ID if none available + import uuid + + session_id = str(uuid.uuid4()) + + # Get org_id from environment variable (set by main.py) + org_id_str = os.environ.get("CODEGEN_CLAUDE_ORG_ID") + org_id = None + if org_id_str: + try: + org_id = int(org_id_str) + except ValueError: + pass + + # If we don't have org_id, try to resolve it + if org_id is None and resolve_org_id: + org_id = resolve_org_id(None) + + # Create session via API if available + agent_run_id = None + if org_id: + agent_run_id = create_claude_session(session_id, org_id) + + # Prepare session data + session_data = {"session_id": session_id, "agent_run_id": agent_run_id, "org_id": org_id, "hook_event": input_data.get("hook_event_name"), "timestamp": input_data.get("timestamp")} + + # Output the session data (this gets written to the session file by the hook command) + print(json.dumps(session_data, indent=2)) + + except Exception as e: + # If anything fails, at least output basic session data + session_id = os.environ.get("CODEGEN_CLAUDE_SESSION_ID", "unknown") + fallback_data = {"session_id": session_id, "error": str(e), "agent_run_id": None, "org_id": None} + print(json.dumps(fallback_data, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/src/codegen.backup/cli/commands/claude/config/claude_session_stop_hook.py b/src/codegen.backup/cli/commands/claude/config/claude_session_stop_hook.py new file mode 100644 index 000000000..48ea07ddf --- /dev/null +++ b/src/codegen.backup/cli/commands/claude/config/claude_session_stop_hook.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Claude Code stop hook script for API integration. + +This script is called by Claude Code on Stop to: +1. Read the session context (session_id, org_id) +2. Send a COMPLETE status to the backend API +""" + +import json +import os +import sys +from pathlib import Path + +# Add the codegen CLI to the path so we can import from it +script_dir = Path(__file__).parent +codegen_cli_dir = script_dir.parent.parent.parent.parent +sys.path.insert(0, str(codegen_cli_dir)) + +try: + from codegen.cli.commands.claude.claude_session_api import update_claude_session_status +except ImportError: + update_claude_session_status = None + + +def read_session_file() -> dict: + """Read session data written by the SessionStart hook, if available.""" + session_path = Path.home() / ".codegen" / "claude-session.json" + if not session_path.exists(): + return {} + try: + with open(session_path) as f: + return json.load(f) + except Exception: + return {} + + +def main(): + try: + # Prefer environment variables set by the CLI wrapper + session_id = os.environ.get("CODEGEN_CLAUDE_SESSION_ID") + org_id = os.environ.get("CODEGEN_CLAUDE_ORG_ID") + + # Fallback to reading the session file + if not session_id or not org_id: + data = read_session_file() + session_id = session_id or data.get("session_id") + org_id = org_id or data.get("org_id") + + # Normalize org_id type + if isinstance(org_id, str): + try: + org_id = int(org_id) + except ValueError: + org_id = None + + if update_claude_session_status and session_id: + update_claude_session_status(session_id, "COMPLETE", org_id) + + # Print minimal output to avoid noisy hooks + print(json.dumps({"session_id": session_id, "status": "COMPLETE"})) + + except Exception as e: + # Ensure hook doesn't fail Claude if something goes wrong + print(json.dumps({"error": str(e)})) + + +if __name__ == "__main__": + main() diff --git a/src/codegen.backup/cli/commands/claude/config/mcp_setup.py b/src/codegen.backup/cli/commands/claude/config/mcp_setup.py new file mode 100644 index 000000000..f3cd8be03 --- /dev/null +++ b/src/codegen.backup/cli/commands/claude/config/mcp_setup.py @@ -0,0 +1,67 @@ +import subprocess + +from codegen.cli.api.endpoints import MCP_SERVER_ENDPOINT +from codegen.cli.auth.token_manager import get_current_token +from codegen.cli.commands.claude.quiet_console import console +from codegen.cli.commands.claude.utils import resolve_claude_path + + +def add_codegen_mcp_server(): + console.print("🔧 Configuring MCP server 'codegen-tools'...", style="blue") + try: + token = get_current_token() + if not token: + console.print("⚠️ No authentication token found. Please run 'codegen login' first.", style="yellow") + return + + claude_path = resolve_claude_path() + if not claude_path: + console.print("⚠️ 'claude' CLI not found to add MCP server", style="yellow") + return + + add_result = subprocess.run( + [ + claude_path, + "mcp", + "add", + "--transport", + "http", + "codegen-tools", + MCP_SERVER_ENDPOINT, + "--header", + f"Authorization: Bearer {token}", + ], + capture_output=True, + text=True, + timeout=15, + ) + if add_result.returncode == 0: + console.print("✅ MCP server added: codegen-tools -> http", style="green") + else: + stderr = add_result.stderr.strip() if add_result.stderr else add_result.stdout.strip() + console.print(f"⚠️ Failed to add MCP server (code {add_result.returncode}): {stderr}", style="yellow") + except subprocess.TimeoutExpired: + console.print("⚠️ MCP server add timed out", style="yellow") + except FileNotFoundError: + console.print("⚠️ 'claude' CLI not found to add MCP server", style="yellow") + except Exception as e: + console.print(f"⚠️ Error adding MCP server: {e}", style="yellow") + + +def cleanup_codegen_mcp_server(): + try: + claude_path = resolve_claude_path() + if not claude_path: + # Silently skip if claude is not found during cleanup + return + + subprocess.run( + [ + claude_path, + "mcp", + "remove", + "codegen-tools", + ], + ) + except Exception as e: + console.print(f"⚠️ Error removing MCP server: {e}", style="yellow") diff --git a/src/codegen.backup/cli/commands/claude/hooks.py b/src/codegen.backup/cli/commands/claude/hooks.py new file mode 100644 index 000000000..6ecc45838 --- /dev/null +++ b/src/codegen.backup/cli/commands/claude/hooks.py @@ -0,0 +1,219 @@ +"""Claude hooks management for session tracking.""" + +import json +import os +from pathlib import Path + +from codegen.cli.commands.claude.quiet_console import console + +CLAUDE_CONFIG_DIR = Path.home() / ".claude" +HOOKS_CONFIG_FILE = CLAUDE_CONFIG_DIR / "settings.json" +CODEGEN_DIR = Path.home() / ".codegen" +SESSION_FILE = CODEGEN_DIR / "claude-session.json" +SESSION_LOG_FILE = CODEGEN_DIR / "claude-sessions.log" + + +def ensure_claude_hook() -> bool: + """Ensure the Claude hooks are properly set up for session tracking. + + This function will: + 1. Create necessary directories + 2. Create the hooks file if it doesn't exist + 3. Always overwrite any existing SessionStart and Stop hooks with our commands + + Returns: + bool: True if hooks were set up successfully, False otherwise + """ + try: + # Create .codegen directory if it doesn't exist + CODEGEN_DIR.mkdir(exist_ok=True) + + # Clean up old session file if it exists + if SESSION_FILE.exists(): + SESSION_FILE.unlink() + + # Ensure Claude config directory exists + CLAUDE_CONFIG_DIR.mkdir(exist_ok=True) + + # Build the shell command that will create session via API and write session data + + # Build the stop hook command to mark session COMPLETE + stop_hook_script_path = Path(__file__).parent / "config" / "claude_session_stop_hook.py" + stop_hook_command = f"python3 {stop_hook_script_path}" + + # Build the user prompt submit hook to set status ACTIVE + active_hook_script_path = Path(__file__).parent / "config" / "claude_session_active_hook.py" + active_hook_command = f"python3 {active_hook_script_path}" + + # Read existing hooks config or create new one + hooks_config = {} + if HOOKS_CONFIG_FILE.exists(): + try: + with open(HOOKS_CONFIG_FILE) as f: + content = f.read().strip() + if content: + hooks_config = json.loads(content) + else: + console.print("⚠️ Hooks file is empty, creating new configuration", style="yellow") + except (OSError, json.JSONDecodeError) as e: + console.print(f"⚠️ Could not read existing hooks file: {e}, creating new one", style="yellow") + + # Ensure proper structure exists + if "hooks" not in hooks_config: + hooks_config["hooks"] = {} + if "Stop" not in hooks_config["hooks"]: + hooks_config["hooks"]["Stop"] = [] + if "UserPromptSubmit" not in hooks_config["hooks"]: + hooks_config["hooks"]["UserPromptSubmit"] = [] + + # Get existing hooks + stop_hooks = hooks_config["hooks"]["Stop"] + active_hooks = hooks_config["hooks"]["UserPromptSubmit"] + + # Check if we're replacing existing hooks + replaced_existing = (len(stop_hooks) > 0) or (len(active_hooks) > 0) + + # Create the new hook structures (following Claude's format) + new_stop_hook_group = {"hooks": [{"type": "command", "command": stop_hook_command}]} + new_active_hook_group = {"hooks": [{"type": "command", "command": active_hook_command}]} + + # Replace all existing hooks with our single hook per event + hooks_config["hooks"]["Stop"] = [new_stop_hook_group] + hooks_config["hooks"]["UserPromptSubmit"] = [new_active_hook_group] + + # Write updated config with nice formatting + with open(HOOKS_CONFIG_FILE, "w") as f: + json.dump(hooks_config, f, indent=2) + f.write("\n") # Add trailing newline for cleaner file + + if replaced_existing: + console.print("✅ Replaced existing Claude hooks (SessionStart, Stop)", style="green") + else: + console.print("✅ Registered new Claude hooks (SessionStart, Stop)", style="green") + console.print(f" Stop hook: {stop_hook_command}", style="dim") + console.print(f" Active hook:{' ' if len('Active hook:') < 1 else ''} {active_hook_command}", style="dim") + + # Verify the hook was written correctly + try: + with open(HOOKS_CONFIG_FILE) as f: + verify_config = json.load(f) + + found_stop_hook = False + for hook_group in verify_config.get("hooks", {}).get("Stop", []): + for hook in hook_group.get("hooks", []): + if "claude_session_stop_hook.py" in hook.get("command", ""): + found_stop_hook = True + break + found_active_hook = False + for hook_group in verify_config.get("hooks", {}).get("UserPromptSubmit", []): + for hook in hook_group.get("hooks", []): + if "claude_session_active_hook.py" in hook.get("command", ""): + found_active_hook = True + break + + if found_stop_hook and found_active_hook: + console.print("✅ Hook configuration verified", style="dim") + else: + console.print("⚠️ Hook was written but verification failed", style="yellow") + return False + + except Exception as e: + console.print(f"⚠️ Could not verify hook configuration: {e}", style="yellow") + return False + + return True + + except Exception as e: + console.print(f"❌ Failed to set up Claude hook: {e}", style="red") + return False + + +def cleanup_claude_hook() -> None: + """Remove the Codegen Claude hooks from the hooks configuration.""" + try: + if not HOOKS_CONFIG_FILE.exists(): + return + + with open(HOOKS_CONFIG_FILE) as f: + hooks_config = json.load(f) + + if "hooks" not in hooks_config: + return + + session_start_hooks = hooks_config["hooks"].get("SessionStart", []) + stop_hooks = hooks_config["hooks"].get("Stop", []) + active_hooks = hooks_config["hooks"].get("UserPromptSubmit", []) + modified = False + + # Filter out any hook groups that contain our command + new_session_hooks = [] + for hook_group in session_start_hooks: + # Check if this group contains our hook + contains_our_hook = False + for hook in hook_group.get("hooks", []): + if hook.get("command") and "claude-session.json" in hook.get("command", ""): + contains_our_hook = True + modified = True + break + + # Keep hook groups that don't contain our hook + if not contains_our_hook: + new_session_hooks.append(hook_group) + + # Update SessionStart hooks if we removed something + if modified: + hooks_config["hooks"]["SessionStart"] = new_session_hooks + + # Now also remove Stop hook referencing our stop script + new_stop_hooks = [] + for hook_group in stop_hooks: + contains_stop = False + for hook in hook_group.get("hooks", []): + if hook.get("command") and "claude_session_stop_hook.py" in hook.get("command", ""): + contains_stop = True + break + if not contains_stop: + new_stop_hooks.append(hook_group) + else: + modified = True + + if stop_hooks is not None: + hooks_config["hooks"]["Stop"] = new_stop_hooks + + # Remove UserPromptSubmit hook referencing our active script + new_active_hooks = [] + for hook_group in active_hooks: + contains_active = False + for hook in hook_group.get("hooks", []): + if hook.get("command") and "claude_session_active_hook.py" in hook.get("command", ""): + contains_active = True + break + if not contains_active: + new_active_hooks.append(hook_group) + else: + modified = True + + if active_hooks is not None: + hooks_config["hooks"]["UserPromptSubmit"] = new_active_hooks + + # Write updated config if anything changed + if modified: + with open(HOOKS_CONFIG_FILE, "w") as f: + json.dump(hooks_config, f, indent=2) + f.write("\n") # Add trailing newline + console.print("✅ Removed Claude hooks", style="dim") + + # Clean up session files + if SESSION_FILE.exists(): + SESSION_FILE.unlink() + + except Exception as e: + console.print(f"⚠️ Error cleaning up hook: {e}", style="yellow") + + +def get_codegen_url(session_id: str) -> str: + """Get the Codegen URL for a session ID.""" + # You can customize this based on your environment + base_url = os.environ.get("CODEGEN_BASE_URL", "https://codegen.com") + # Use the format: codegen.com/claude-code/{session-id} + return f"{base_url}/claude-code/{session_id}" diff --git a/src/codegen.backup/cli/commands/claude/main.py b/src/codegen.backup/cli/commands/claude/main.py new file mode 100644 index 000000000..41e532d42 --- /dev/null +++ b/src/codegen.backup/cli/commands/claude/main.py @@ -0,0 +1,403 @@ +"""Claude Code command with session tracking.""" + +import json +import os +import signal +import subprocess +import sys +import time + +import requests +import typer +from rich import box +from rich.console import Console +from rich.panel import Panel + +from codegen.cli.api.endpoints import API_ENDPOINT +from codegen.cli.auth.token_manager import get_current_token +from codegen.cli.commands.claude.claude_log_watcher import ClaudeLogWatcherManager +from codegen.cli.commands.claude.claude_session_api import ( + create_claude_session, + generate_session_id, + update_claude_session_status, +) +from codegen.cli.commands.claude.config.mcp_setup import add_codegen_mcp_server, cleanup_codegen_mcp_server +from codegen.cli.commands.claude.hooks import SESSION_FILE, cleanup_claude_hook, ensure_claude_hook, get_codegen_url +from codegen.cli.commands.claude.quiet_console import console +from codegen.cli.commands.claude.utils import resolve_claude_path +from codegen.cli.rich.spinners import create_spinner +from codegen.cli.utils.org import resolve_org_id +from codegen.shared.logging.get_logger import get_logger + +# Initialize logger +logger = get_logger(__name__) + + +def _get_session_context() -> dict: + """Get session context for logging.""" + try: + from codegen.cli.telemetry.otel_setup import get_session_uuid + + return {"session_id": get_session_uuid()} + except ImportError: + return {} + + +t_console = Console() + + +def _run_claude_background(resolved_org_id: int, prompt: str | None) -> None: + """Create a background agent run with Claude context and exit.""" + logger.info( + "Claude background run started", + extra={"operation": "claude.background", "org_id": resolved_org_id, "prompt_length": len(prompt) if prompt else 0, "command": "codegen claude --background", **_get_session_context()}, + ) + + start_time = time.time() + token = get_current_token() + if not token: + logger.error( + "Claude background run failed - not authenticated", extra={"operation": "claude.background", "org_id": resolved_org_id, "error_type": "not_authenticated", **_get_session_context()} + ) + console.print("[red]Error:[/red] Not authenticated. Please run 'codegen login' first.") + raise typer.Exit(1) + + payload = {"prompt": prompt or "Start a Claude Code background session"} + + spinner = create_spinner("Creating agent run...") + spinner.start() + try: + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "x-codegen-client": "codegen__claude_code", + } + url = f"{API_ENDPOINT.rstrip('/')}/v1/organizations/{resolved_org_id}/agent/run" + response = requests.post(url, headers=headers, json=payload) + response.raise_for_status() + agent_run_data = response.json() + + duration_ms = (time.time() - start_time) * 1000 + run_id = agent_run_data.get("id", "Unknown") + status = agent_run_data.get("status", "Unknown") + + logger.info( + "Claude background run created successfully", + extra={"operation": "claude.background", "org_id": resolved_org_id, "agent_run_id": run_id, "status": status, "duration_ms": duration_ms, "success": True, **_get_session_context()}, + ) + + except Exception as e: + duration_ms = (time.time() - start_time) * 1000 + logger.error( + "Claude background run failed", + extra={ + "operation": "claude.background", + "org_id": resolved_org_id, + "error_type": type(e).__name__, + "error_message": str(e), + "duration_ms": duration_ms, + "success": False, + **_get_session_context(), + }, + exc_info=True, + ) + raise + finally: + spinner.stop() + + run_id = agent_run_data.get("id", "Unknown") + status = agent_run_data.get("status", "Unknown") + web_url = agent_run_data.get("web_url", "") + + result_lines = [ + f"[cyan]Agent Run ID:[/cyan] {run_id}", + f"[cyan]Status:[/cyan] {status}", + ] + if web_url: + result_lines.append(f"[cyan]Web URL:[/cyan] {web_url}") + + t_console.print( + Panel( + "\n".join(result_lines), + title="🤖 [bold]Background Agent Run Created[/bold]", + border_style="green", + box=box.ROUNDED, + padding=(1, 2), + ) + ) + t_console.print("\n[dim]💡 Track progress with:[/dim] [cyan]codegen agents[/cyan]") + if web_url: + t_console.print(f"[dim]🌐 View in browser:[/dim] [link]{web_url}[/link]") + + +def _run_claude_interactive(resolved_org_id: int, no_mcp: bool | None) -> None: + """Launch Claude Code with session tracking and log watching.""" + # Generate session ID for tracking + session_id = generate_session_id() + + logger.info( + "Claude interactive session started", + extra={"operation": "claude.interactive", "org_id": resolved_org_id, "claude_session_id": session_id, "mcp_disabled": bool(no_mcp), "command": "codegen claude", **_get_session_context()}, + ) + + console.print(f"🆔 Generated session ID: {session_id[:8]}...", style="dim") + + console.print("🚀 Starting Claude Code with session tracking...", style="blue") + console.print(f"🎯 Organization ID: {resolved_org_id}", style="dim") + + # Set up environment variables for hooks to access session information + os.environ["CODEGEN_CLAUDE_SESSION_ID"] = session_id + os.environ["CODEGEN_CLAUDE_ORG_ID"] = str(resolved_org_id) + + # Proactively create the backend session as a fallback in case hooks fail + try: + agent_run_id = create_claude_session(session_id, resolved_org_id) + if agent_run_id: + console.print("✅ Backend session created", style="green") + else: + console.print("⚠️ Could not create backend session at startup (will rely on hooks)", style="yellow") + except Exception as e: + agent_run_id = None + console.print(f"⚠️ Session creation error at startup: {e}", style="yellow") + + # Set up Claude hook for session tracking + if not ensure_claude_hook(): + console.print("⚠️ Failed to set up session tracking hook", style="yellow") + + # Write session context file for downstream hooks and tools (after hook setup) + try: + SESSION_FILE.parent.mkdir(exist_ok=True) + session_payload = { + "session_id": session_id, + "agent_run_id": agent_run_id, + "org_id": resolved_org_id, + "hook_event": "Startup", + } + with open(SESSION_FILE, "w") as f: + json.dump(session_payload, f, indent=2) + f.write("\n") + console.print("📝 Wrote session file to ~/.codegen/claude-session.json", style="dim") + except Exception as e: + console.print(f"⚠️ Could not write session file: {e}", style="yellow") + + # Initialize log watcher manager + log_watcher_manager = ClaudeLogWatcherManager() + + # Resolve Claude CLI path and test accessibility + claude_path = resolve_claude_path() + if not claude_path: + logger.error( + "Claude CLI not found", + extra={"operation": "claude.interactive", "org_id": resolved_org_id, "claude_session_id": session_id, "error_type": "claude_cli_not_found", **_get_session_context()}, + ) + console.print("❌ Claude Code CLI not found.", style="red") + console.print( + "💡 If you migrated a local install, ensure `~/.claude/local/claude` exists, or add it to PATH.", + style="dim", + ) + console.print( + "💡 Otherwise install globally via npm (e.g., `npm i -g claude`) or run `claude /migrate`.", + style="dim", + ) + update_claude_session_status(session_id, "ERROR", resolved_org_id) + raise typer.Exit(1) + + console.print(f"🔍 Using Claude CLI at: {claude_path}", style="blue") + try: + test_result = subprocess.run([claude_path, "--version"], capture_output=True, text=True, timeout=10) + if test_result.returncode == 0: + console.print(f"✅ Claude Code found: {test_result.stdout.strip()}", style="green") + else: + console.print(f"⚠️ Claude Code test failed with code {test_result.returncode}", style="yellow") + if test_result.stderr: + console.print(f"Error: {test_result.stderr.strip()}", style="red") + except subprocess.TimeoutExpired: + console.print("⚠️ Claude Code version check timed out", style="yellow") + except Exception as e: + console.print(f"⚠️ Claude Code test error: {e}", style="yellow") + + # If MCP endpoint provided, register MCP server via Claude CLI before launch + if not no_mcp: + add_codegen_mcp_server() + + console.print("🔵 Starting Claude Code session...", style="blue") + + try: + # Launch Claude Code with our session ID + console.print(f"🚀 Launching Claude Code with session ID: {session_id[:8]}...", style="blue") + + url = get_codegen_url(session_id) + console.print(f"\n🔵 Codegen URL: {url}\n", style="bold blue") + + process = subprocess.Popen([claude_path, "--session-id", session_id]) + + # Start log watcher for the session + console.print("📋 Starting log watcher...", style="blue") + log_watcher_started = log_watcher_manager.start_watcher( + session_id=session_id, + org_id=resolved_org_id, + poll_interval=1.0, + on_log_entry=None, + ) + + if not log_watcher_started: + console.print("⚠️ Failed to start log watcher", style="yellow") + + # Handle Ctrl+C gracefully + def signal_handler(signum, frame): + console.print("\n🛑 Stopping Claude Code...", style="yellow") + log_watcher_manager.stop_all_watchers() + process.terminate() + cleanup_claude_hook() + cleanup_codegen_mcp_server() + update_claude_session_status(session_id, "COMPLETE", resolved_org_id) + sys.exit(0) + + signal.signal(signal.SIGINT, signal_handler) + + # Wait for Claude Code to finish + returncode = process.wait() + + # Handle session completion based on exit code + session_status = "COMPLETE" if returncode == 0 else "ERROR" + update_claude_session_status(session_id, session_status, resolved_org_id) + + if returncode != 0: + logger.error( + "Claude interactive session failed", + extra={ + "operation": "claude.interactive", + "org_id": resolved_org_id, + "claude_session_id": session_id, + "exit_code": returncode, + "session_status": session_status, + **_get_session_context(), + }, + ) + console.print(f"❌ Claude Code exited with error code {returncode}", style="red") + else: + logger.info( + "Claude interactive session completed successfully", + extra={ + "operation": "claude.interactive", + "org_id": resolved_org_id, + "claude_session_id": session_id, + "exit_code": returncode, + "session_status": session_status, + **_get_session_context(), + }, + ) + console.print("✅ Claude Code finished successfully", style="green") + + except FileNotFoundError: + logger.error( + "Claude Code executable not found", + extra={"operation": "claude.interactive", "org_id": resolved_org_id, "claude_session_id": session_id, "error_type": "claude_executable_not_found", **_get_session_context()}, + ) + console.print("❌ Claude Code not found. Please install Claude Code first.", style="red") + console.print("💡 Visit: https://claude.ai/download", style="dim") + log_watcher_manager.stop_all_watchers() + update_claude_session_status(session_id, "ERROR", resolved_org_id) + raise typer.Exit(1) + except KeyboardInterrupt: + logger.info( + "Claude interactive session interrupted by user", + extra={ + "operation": "claude.interactive", + "org_id": resolved_org_id, + "claude_session_id": session_id, + "session_status": "CANCELLED", + "exit_reason": "user_interrupt", + **_get_session_context(), + }, + ) + console.print("\n🛑 Interrupted by user", style="yellow") + log_watcher_manager.stop_all_watchers() + update_claude_session_status(session_id, "CANCELLED", resolved_org_id) + except Exception as e: + logger.error( + "Claude interactive session error", + extra={ + "operation": "claude.interactive", + "org_id": resolved_org_id, + "claude_session_id": session_id, + "error_type": type(e).__name__, + "error_message": str(e), + "session_status": "ERROR", + **_get_session_context(), + }, + exc_info=True, + ) + console.print(f"❌ Error running Claude Code: {e}", style="red") + log_watcher_manager.stop_all_watchers() + update_claude_session_status(session_id, "ERROR", resolved_org_id) + raise typer.Exit(1) + finally: + # Clean up resources + try: + log_watcher_manager.stop_all_watchers() + except Exception as e: + console.print(f"⚠️ Error stopping log watchers: {e}", style="yellow") + + cleanup_claude_hook() + + # Show final session info + url = get_codegen_url(session_id) + console.print(f"\n🔵 Session URL: {url}", style="bold blue") + console.print(f"🆔 Session ID: {session_id}", style="dim") + console.print(f"🎯 Organization ID: {resolved_org_id}", style="dim") + console.print("💡 Check your backend to see the session data", style="dim") + + +def claude( + org_id: int | None = typer.Option(None, help="Organization ID (defaults to CODEGEN_ORG_ID/REPOSITORY_ORG_ID or auto-detect)"), + no_mcp: bool | None = typer.Option(False, "--no-mcp", help="Disable Codegen's MCP server with additional capabilities over HTTP"), + background: str | None = typer.Option(None, "--background", "-b", help="Create a background agent run with this prompt instead of launching Claude Code"), +): + """Run Claude Code with session tracking or create a background run.""" + logger.info( + "Claude command invoked", + extra={ + "operation": "claude.command", + "org_id": org_id, + "no_mcp": bool(no_mcp), + "is_background": background is not None, + "background_prompt_length": len(background) if background else 0, + "command": f"codegen claude{' --background' if background else ''}", + **_get_session_context(), + }, + ) + + # Resolve org_id early for session management + resolved_org_id = resolve_org_id(org_id) + if resolved_org_id is None: + logger.error("Claude command failed - no org ID", extra={"operation": "claude.command", "error_type": "org_id_missing", **_get_session_context()}) + console.print("[red]Error:[/red] Organization ID not provided. Pass --org-id, set CODEGEN_ORG_ID, or REPOSITORY_ORG_ID.") + raise typer.Exit(1) + + try: + if background is not None: + # Use the value from --background as the prompt + final_prompt = background + _run_claude_background(resolved_org_id, final_prompt) + return + + _run_claude_interactive(resolved_org_id, no_mcp) + + except typer.Exit: + # Let typer exits pass through without additional logging + raise + except Exception as e: + logger.error( + "Claude command failed unexpectedly", + extra={ + "operation": "claude.command", + "org_id": resolved_org_id, + "error_type": type(e).__name__, + "error_message": str(e), + "is_background": background is not None, + **_get_session_context(), + }, + exc_info=True, + ) + raise diff --git a/src/codegen.backup/cli/commands/claude/quiet_console.py b/src/codegen.backup/cli/commands/claude/quiet_console.py new file mode 100644 index 000000000..b9a040c3d --- /dev/null +++ b/src/codegen.backup/cli/commands/claude/quiet_console.py @@ -0,0 +1,33 @@ +"""Silent console utilities for Claude CLI. + +This module provides a shared Rich console instance that is silent by default +to avoid interfering with Claude's terminal UI. +""" + +from __future__ import annotations + +import io +import os +from rich.console import Console + + +def _create_console() -> Console: + """Create a console instance. + + If CODEGEN_CLAUDE_VERBOSE is set to a truthy value, return a normal + Console for debugging; otherwise, return a Console that writes to an + in-memory buffer so nothing is emitted to stdout/stderr. + """ + verbose = os.environ.get("CODEGEN_CLAUDE_VERBOSE", "").strip().lower() + is_verbose = verbose in ("1", "true", "yes", "on") + + if is_verbose: + return Console() + + # Silent console: sink all output + return Console(file=io.StringIO()) + + +# Shared console used across Claude CLI modules +console = _create_console() + diff --git a/src/codegen.backup/cli/commands/claude/utils.py b/src/codegen.backup/cli/commands/claude/utils.py new file mode 100644 index 000000000..b8fb06049 --- /dev/null +++ b/src/codegen.backup/cli/commands/claude/utils.py @@ -0,0 +1,39 @@ +"""Utility functions for Claude CLI integration.""" + +import os +from shutil import which + + +def resolve_claude_path() -> str | None: + """Resolve the path to the Claude Code CLI. + + Tries PATH first, then common local install locations created by `claude /migrate`. + + Returns: + Path to the claude executable if found, None otherwise. + """ + # 1) Check system PATH first + path_from_path = which("claude") + if path_from_path: + return path_from_path + + # 2) Check common local install locations + home = os.path.expanduser("~") + candidates = [ + # Local install created by `claude /migrate` + os.path.join(home, ".claude", "local", "claude"), + os.path.join(home, ".claude", "local", "node_modules", ".bin", "claude"), + # Common global install locations + "/usr/local/bin/claude", + "/opt/homebrew/bin/claude", # Homebrew on Apple Silicon + ] + + for candidate in candidates: + try: + if os.path.isfile(candidate) and os.access(candidate, os.X_OK): + return candidate + except Exception: + # Best-effort checks only; ignore filesystem errors + pass + + return None diff --git a/src/codegen.backup/cli/commands/config/main.py b/src/codegen.backup/cli/commands/config/main.py new file mode 100644 index 000000000..efff72d78 --- /dev/null +++ b/src/codegen.backup/cli/commands/config/main.py @@ -0,0 +1,131 @@ +import logging + +import rich +import typer +from rich.table import Table + +from codegen.cli.commands.config.telemetry import telemetry_app +from codegen.configs.constants import ENV_FILENAME, GLOBAL_ENV_FILE +from codegen.configs.user_config import UserConfig +from codegen.shared.logging.get_logger import get_logger +from codegen.shared.path import get_git_root_path + +# Initialize logger for config commands +logger = get_logger(__name__) + +# Create a Typer app for the config command +config_command = typer.Typer(help="Manage codegen configuration.") + +# Add telemetry subcommands +config_command.add_typer(telemetry_app, name="telemetry") + + +@config_command.command(name="list") +def list_config(): + """List current configuration values.""" + logger.info("Config list command invoked", extra={"operation": "config.list", "command": "codegen config list"}) + + 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") +def get_config(key: str = typer.Argument(..., help="Configuration key to get")): + """Get a configuration value.""" + logger.info("Config get command invoked", extra={"operation": "config.get", "key": key, "command": f"codegen config get {key}"}) + + config = _get_user_config() + if not config.has_key(key): + logger.warning("Config key not found", extra={"operation": "config.get", "key": key, "error_type": "key_not_found"}) + rich.print(f"[red]Error: Configuration key '{key}' not found[/red]") + return + + value = config.get(key) + # Don't log debug info for successful value retrieval - focus on user actions + + rich.print(f"[cyan]{key}[/cyan]=[magenta]{value}[/magenta]") + + +@config_command.command(name="set") +def set_config(key: str = typer.Argument(..., help="Configuration key to set"), value: str = typer.Argument(..., help="Configuration value to set")): + """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/src/codegen.backup/cli/commands/config/telemetry.py b/src/codegen.backup/cli/commands/config/telemetry.py new file mode 100644 index 000000000..9aed6d204 --- /dev/null +++ b/src/codegen.backup/cli/commands/config/telemetry.py @@ -0,0 +1,156 @@ +"""Telemetry configuration commands.""" + +import json +from pathlib import Path + +import typer +from rich.console import Console +from rich.panel import Panel +from rich.syntax import Syntax +from rich.table import Table + +from codegen.cli.telemetry import update_telemetry_consent +from codegen.configs.constants import GLOBAL_CONFIG_DIR, GLOBAL_ENV_FILE +from codegen.configs.models.telemetry import TelemetryConfig + +console = Console() + +# Create the telemetry sub-app +telemetry_app = typer.Typer(help="Manage telemetry settings") + + +@telemetry_app.command() +def enable(): + """Enable telemetry data collection.""" + update_telemetry_consent(enabled=True) + + +@telemetry_app.command() +def disable(): + """Disable telemetry data collection.""" + update_telemetry_consent(enabled=False) + + +@telemetry_app.command() +def status(): + """Show current telemetry settings.""" + telemetry = TelemetryConfig(env_filepath=GLOBAL_ENV_FILE) + + table = Table(title="Telemetry Settings", show_header=False) + table.add_column("Setting", style="cyan") + table.add_column("Value", style="white") + + table.add_row("Enabled", "✅ Yes" if telemetry.enabled else "❌ No") + table.add_row("Debug Mode", "Yes" if telemetry.debug else "No") + + console.print(table) + console.print("\n[dim]Telemetry helps us improve the CLI experience.[/dim]") + console.print("[dim]No personal information or source code is collected.[/dim]") + + +@telemetry_app.command() +def debug( + enable: bool = typer.Option(None, "--enable/--disable", help="Enable or disable debug mode"), + show_logs: bool = typer.Option(False, "--logs", help="Show recent debug logs"), + clear: bool = typer.Option(False, "--clear", help="Clear debug logs"), +): + """Manage telemetry debug mode and logs.""" + telemetry = TelemetryConfig(env_filepath=GLOBAL_ENV_FILE) + debug_dir = GLOBAL_CONFIG_DIR / "telemetry_debug" + + # Handle enable/disable + if enable is not None: + telemetry.debug = enable + telemetry.write_to_file(GLOBAL_ENV_FILE) + + # Refresh logging configuration to immediately apply the debug mode change + try: + from codegen.shared.logging.get_logger import refresh_telemetry_config + + refresh_telemetry_config() + except ImportError: + pass # Logging refresh not available + + console.print(f"[green]✓ Debug mode {'enabled' if enable else 'disabled'}[/green]") + if enable: + console.print(f"[dim]Debug logs will be written to: {debug_dir}[/dim]") + console.print("[dim]Console logging will now be enabled for all CLI operations[/dim]") + else: + console.print("[dim]Console logging will now be disabled for CLI operations[/dim]") + + # Handle clear + if clear: + if debug_dir.exists(): + import shutil + + shutil.rmtree(debug_dir) + console.print("[green]✓ Debug logs cleared[/green]") + else: + console.print("[yellow]No debug logs to clear[/yellow]") + return + + # Handle show logs + if show_logs: + if not debug_dir.exists(): + console.print("[yellow]No debug logs found[/yellow]") + return + + # Find most recent session file + session_files = sorted(debug_dir.glob("session_*.jsonl"), reverse=True) + if not session_files: + console.print("[yellow]No debug sessions found[/yellow]") + return + + latest_file = session_files[0] + console.print(f"\n[cyan]Latest session:[/cyan] {latest_file.name}") + + # Read and display spans + with open(latest_file) as f: + spans = [] + for line in f: + data = json.loads(line) + if data["type"] == "span": + spans.append(data) + + if not spans: + console.print("[yellow]No spans recorded in this session[/yellow]") + return + + # Create table + table = Table(title=f"Telemetry Spans ({len(spans)} total)") + table.add_column("Operation", style="cyan") + table.add_column("Duration (ms)", style="green") + table.add_column("Status", style="yellow") + table.add_column("Key Attributes", style="white") + + for span in spans[-10:]: # Show last 10 spans + duration = f"{span.get('duration_ms', 0):.2f}" if span.get("duration_ms") else "N/A" + status = span["status"]["status_code"] + + # Extract key attributes + attrs = span.get("attributes", {}) + key_attrs = [] + for key in ["cli.command.name", "cli.operation.name", "event.name"]: + if key in attrs: + key_attrs.append(f"{key.split('.')[-1]}: {attrs[key]}") + + table.add_row(span["name"], duration, status, "\n".join(key_attrs[:2]) if key_attrs else "") + + console.print(table) + console.print(f"\n[dim]Full logs available at: {latest_file}[/dim]") + + # If no action specified, show current status + if enable is None and not show_logs and not clear: + console.print(f"Debug mode: {'[green]Enabled[/green]' if telemetry.debug else '[red]Disabled[/red]'}") + if debug_dir.exists(): + log_count = len(list(debug_dir.glob("session_*.jsonl"))) + console.print(f"Debug sessions: {log_count}") + console.print(f"Debug directory: {debug_dir}") + + +@telemetry_app.callback(invoke_without_command=True) +def telemetry_callback(ctx: typer.Context): + """Manage telemetry settings.""" + if ctx.invoked_subcommand is None: + # If no subcommand is provided, show status + status() diff --git a/src/codegen.backup/cli/commands/init/main.py b/src/codegen.backup/cli/commands/init/main.py new file mode 100644 index 000000000..3d6b4d421 --- /dev/null +++ b/src/codegen.backup/cli/commands/init/main.py @@ -0,0 +1,87 @@ +from pathlib import Path + +import rich +import typer + +from codegen.cli.auth.session import CodegenSession +from codegen.cli.rich.codeblocks import format_command +from codegen.shared.logging.get_logger import get_logger +from codegen.shared.path import get_git_root_path + +# Initialize logger +logger = get_logger(__name__) + + +def init( + path: str | None = typer.Option(None, help="Path within a git repository. Defaults to the current directory."), + token: str | None = typer.Option(None, help="Access token for the git repository. Required for full functionality."), + language: str | None = typer.Option(None, help="Override automatic language detection (python or typescript)"), + fetch_docs: bool = typer.Option(False, "--fetch-docs", help="Fetch docs and examples (requires auth)"), +): + """Initialize or update the Codegen folder.""" + logger.info("Init command started", extra={"operation": "init", "path": path, "language": language, "fetch_docs": fetch_docs, "has_token": bool(token)}) + + # Validate language option + if language and language.lower() not in ["python", "typescript"]: + logger.error("Invalid language specified", extra={"operation": "init", "language": language, "error_type": "invalid_language"}) + rich.print(f"[bold red]Error:[/bold red] Invalid language '{language}'. Must be 'python' or 'typescript'.") + raise typer.Exit(1) + + # Print a message if not in a git repo + path_obj = Path.cwd() if path is None else Path(path) + repo_path = get_git_root_path(path_obj) + rich.print(f"Found git repository at: {repo_path}") + + if repo_path is None: + logger.error("Not in a git repository", extra={"operation": "init", "path": str(path_obj), "error_type": "not_git_repo"}) + rich.print(f"\n[bold red]Error:[/bold red] Path={path_obj} 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("codegen init")) + raise typer.Exit(1) + + # At this point, repo_path is guaranteed to be not None + assert repo_path is not None + + # Session creation details not needed in logs + + session = CodegenSession(repo_path=repo_path, git_token=token) + if language: + session.config.repository.language = language.upper() + session.config.save() + # Language override details included in completion log + + action = "Updating" if session.existing else "Initializing" + + logger.info( + "Codegen session created", + extra={"operation": "init", "repo_path": str(repo_path), "action": action.lower(), "existing": session.existing, "language": getattr(session.config.repository, "language", None)}, + ) + + # Create the codegen directory + codegen_dir = session.codegen_dir + codegen_dir.mkdir(parents=True, exist_ok=True) + + logger.info( + "Init completed successfully", + extra={ + "operation": "init", + "repo_path": str(repo_path), + "codegen_dir": str(codegen_dir), + "action": action.lower(), + "language": getattr(session.config.repository, "language", None), + "fetch_docs": fetch_docs, + }, + ) + + # Print success message + rich.print(f"✅ {action} complete\n") + rich.print(f"Codegen workspace initialized at: [bold]{codegen_dir}[/bold]") + + # Print next steps + rich.print("\n[bold]What's next?[/bold]\n") + rich.print("1. Create a function:") + rich.print(format_command('codegen create my-function . -d "describe what you want to do"')) + rich.print("2. Run it:") + rich.print(format_command("codegen run my-function --apply-local")) diff --git a/src/codegen.backup/cli/commands/init/render.py b/src/codegen.backup/cli/commands/init/render.py new file mode 100644 index 000000000..7c7ee42ed --- /dev/null +++ b/src/codegen.backup/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/src/codegen.backup/cli/commands/integrations/__init__.py b/src/codegen.backup/cli/commands/integrations/__init__.py new file mode 100644 index 000000000..82f34a41d --- /dev/null +++ b/src/codegen.backup/cli/commands/integrations/__init__.py @@ -0,0 +1 @@ +"""Integrations command module.""" diff --git a/src/codegen.backup/cli/commands/integrations/main.py b/src/codegen.backup/cli/commands/integrations/main.py new file mode 100644 index 000000000..8b27ca341 --- /dev/null +++ b/src/codegen.backup/cli/commands/integrations/main.py @@ -0,0 +1,146 @@ +"""Integrations command for the Codegen CLI.""" + +import webbrowser + +import requests +import typer +from rich.console import Console +from rich.table import Table + +from codegen.cli.api.endpoints import API_ENDPOINT +from codegen.cli.auth.token_manager import get_current_token +from codegen.cli.rich.spinners import create_spinner +from codegen.cli.utils.org import resolve_org_id +from codegen.cli.utils.url import generate_webapp_url + +console = Console() + +# Create the integrations app +integrations_app = typer.Typer(help="Manage Codegen integrations") + + +@integrations_app.command("list") +def list_integrations(org_id: int | None = typer.Option(None, help="Organization ID (defaults to CODEGEN_ORG_ID/REPOSITORY_ORG_ID or auto-detect)")): + """List organization integrations from the Codegen API.""" + # Get the current token + token = get_current_token() + if not token: + console.print("[red]Error:[/red] Not authenticated. Please run 'codegen login' first.") + raise typer.Exit(1) + + try: + # Resolve org id + resolved_org_id = resolve_org_id(org_id) + if resolved_org_id is None: + console.print("[red]Error:[/red] Organization ID not provided. Pass --org-id, set CODEGEN_ORG_ID, or REPOSITORY_ORG_ID.") + raise typer.Exit(1) + + # Make API request to list integrations with spinner + spinner = create_spinner("Fetching organization integrations...") + spinner.start() + + try: + headers = {"Authorization": f"Bearer {token}"} + url = f"{API_ENDPOINT.rstrip('/')}/v1/organizations/{resolved_org_id}/integrations" + response = requests.get(url, headers=headers) + response.raise_for_status() + response_data = response.json() + finally: + spinner.stop() + + # Extract integrations from the response structure + integrations_data = response_data.get("integrations", []) + organization_name = response_data.get("organization_name", "Unknown") + total_active = response_data.get("total_active_integrations", 0) + + if not integrations_data: + console.print("[yellow]No integrations found.[/yellow]") + return + + # Create a table to display integrations + table = Table( + title=f"Integrations for {organization_name}", + border_style="blue", + show_header=True, + title_justify="center", + ) + table.add_column("Integration", style="cyan", no_wrap=True) + table.add_column("Status", style="white", justify="center") + table.add_column("Type", style="magenta") + table.add_column("Details", style="dim") + + # Add integrations to table + for integration in integrations_data: + integration_type = integration.get("integration_type", "Unknown") + active = integration.get("active", False) + token_id = integration.get("token_id") + installation_id = integration.get("installation_id") + metadata = integration.get("metadata", {}) + + # Status with emoji + status = "✅ Active" if active else "❌ Inactive" + + # Determine integration category + if integration_type.endswith("_user"): + category = "User Token" + elif integration_type.endswith("_app"): + category = "App Install" + elif integration_type in ["github", "slack_app", "linear_app"]: + category = "App Install" + else: + category = "Token-based" + + # Build details string + details = [] + if token_id: + details.append(f"Token ID: {token_id}") + if installation_id: + details.append(f"Install ID: {installation_id}") + if metadata and isinstance(metadata, dict): + for key, value in metadata.items(): + if key == "webhook_secret": + details.append(f"{key}: ***secret***") + else: + details.append(f"{key}: {value}") + + details_str = ", ".join(details) if details else "No details" + if len(details_str) > 50: + details_str = details_str[:47] + "..." + + table.add_row(integration_type.replace("_", " ").title(), status, category, details_str) + + console.print(table) + console.print(f"\n[green]Total: {len(integrations_data)} integrations ({total_active} active)[/green]") + + except requests.RequestException as e: + console.print(f"[red]Error fetching integrations:[/red] {e}", style="bold red") + raise typer.Exit(1) + except Exception as e: + console.print(f"[red]Unexpected error:[/red] {e}", style="bold red") + raise typer.Exit(1) + + +@integrations_app.command("add") +def add_integration(): + """Open the Codegen integrations page in your browser to add new integrations.""" + console.print("🌐 Opening Codegen integrations page...", style="bold blue") + + # Generate the web URL using the environment-aware utility + web_url = generate_webapp_url("integrations") + + try: + webbrowser.open(web_url) + console.print(f"✅ Opened [link]{web_url}[/link] in your browser", style="green") + console.print("💡 You can add new integrations from the web interface", style="dim") + except Exception as e: + console.print(f"❌ Failed to open browser: {e}", style="red") + console.print(f"🔗 Please manually visit: {web_url}", style="yellow") + + +# Default callback for the integrations app +@integrations_app.callback(invoke_without_command=True) +def integrations_callback(ctx: typer.Context): + """Manage Codegen integrations.""" + if ctx.invoked_subcommand is None: + # If no subcommand is provided, run list by default + list_integrations(org_id=None) diff --git a/src/codegen.backup/cli/commands/login/main.py b/src/codegen.backup/cli/commands/login/main.py new file mode 100644 index 000000000..6953a29fc --- /dev/null +++ b/src/codegen.backup/cli/commands/login/main.py @@ -0,0 +1,37 @@ +import typer + +from codegen.cli.auth.login import login_routine +from codegen.cli.auth.token_manager import get_current_token +from codegen.shared.logging.get_logger import get_logger + +# Initialize logger +logger = get_logger(__name__) + + +def _get_session_context() -> dict: + """Get session context for logging.""" + try: + from codegen.cli.telemetry.otel_setup import get_session_uuid + + return {"session_id": get_session_uuid()} + except ImportError: + return {} + + +def login(token: str | None = typer.Option(None, help="API token for authentication")): + """Store authentication token.""" + extra = {"operation": "auth.login", "has_provided_token": bool(token), "command": "codegen login", **_get_session_context()} + logger.info("Login command invoked", extra=extra) + + # Check if already authenticated + current_token = get_current_token() + if current_token: + logger.debug("User already authenticated", extra={"operation": "auth.login", "already_authenticated": True, **_get_session_context()}) + pass # Just proceed silently with re-authentication + + try: + login_routine(token) + logger.info("Login completed successfully", extra={"operation": "auth.login", "success": True, **_get_session_context()}) + except Exception as e: + logger.error("Login failed", extra={"operation": "auth.login", "error_type": type(e).__name__, "error_message": str(e), "success": False, **_get_session_context()}, exc_info=True) + raise diff --git a/src/codegen.backup/cli/commands/logout/main.py b/src/codegen.backup/cli/commands/logout/main.py new file mode 100644 index 000000000..1a254f684 --- /dev/null +++ b/src/codegen.backup/cli/commands/logout/main.py @@ -0,0 +1,21 @@ +import rich + +from codegen.cli.auth.token_manager import TokenManager +from codegen.shared.logging.get_logger import get_logger + +# Initialize logger +logger = get_logger(__name__) + + +def logout(): + """Clear stored authentication token.""" + logger.info("Logout command invoked", extra={"operation": "auth.logout", "command": "codegen logout"}) + + try: + token_manager = TokenManager() + token_manager.clear_token() + logger.info("Logout completed successfully", extra={"operation": "auth.logout", "success": True}) + rich.print("Successfully logged out") + except Exception as e: + logger.error("Logout failed", extra={"operation": "auth.logout", "error_type": type(e).__name__, "error_message": str(e), "success": False}, exc_info=True) + raise diff --git a/src/codegen.backup/cli/commands/org/__init__.py b/src/codegen.backup/cli/commands/org/__init__.py new file mode 100644 index 000000000..ba2d89354 --- /dev/null +++ b/src/codegen.backup/cli/commands/org/__init__.py @@ -0,0 +1,5 @@ +"""Organization management command.""" + +from .main import org + +__all__ = ["org"] \ No newline at end of file diff --git a/src/codegen.backup/cli/commands/org/main.py b/src/codegen.backup/cli/commands/org/main.py new file mode 100644 index 000000000..5b5f2a4d2 --- /dev/null +++ b/src/codegen.backup/cli/commands/org/main.py @@ -0,0 +1,129 @@ +"""Organization management command for switching between organizations.""" + +import os + +import typer +from rich.console import Console + +from codegen.cli.auth.token_manager import get_cached_organizations, get_current_token +from codegen.cli.commands.org.tui import OrgSelectorApp + +console = Console() + + +def org( + set_default: int | None = typer.Option(None, "--set-default", "-s", help="Set default organization ID"), + list_orgs: bool = typer.Option(False, "--list", "-l", help="List available organizations"), +): + """Manage and switch between organizations.""" + # Check if user is authenticated + token = get_current_token() + if not token: + console.print("[red]Error:[/red] Not authenticated. Please run 'codegen login' first.") + raise typer.Exit(1) + + # Get cached organizations + cached_orgs = get_cached_organizations() + if not cached_orgs: + console.print("[red]Error:[/red] No organizations found in cache. Please run 'codegen login' to refresh.") + raise typer.Exit(1) + + # Handle list mode + if list_orgs: + _list_organizations(cached_orgs) + return + + # Handle set default mode + if set_default is not None: + _set_default_organization(set_default, cached_orgs) + return + + # No flags provided, launch TUI + _run_org_selector_tui() + + +def _list_organizations(cached_orgs: list[dict]) -> None: + """List all available organizations.""" + from rich.table import Table + + table = Table(title="Available Organizations") + table.add_column("ID", style="cyan", no_wrap=True) + table.add_column("Name", style="green") + + for org in cached_orgs: + table.add_row(str(org["id"]), org["name"]) + + console.print(table) + + +def _set_default_organization(org_id: int, cached_orgs: list[dict]) -> None: + """Set the default organization via environment variable.""" + # Check if org ID exists in cache + org_found = None + for org in cached_orgs: + if org["id"] == org_id: + org_found = org + break + + if not org_found: + available_orgs = ", ".join([f"{org['name']} ({org['id']})" for org in cached_orgs]) + console.print(f"[red]Error:[/red] Organization ID {org_id} not found in your accessible organizations.") + console.print(f"[yellow]Available organizations:[/yellow] {available_orgs}") + raise typer.Exit(1) + + # Set the environment variable + os.environ["CODEGEN_ORG_ID"] = str(org_id) + + # Try to update .env file if it exists + env_file_path = ".env" + if os.path.exists(env_file_path): + _update_env_file(env_file_path, "CODEGEN_ORG_ID", str(org_id)) + console.print(f"[green]✓ Updated {env_file_path} with CODEGEN_ORG_ID={org_id}[/green]") + else: + console.print(f"[yellow]Info:[/yellow] No .env file found. Set environment variable manually:") + console.print(f"[cyan]export CODEGEN_ORG_ID={org_id}[/cyan]") + + console.print(f"[green]✓ Default organization set to:[/green] {org_found['name']} ({org_id})") + + +def _update_env_file(file_path: str, key: str, value: str) -> None: + """Update or add an environment variable in the .env file.""" + lines = [] + key_found = False + + # Read existing lines + try: + with open(file_path) as f: + lines = f.readlines() + except FileNotFoundError: + pass + + # Ensure all lines end with newline + for i, line in enumerate(lines): + if not line.endswith('\n'): + lines[i] = line + '\n' + + # Update existing key or note if we need to add it + for i, line in enumerate(lines): + if line.strip().startswith(f"{key}="): + lines[i] = f"{key}={value}\n" + key_found = True + break + + # Add new key if not found + if not key_found: + lines.append(f"{key}={value}\n") + + # Write back to file + with open(file_path, "w") as f: + f.writelines(lines) + + +def _run_org_selector_tui() -> None: + """Launch the organization selector TUI.""" + try: + app = OrgSelectorApp() + app.run() + except Exception as e: + console.print(f"[red]Error launching TUI:[/red] {e}") + raise typer.Exit(1) \ No newline at end of file diff --git a/src/codegen.backup/cli/commands/org/tui.py b/src/codegen.backup/cli/commands/org/tui.py new file mode 100644 index 000000000..f1640103d --- /dev/null +++ b/src/codegen.backup/cli/commands/org/tui.py @@ -0,0 +1,325 @@ +"""Organization selector TUI using Textual - Fixed version.""" + +import os + +from textual.app import App, ComposeResult +from textual.binding import Binding +from textual.containers import Container, Vertical +from textual.screen import Screen +from textual.widgets import DataTable, Footer, Header, Static + +from codegen.cli.auth.token_manager import get_cached_organizations, get_current_org_id +from codegen.cli.utils.org import resolve_org_id + + +class OrgSelectorTUI(Screen): + """TUI for selecting and switching organizations.""" + + BINDINGS = [ + Binding("escape,ctrl+c", "quit", "Quit", priority=True), + Binding("enter", "select_org", "Select", show=True), + Binding("q", "quit", "Quit", show=True), + ] + + def __init__(self): + super().__init__() + self.organizations = get_cached_organizations() or [] + self.current_org_id = get_current_org_id() + + def compose(self) -> ComposeResult: + """Create child widgets for the app.""" + yield Header() + + if not self.organizations: + yield Container( + Static("⚠️ No organizations found. Please run 'codegen login' first.", classes="warning-message"), + id="no-orgs-warning" + ) + else: + with Vertical(): + yield Static("🏢 Select Your Organization", classes="title") + yield Static("Use ↑↓ to navigate, Enter to select, Q/Esc to quit", classes="help") + + table = DataTable(id="orgs-table", cursor_type="row") + table.add_columns("Current", "ID", "Organization Name") + + # Get the actual current org ID (checks environment variables first) + actual_current_org_id = resolve_org_id() + + for org in self.organizations: + org_id = org["id"] + org_name = org["name"] + is_current = "●" if org_id == actual_current_org_id else " " + + table.add_row(is_current, str(org_id), org_name, key=str(org_id)) + + yield table + + yield Static( + "\n💡 Selecting an organization will update your CODEGEN_ORG_ID environment variable.", + classes="help" + ) + + yield Footer() + + def on_mount(self) -> None: + """Called when the screen is mounted.""" + # Set focus on the table if it exists + if self.organizations: + try: + table = self.query_one("#orgs-table", DataTable) + table.focus() + except Exception: + pass + + def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None: + """Handle DataTable row selection (Enter key).""" + if event.data_table.id == "orgs-table": + self._handle_org_selection() + + def action_select_org(self) -> None: + """Select the highlighted organization (fallback action).""" + self._handle_org_selection() + + def _handle_org_selection(self) -> None: + """Handle organization selection logic.""" + if not self.organizations: + self.notify("❌ No organizations available", severity="error") + return + + try: + table = self.query_one("#orgs-table", DataTable) + + if table.cursor_row is not None and table.cursor_row < len(self.organizations): + # Get the selected organization directly from the cursor position + selected_org = self.organizations[table.cursor_row] + selected_org_id = selected_org["id"] + + # Set the organization + self._set_organization(selected_org_id, selected_org["name"]) + else: + self.notify(f"❌ Invalid cursor position: {table.cursor_row}/{len(self.organizations)}", severity="error") + except Exception as e: + self.notify(f"❌ Error in select org: {e}", severity="error") + + def _set_organization(self, org_id: int, org_name: str) -> None: + """Set the selected organization as default.""" + # Set environment variable + os.environ["CODEGEN_ORG_ID"] = str(org_id) + + # Try to update .env file + env_updated = self._update_env_file(org_id) + + if env_updated: + self.notify(f"✓ Set default organization: {org_name} (ID: {org_id})") + self.notify("✓ Updated .env file with CODEGEN_ORG_ID") + else: + self.notify(f"✓ Set organization: {org_name} (ID: {org_id})") + self.notify("ℹ Add 'export CODEGEN_ORG_ID={org_id}' to your shell for persistence") + + # Wait a moment for user to see the notifications, then close + self.set_timer(2.0, self._close_screen) + + def _update_env_file(self, org_id: int) -> bool: + """Update the .env file with the new organization ID.""" + env_file_path = ".env" + + try: + lines = [] + key_found = False + + # Read existing lines if file exists + if os.path.exists(env_file_path): + with open(env_file_path) as f: + lines = f.readlines() + + # Ensure all lines end with newline + for i, line in enumerate(lines): + if not line.endswith('\n'): + lines[i] = line + '\n' + + # Update existing CODEGEN_ORG_ID or note that we need to add it + for i, line in enumerate(lines): + if line.strip().startswith("CODEGEN_ORG_ID="): + lines[i] = f"CODEGEN_ORG_ID={org_id}\n" + key_found = True + break + + # Add new line if not found + if not key_found: + lines.append(f"CODEGEN_ORG_ID={org_id}\n") + + # Write back to file + with open(env_file_path, "w") as f: + f.writelines(lines) + + return True + + except Exception: + return False + + def _close_screen(self) -> None: + """Close the screen.""" + try: + # Pop ourselves from the screen stack + self.app.pop_screen() + except Exception: + # Fallback - try to dismiss the screen + self.dismiss() + + def action_quit(self) -> None: + """Quit the application or close the screen.""" + self._close_screen() + + +class OrgSelectorApp(App): + """Standalone app wrapper for the organization selector.""" + + CSS_PATH = "../../tui/codegen_theme.tcss" # Use custom Codegen theme + TITLE = "Organization Selector - Codegen CLI" + BINDINGS = [ + Binding("escape,ctrl+c", "quit", "Quit", priority=True), + Binding("enter", "select_org", "Select", show=True), + Binding("q", "quit", "Quit", show=True), + ] + + def __init__(self): + super().__init__() + self.organizations = get_cached_organizations() or [] + self.current_org_id = get_current_org_id() + + def compose(self) -> ComposeResult: + """Create child widgets for the app.""" + yield Header() + + if not self.organizations: + yield Container( + Static("⚠️ No organizations found. Please run 'codegen login' first.", classes="warning-message"), + id="no-orgs-warning" + ) + else: + with Vertical(): + yield Static("🏢 Select Your Organization", classes="title") + yield Static("Use ↑↓ to navigate, Enter to select, Q/Esc to quit", classes="help") + + table = DataTable(id="orgs-table", cursor_type="row") + table.add_columns("Current", "ID", "Organization Name") + + # Get the actual current org ID (checks environment variables first) + actual_current_org_id = resolve_org_id() + + for org in self.organizations: + org_id = org["id"] + org_name = org["name"] + is_current = "●" if org_id == actual_current_org_id else " " + + table.add_row(is_current, str(org_id), org_name, key=str(org_id)) + + yield table + + yield Static( + "\n💡 Selecting an organization will update your CODEGEN_ORG_ID environment variable.", + classes="help" + ) + + yield Footer() + + def on_mount(self) -> None: + """Called when the app mounts.""" + # Set focus on the table if it exists + if self.organizations: + try: + table = self.query_one("#orgs-table", DataTable) + table.focus() + except Exception: + pass + + def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None: + """Handle DataTable row selection (Enter key).""" + if event.data_table.id == "orgs-table": + self._handle_org_selection() + + def action_select_org(self) -> None: + """Select the highlighted organization (fallback action).""" + self._handle_org_selection() + + def _handle_org_selection(self) -> None: + """Handle organization selection logic.""" + if not self.organizations: + self.notify("❌ No organizations available", severity="error") + return + + try: + table = self.query_one("#orgs-table", DataTable) + + if table.cursor_row is not None and table.cursor_row < len(self.organizations): + # Get the selected organization directly from the cursor position + selected_org = self.organizations[table.cursor_row] + selected_org_id = selected_org["id"] + + # Set the organization + self._set_organization(selected_org_id, selected_org["name"]) + else: + self.notify(f"❌ Invalid cursor position: {table.cursor_row}/{len(self.organizations)}", severity="error") + except Exception as e: + self.notify(f"❌ Error in select org: {e}", severity="error") + + def _set_organization(self, org_id: int, org_name: str) -> None: + """Set the selected organization as default.""" + # Set environment variable + os.environ["CODEGEN_ORG_ID"] = str(org_id) + + # Try to update .env file + env_updated = self._update_env_file(org_id) + + if env_updated: + self.notify(f"✓ Set default organization: {org_name} (ID: {org_id})") + self.notify("✓ Updated .env file with CODEGEN_ORG_ID") + else: + self.notify(f"✓ Set organization: {org_name} (ID: {org_id})") + self.notify("ℹ Add 'export CODEGEN_ORG_ID={org_id}' to your shell for persistence") + + # Wait a moment for user to see the notifications, then exit + self.set_timer(2.0, self.exit) + + def _update_env_file(self, org_id: int) -> bool: + """Update the .env file with the new organization ID.""" + env_file_path = ".env" + + try: + lines = [] + key_found = False + + # Read existing lines if file exists + if os.path.exists(env_file_path): + with open(env_file_path) as f: + lines = f.readlines() + + # Ensure all lines end with newline + for i, line in enumerate(lines): + if not line.endswith('\n'): + lines[i] = line + '\n' + + # Update existing CODEGEN_ORG_ID or note that we need to add it + for i, line in enumerate(lines): + if line.strip().startswith("CODEGEN_ORG_ID="): + lines[i] = f"CODEGEN_ORG_ID={org_id}\n" + key_found = True + break + + # Add new line if not found + if not key_found: + lines.append(f"CODEGEN_ORG_ID={org_id}\n") + + # Write back to file + with open(env_file_path, "w") as f: + f.writelines(lines) + + return True + + except Exception: + return False + + def action_quit(self) -> None: + """Quit the application.""" + self.exit() \ No newline at end of file diff --git a/src/codegen.backup/cli/commands/profile/main.py b/src/codegen.backup/cli/commands/profile/main.py new file mode 100644 index 000000000..a35be06b8 --- /dev/null +++ b/src/codegen.backup/cli/commands/profile/main.py @@ -0,0 +1,192 @@ +"""Profile command for the Codegen CLI.""" + +import requests +import typer +from rich.console import Console + +from codegen.cli.api.endpoints import API_ENDPOINT +from codegen.cli.auth.token_manager import ( + get_cached_organizations, + get_current_org_name, + get_current_token, + get_current_user_info, + set_default_organization, +) +from codegen.cli.rich.spinners import create_spinner +from codegen.cli.utils.org import resolve_org_id +from codegen.cli.utils.simple_selector import simple_org_selector + +console = Console() + +# Create the profile Typer app +profile_app = typer.Typer(name="profile", help="Manage user profile and organization settings.") + + +def _get_profile_data() -> dict: + """Get profile data (shared between commands).""" + # Get the current token + token = get_current_token() + if not token: + console.print("[red]Error:[/red] Not authenticated. Please run 'codegen login' first.") + raise typer.Exit(1) + + # Try to get stored user and org info first (fast, no API calls) + user_info = get_current_user_info() + org_name = get_current_org_name() + org_id = resolve_org_id() # This now uses stored data first + + # If we have stored data, use it directly + if user_info and user_info.get("id"): + user_id = user_info.get("id", "Unknown") + full_name = user_info.get("full_name", "") + email = user_info.get("email", "") + github_username = user_info.get("github_username", "") + role = "Member" # Default role for stored data + else: + # Fall back to API call if no stored data + spinner = create_spinner("Fetching user profile info...") + spinner.start() + try: + headers = {"Authorization": f"Bearer {token}"} + 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", "Unknown") + full_name = user_data.get("full_name", "") + email = user_data.get("email", "") + github_username = user_data.get("github_username", "") + role = user_data.get("role", "Member") + except requests.RequestException as e: + spinner.stop() + console.print(f"[red]Error:[/red] Failed to fetch profile information: {e}") + raise typer.Exit(1) + finally: + spinner.stop() + + # If no stored org name but we have an org_id, try to fetch it + if org_id and not org_name: + spinner = create_spinner("Fetching organization info...") + spinner.start() + try: + headers = {"Authorization": f"Bearer {token}"} + orgs_response = requests.get(f"{API_ENDPOINT.rstrip('/')}/v1/organizations", headers=headers) + orgs_response.raise_for_status() + orgs_data = orgs_response.json() + + # Find the organization by ID + orgs = orgs_data.get("items", []) + for org in orgs: + if org.get("id") == org_id: + org_name = org.get("name") + break + except requests.RequestException: + # Ignore errors for org name lookup - not critical + pass + finally: + spinner.stop() + + return { + "user_id": user_id, + "full_name": full_name, + "email": email, + "github_username": github_username, + "role": role, + "org_name": org_name, + "org_id": org_id, + } + + +@profile_app.callback(invoke_without_command=True) +def profile_main(ctx: typer.Context): + """Display organization selection dropdown or profile info.""" + if ctx.invoked_subcommand is None: + # No subcommand - show organization selector + _show_org_selector() + + +@profile_app.command("list") +def profile_list(): + """List all available organizations.""" + data = _get_profile_data() + cached_orgs = get_cached_organizations() + + if not cached_orgs: + console.print("[yellow]No organizations found. Please run 'codegen login' first.[/yellow]") + return + + # Build profile information + if data["user_id"] != "Unknown": + console.print(f"[dim]User ID:[/dim] [blue]{data['user_id']}[/blue]") + if data["full_name"]: + console.print(f"[dim]Name:[/dim] [blue]{data['full_name']}[/blue]") + if data["email"]: + console.print(f"[dim]Email:[/dim] [blue]{data['email']}[/blue]") + if data["github_username"]: + console.print(f"[dim]GitHub:[/dim] [blue]{data['github_username']}[/blue]") + if data["role"]: + console.print(f"[dim]Role:[/dim] [blue]{data['role']}[/blue]") + + # Current organization + if data["org_name"]: + console.print(f"[dim]Current Org:[/dim] [blue]{data['org_name']} ({data['org_id']})[/blue]") + elif data["org_id"]: + console.print(f"[dim]Current Org:[/dim] [blue]Organization {data['org_id']}[/blue]") + else: + console.print("[dim]Current Org:[/dim] [yellow]Not configured[/yellow]") + + console.print() + console.print("[dim]Available Organizations:[/dim]") + + for org in cached_orgs: + org_id = org.get("id") + org_name = org.get("name") + is_current = " [green](current)[/green]" if org_id == data["org_id"] else "" + console.print(f" • [blue]{org_name}[/blue] [dim](ID: {org_id})[/dim]{is_current}") + + +def _show_org_selector(): + """Show the organization selector.""" + cached_orgs = get_cached_organizations() + + if not cached_orgs: + console.print("[red]Error:[/red] No organizations found. Please run 'codegen login' first.") + raise typer.Exit(1) + + if len(cached_orgs) == 1: + # Only one org, set it as default + org = cached_orgs[0] + org_id = org.get("id") + org_name = org.get("name") + try: + set_default_organization(org_id, org_name) + console.print(f"[green]✓[/green] Set default organization: {org_name} (ID: {org_id})") + except Exception as e: + console.print(f"[red]Error:[/red] Failed to set default organization: {e}") + raise typer.Exit(1) + return + + # Multiple orgs - show simple selector + current_org_id = resolve_org_id() + console.print("[blue]Select your default organization:[/blue]") + + selected_org = simple_org_selector(organizations=cached_orgs, current_org_id=current_org_id, title="👤 Select Default Organization") + + if selected_org: + org_id = selected_org.get("id") + org_name = selected_org.get("name") + try: + set_default_organization(org_id, org_name) + console.print(f"\n[green]✓ Set default organization:[/green] {org_name} (ID: {org_id})") + console.print("[green]✓ Updated ~/.codegen/auth.json[/green]") + except Exception as e: + console.print(f"\n[red]Error:[/red] Failed to set default organization: {e}") + raise typer.Exit(1) + else: + console.print("\n[yellow]No organization selected.[/yellow]") + + +# For backward compatibility, export the profile function +def profile(): + """Display organization selector (legacy function).""" + _show_org_selector() diff --git a/src/codegen.backup/cli/commands/repo/__init__.py b/src/codegen.backup/cli/commands/repo/__init__.py new file mode 100644 index 000000000..af83c5eba --- /dev/null +++ b/src/codegen.backup/cli/commands/repo/__init__.py @@ -0,0 +1,5 @@ +"""Repository management commands.""" + +from .main import repo + +__all__ = ["repo"] \ No newline at end of file diff --git a/src/codegen.backup/cli/commands/repo/main.py b/src/codegen.backup/cli/commands/repo/main.py new file mode 100644 index 000000000..d3eb00fa7 --- /dev/null +++ b/src/codegen.backup/cli/commands/repo/main.py @@ -0,0 +1,159 @@ +"""Repository management command for managing repository configuration.""" + +import typer +from rich.console import Console +from rich.table import Table + +from codegen.cli.utils.repo import ( + get_current_repo_id, + get_repo_env_status, + set_repo_env_variable, + update_env_file_with_repo, + clear_repo_env_variables, + ensure_repositories_cached +) +from codegen.cli.auth.token_manager import get_current_token + +console = Console() + + +def repo( + set_default: int | None = typer.Option(None, "--set-default", "-s", help="Set default repository ID"), + clear: bool = typer.Option(False, "--clear", "-c", help="Clear repository configuration"), + list_config: bool = typer.Option(False, "--list", "-l", help="List current repository configuration"), + list_repos: bool = typer.Option(False, "--list-repos", "-lr", help="List available repositories"), +): + """Manage repository configuration and environment variables.""" + + # Handle list repositories mode + if list_repos: + _list_repositories() + return + + # Handle list config mode + if list_config: + _list_repo_config() + return + + # Handle clear mode + if clear: + _clear_repo_config() + return + + # Handle set default mode + if set_default is not None: + _set_default_repository(set_default) + return + + # No flags provided, launch TUI + _run_repo_selector_tui() + + +def _list_repo_config() -> None: + """List current repository configuration.""" + table = Table(title="Repository Configuration") + table.add_column("Setting", style="cyan", no_wrap=True) + table.add_column("Value", style="green") + table.add_column("Status", style="yellow") + + # Current repository ID + current_repo_id = get_current_repo_id() + if current_repo_id: + table.add_row("Current Repository ID", str(current_repo_id), "✅ Active") + else: + table.add_row("Current Repository ID", "Not configured", "❌ Inactive") + + # Environment variables + env_status = get_repo_env_status() + for var_name, value in env_status.items(): + status = "✅ Set" if value != "Not set" else "❌ Not set" + table.add_row(var_name, value, status) + + console.print(table) + + +def _list_repositories() -> None: + """List all available repositories.""" + # Check if user is authenticated + token = get_current_token() + if not token: + console.print("[red]Error:[/red] Not authenticated. Please run 'codegen login' first.") + raise typer.Exit(1) + + # Get cached or fetch repositories + repositories = ensure_repositories_cached() + if not repositories: + console.print("[red]Error:[/red] No repositories found.") + raise typer.Exit(1) + + table = Table(title="Available Repositories") + table.add_column("ID", style="cyan", no_wrap=True) + table.add_column("Name", style="green") + table.add_column("Description", style="dim") + table.add_column("Current", style="yellow") + + current_repo_id = get_current_repo_id() + + for repo in repositories: + repo_id = repo.get("id", "Unknown") + repo_name = repo.get("name", "Unknown") + repo_desc = repo.get("description", "") + is_current = "●" if repo_id == current_repo_id else "" + + table.add_row(str(repo_id), repo_name, repo_desc, is_current) + + console.print(table) + + +def _set_default_repository(repo_id: int) -> None: + """Set default repository ID.""" + try: + # Set in environment + success = set_repo_env_variable(repo_id, "CODEGEN_REPO_ID") + if not success: + console.print("[red]Error:[/red] Failed to set repository ID in environment.") + raise typer.Exit(1) + + # Try to update .env file + env_updated = update_env_file_with_repo(repo_id) + + if env_updated: + console.print(f"[green]✓[/green] Set default repository ID to: [cyan]{repo_id}[/cyan]") + console.print("[green]✓[/green] Updated .env file with CODEGEN_REPO_ID") + else: + console.print(f"[green]✓[/green] Set repository ID to: [cyan]{repo_id}[/cyan]") + console.print("[yellow]ℹ[/yellow] Could not update .env file. Add 'export CODEGEN_REPO_ID={repo_id}' to your shell for persistence") + + except Exception as e: + console.print(f"[red]Error:[/red] Failed to set default repository: {e}") + raise typer.Exit(1) + + +def _clear_repo_config() -> None: + """Clear repository configuration.""" + try: + clear_repo_env_variables() + console.print("[green]✓[/green] Cleared repository configuration from environment variables") + + # Note: We don't automatically clear the .env file to avoid data loss + console.print("[yellow]ℹ[/yellow] To permanently remove from .env file, manually delete the CODEGEN_REPO_ID line") + + except Exception as e: + console.print(f"[red]Error:[/red] Failed to clear repository configuration: {e}") + raise typer.Exit(1) + + +def _run_repo_selector_tui() -> None: + """Launch the repository selector TUI.""" + try: + from codegen.cli.commands.repo.tui import RepoSelectorApp + + app = RepoSelectorApp() + app.run() + + except ImportError: + console.print("[red]Error:[/red] Repository selector TUI not available") + raise typer.Exit(1) + except Exception as e: + console.print(f"[red]Error:[/red] Failed to launch repository selector: {e}") + raise typer.Exit(1) \ No newline at end of file diff --git a/src/codegen.backup/cli/commands/repo/tui.py b/src/codegen.backup/cli/commands/repo/tui.py new file mode 100644 index 000000000..a9368724c --- /dev/null +++ b/src/codegen.backup/cli/commands/repo/tui.py @@ -0,0 +1,303 @@ +"""Repository selector TUI using Textual.""" + +import os + +from textual.app import App, ComposeResult +from textual.binding import Binding +from textual.containers import Container, Vertical +from textual.screen import Screen +from textual.widgets import DataTable, Footer, Header, Static + +from codegen.cli.auth.token_manager import get_cached_repositories, get_current_token +from codegen.cli.utils.repo import ( + get_current_repo_id, + set_repo_env_variable, + update_env_file_with_repo, + ensure_repositories_cached +) +from codegen.cli.utils.org import resolve_org_id + + +class RepoSelectorTUI(Screen): + """TUI for selecting and switching repositories.""" + + BINDINGS = [ + Binding("escape,ctrl+c", "quit", "Quit", priority=True), + Binding("enter", "select_repo", "Select", show=True), + Binding("q", "quit", "Quit", show=True), + ] + + def __init__(self): + super().__init__() + self.repositories = ensure_repositories_cached() or [] + self.current_repo_id = get_current_repo_id() + + def compose(self) -> ComposeResult: + """Create child widgets for the app.""" + yield Header() + + if not self.repositories: + yield Container( + Static("⚠️ No repositories found. Fetching repositories...", classes="warning-message"), + id="no-repos-warning" + ) + else: + with Vertical(): + yield Static("🗂️ Select Your Repository", classes="title") + yield Static("Use ↑↓ to navigate, Enter to select, Q/Esc to quit", classes="help") + + table = DataTable(id="repos-table", cursor_type="row") + table.add_columns("Current", "ID", "Repository Name") + + # Get the actual current repo ID (checks environment variables first) + actual_current_repo_id = get_current_repo_id() + + for repo in self.repositories: + repo_id = repo["id"] + repo_name = repo["name"] + is_current = "●" if repo_id == actual_current_repo_id else " " + + table.add_row(is_current, str(repo_id), repo_name, key=str(repo_id)) + + yield table + + yield Static( + "\n💡 Selecting a repository will update your CODEGEN_REPO_ID environment variable.", + classes="help" + ) + + yield Footer() + + def on_mount(self) -> None: + """Called when the screen is mounted.""" + if self.repositories: + try: + table = self.query_one("#repos-table", DataTable) + table.focus() + except Exception: + pass + + def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None: + """Handle DataTable row selection (Enter key).""" + if event.data_table.id == "repos-table": + self._handle_repo_selection() + + def action_select_repo(self) -> None: + """Select repository (fallback for direct key binding).""" + self._handle_repo_selection() + + def _handle_repo_selection(self) -> None: + """Handle repository selection logic.""" + try: + table = self.query_one("#repos-table", DataTable) + if table.cursor_row is not None and table.cursor_row < len(self.repositories): + selected_repo = self.repositories[table.cursor_row] + repo_id = selected_repo["id"] + repo_name = selected_repo["name"] + + self._set_repository(repo_id, repo_name) + except Exception as e: + self.notify(f"❌ Error selecting repository: {e}", severity="error") + + def _set_repository(self, repo_id: int, repo_name: str) -> None: + """Set the selected repository as the current one.""" + # Update environment variable + os.environ["CODEGEN_REPO_ID"] = str(repo_id) + + # Try to update .env file + env_updated = self._update_env_file(repo_id) + + if env_updated: + self.notify(f"✓ Set default repository: {repo_name} (ID: {repo_id})") + self.notify("✓ Updated .env file with CODEGEN_REPO_ID") + else: + self.notify(f"✓ Set repository: {repo_name} (ID: {repo_id})") + self.notify("ℹ Add 'export CODEGEN_REPO_ID={repo_id}' to your shell for persistence") + + # Wait a moment for user to see the notifications, then exit + self.set_timer(2.0, self._close_screen) + + def _update_env_file(self, repo_id: int) -> bool: + """Update the .env file with the new repository ID.""" + env_file_path = ".env" + + try: + lines = [] + key_updated = False + key_to_update = "CODEGEN_REPO_ID" + + # Read existing .env file if it exists + if os.path.exists(env_file_path): + with open(env_file_path, "r") as f: + lines = f.readlines() + + # Update or add the key + for i, line in enumerate(lines): + if line.strip().startswith(f"{key_to_update}="): + lines[i] = f"{key_to_update}={repo_id}\n" + key_updated = True + break + + # If key wasn't found, add it + if not key_updated: + if lines and not lines[-1].endswith('\n'): + lines.append('\n') + lines.append(f"{key_to_update}={repo_id}\n") + + # Write back to file + with open(env_file_path, "w") as f: + f.writelines(lines) + + return True + + except Exception: + return False + + def _close_screen(self) -> None: + """Close the screen.""" + if hasattr(self.app, 'pop_screen'): + self.app.pop_screen() + else: + self.app.exit() + + def action_quit(self) -> None: + """Quit the TUI.""" + self._close_screen() + + +class RepoSelectorApp(App): + """Standalone app wrapper for the repository selector.""" + + CSS_PATH = "../../tui/codegen_theme.tcss" # Use custom Codegen theme + TITLE = "Repository Selector - Codegen CLI" + BINDINGS = [ + Binding("escape,ctrl+c", "quit", "Quit", priority=True), + Binding("enter", "select_repo", "Select", show=True), + Binding("q", "quit", "Quit", show=True), + ] + + def __init__(self): + super().__init__() + self.repositories = ensure_repositories_cached() or [] + self.current_repo_id = get_current_repo_id() + + def compose(self) -> ComposeResult: + """Create child widgets for the app.""" + yield Header() + + if not self.repositories: + yield Container( + Static("⚠️ No repositories found. Fetching repositories...", classes="warning-message"), + id="no-repos-warning" + ) + else: + with Vertical(): + yield Static("🗂️ Select Your Repository", classes="title") + yield Static("Use ↑↓ to navigate, Enter to select, Q/Esc to quit", classes="help") + + table = DataTable(id="repos-table", cursor_type="row") + table.add_columns("Current", "ID", "Repository Name") + + # Get the actual current repo ID (checks environment variables first) + actual_current_repo_id = get_current_repo_id() + + for repo in self.repositories: + repo_id = repo["id"] + repo_name = repo["name"] + is_current = "●" if repo_id == actual_current_repo_id else " " + + table.add_row(is_current, str(repo_id), repo_name, key=str(repo_id)) + + yield table + + yield Static( + "\n💡 Selecting a repository will update your CODEGEN_REPO_ID environment variable.", + classes="help" + ) + + yield Footer() + + def on_mount(self) -> None: + """Called when the app starts.""" + if self.repositories: + try: + table = self.query_one("#repos-table", DataTable) + table.focus() + except Exception: + pass + + def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None: + """Handle DataTable row selection (Enter key).""" + if event.data_table.id == "repos-table": + self._handle_repo_selection() + + def action_select_repo(self) -> None: + """Select repository (fallback for direct key binding).""" + self._handle_repo_selection() + + def _handle_repo_selection(self) -> None: + """Handle repository selection logic.""" + try: + table = self.query_one("#repos-table", DataTable) + if table.cursor_row is not None and table.cursor_row < len(self.repositories): + selected_repo = self.repositories[table.cursor_row] + repo_id = selected_repo["id"] + repo_name = selected_repo["name"] + + self._set_repository(repo_id, repo_name) + except Exception as e: + self.notify(f"❌ Error selecting repository: {e}", severity="error") + + def _set_repository(self, repo_id: int, repo_name: str) -> None: + """Set the selected repository as the current one.""" + # Update environment variable + os.environ["CODEGEN_REPO_ID"] = str(repo_id) + + # Try to update .env file + env_updated = self._update_env_file(repo_id) + + if env_updated: + self.notify(f"✓ Set default repository: {repo_name} (ID: {repo_id})") + self.notify("✓ Updated .env file with CODEGEN_REPO_ID") + else: + self.notify(f"✓ Set repository: {repo_name} (ID: {repo_id})") + self.notify("ℹ Add 'export CODEGEN_REPO_ID={repo_id}' to your shell for persistence") + + # Wait a moment for user to see the notifications, then exit + self.set_timer(2.0, self.exit) + + def _update_env_file(self, repo_id: int) -> bool: + """Update the .env file with the new repository ID.""" + env_file_path = ".env" + + try: + lines = [] + key_updated = False + key_to_update = "CODEGEN_REPO_ID" + + # Read existing .env file if it exists + if os.path.exists(env_file_path): + with open(env_file_path, "r") as f: + lines = f.readlines() + + # Update or add the key + for i, line in enumerate(lines): + if line.strip().startswith(f"{key_to_update}="): + lines[i] = f"{key_to_update}={repo_id}\n" + key_updated = True + break + + # If key wasn't found, add it + if not key_updated: + if lines and not lines[-1].endswith('\n'): + lines.append('\n') + lines.append(f"{key_to_update}={repo_id}\n") + + # Write back to file + with open(env_file_path, "w") as f: + f.writelines(lines) + + return True + + except Exception: + return False \ No newline at end of file diff --git a/src/codegen.backup/cli/commands/style_debug/main.py b/src/codegen.backup/cli/commands/style_debug/main.py new file mode 100644 index 000000000..3c7fd14c8 --- /dev/null +++ b/src/codegen.backup/cli/commands/style_debug/main.py @@ -0,0 +1,19 @@ +"""Debug command to visualize CLI styling components.""" + +import time + +import typer + +from codegen.cli.rich.spinners import create_spinner + + +def style_debug(text: str = typer.Option("Loading...", help="Text to show in the spinner")): + """Debug command to visualize CLI styling (spinners, etc).""" + try: + with create_spinner(text) as status: + # Run indefinitely until Ctrl+C + while True: + time.sleep(0.1) + except KeyboardInterrupt: + # Exit gracefully on Ctrl+C + pass diff --git a/src/codegen.backup/cli/commands/tools/__init__.py b/src/codegen.backup/cli/commands/tools/__init__.py new file mode 100644 index 000000000..2fcf268de --- /dev/null +++ b/src/codegen.backup/cli/commands/tools/__init__.py @@ -0,0 +1 @@ +"""Tools command module.""" diff --git a/src/codegen.backup/cli/commands/tools/main.py b/src/codegen.backup/cli/commands/tools/main.py new file mode 100644 index 000000000..3b1ef81b4 --- /dev/null +++ b/src/codegen.backup/cli/commands/tools/main.py @@ -0,0 +1,101 @@ +"""Tools command for the Codegen CLI.""" + +import requests +import typer +from rich.console import Console +from rich.table import Table + +from codegen.cli.api.endpoints import API_ENDPOINT +from codegen.cli.auth.token_manager import get_current_token +from codegen.cli.rich.spinners import create_spinner +from codegen.cli.utils.org import resolve_org_id + +console = Console() + + +def tools(org_id: int | None = typer.Option(None, help="Organization ID (defaults to CODEGEN_ORG_ID/REPOSITORY_ORG_ID or auto-detect)")): + """List available tools from the Codegen API.""" + # Get the current token + token = get_current_token() + if not token: + console.print("[red]Error:[/red] Not authenticated. Please run 'codegen login' first.") + raise typer.Exit(1) + + try: + # Resolve org id + resolved_org_id = resolve_org_id(org_id) + if resolved_org_id is None: + console.print("[red]Error:[/red] Organization ID not provided. Pass --org-id, set CODEGEN_ORG_ID, or REPOSITORY_ORG_ID.") + raise typer.Exit(1) + + # Make API request to list tools with spinner + spinner = create_spinner("Fetching available tools...") + spinner.start() + + try: + headers = {"Authorization": f"Bearer {token}"} + url = f"{API_ENDPOINT.rstrip('/')}/v1/organizations/{resolved_org_id}/tools" + response = requests.get(url, headers=headers) + response.raise_for_status() + response_data = response.json() + finally: + spinner.stop() + + # Extract tools from the response structure + if isinstance(response_data, dict) and "tools" in response_data: + tools_data = response_data["tools"] + total_count = response_data.get("total_count", len(tools_data)) + else: + tools_data = response_data + total_count = len(tools_data) if isinstance(tools_data, list) else 1 + + if not tools_data: + console.print("[yellow]No tools found.[/yellow]") + return + + # Handle case where response might be a list of strings vs list of objects + if isinstance(tools_data, list) and len(tools_data) > 0: + # Check if first item is a string or object + if isinstance(tools_data[0], str): + # Simple list of tool names + console.print(f"[green]Found {len(tools_data)} tools:[/green]") + for tool_name in tools_data: + console.print(f" • {tool_name}") + return + + # Create a table to display tools (for structured data) + table = Table( + title="Available Tools", + border_style="blue", + show_header=True, + title_justify="center", + ) + table.add_column("Tool Name", style="cyan", no_wrap=True) + table.add_column("Description", style="white") + table.add_column("Category", style="magenta") + + # Add tools to table + for tool in tools_data: + if isinstance(tool, dict): + tool_name = tool.get("name", "Unknown") + description = tool.get("description", "No description available") + category = tool.get("category", "General") + + # Truncate long descriptions + if len(description) > 80: + description = description[:77] + "..." + + table.add_row(tool_name, description, category) + else: + # Fallback for non-dict items + table.add_row(str(tool), "Unknown", "General") + + console.print(table) + console.print(f"\n[green]Found {total_count} tools available.[/green]") + + except requests.RequestException as e: + console.print(f"[red]Error fetching tools:[/red] {e}", style="bold red") + raise typer.Exit(1) + except Exception as e: + console.print(f"[red]Unexpected error:[/red] {e}", style="bold red") + raise typer.Exit(1) diff --git a/src/codegen.backup/cli/commands/tui/__init__.py b/src/codegen.backup/cli/commands/tui/__init__.py new file mode 100644 index 000000000..f6a335d72 --- /dev/null +++ b/src/codegen.backup/cli/commands/tui/__init__.py @@ -0,0 +1 @@ +"""TUI command module.""" diff --git a/src/codegen.backup/cli/commands/tui/main.py b/src/codegen.backup/cli/commands/tui/main.py new file mode 100644 index 000000000..ec41ed8f4 --- /dev/null +++ b/src/codegen.backup/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.backup/cli/commands/update/main.py b/src/codegen.backup/cli/commands/update/main.py new file mode 100644 index 000000000..f5a22b5e8 --- /dev/null +++ b/src/codegen.backup/cli/commands/update/main.py @@ -0,0 +1,62 @@ +import subprocess +import sys +from importlib.metadata import distribution + +import requests +import rich +import typer +from packaging.version import Version + +import codegen + + +def fetch_pypi_releases(package: str) -> list[str]: + response = requests.get(f"https://pypi.org/pypi/{package}/json") + response.raise_for_status() + return response.json()["releases"].keys() + + +def filter_versions(versions: list[Version], current_version: Version, num_prev_minor_version: int = 1) -> list[Version]: + descending_minor_versions = [v_tuple for v_tuple in sorted(set(v.release[:2] for v in versions), reverse=True) if v_tuple < current_version.release[:2]] + try: + compare_tuple = descending_minor_versions[:num_prev_minor_version][-1] + (0,) + except IndexError: + compare_tuple = (current_version.major, current_version.minor, 0) + + return [v for v in versions if (v.major, v.minor, v.micro) >= compare_tuple] # v.release will only show major,minor if micro doesn't exist. + + +def install_package(package: str, *args: str) -> None: + subprocess.check_call([sys.executable, "-m", "pip", "install", package, *args]) + + +def update( + list_: bool = typer.Option(False, "--list", "-l", help="List all supported versions of the codegen"), + version: str | None = typer.Option(None, "--version", "-v", help="Update to a specific version of the codegen"), +): + """Update Codegen to the latest or specified version + + --list: List all supported versions of the codegen + --version: Update to a specific version of the codegen + """ + if list_ and version: + rich.print("[red]Error:[/red] Cannot specify both --list and --version") + raise typer.Exit(1) + + package_name = codegen.__package__ or "codegen" + package_info = distribution(package_name) + current_version = Version(package_info.version) + + if list_: + releases = fetch_pypi_releases(package_info.name) + filtered_releases = filter_versions([Version(r) for r in releases], current_version, num_prev_minor_version=2) + for release in filtered_releases: + if release.release == current_version.release: + rich.print(f"[bold]{release}[/bold] (current)") + else: + rich.print(release) + elif version: + install_package(f"{package_info.name}=={version}") + else: + # Update to latest version + install_package(package_info.name, "--upgrade") diff --git a/src/codegen.backup/cli/env/constants.py b/src/codegen.backup/cli/env/constants.py new file mode 100644 index 000000000..165048643 --- /dev/null +++ b/src/codegen.backup/cli/env/constants.py @@ -0,0 +1,3 @@ +from codegen.cli.env.enums import Environment + +DEFAULT_ENV = Environment.PRODUCTION diff --git a/src/codegen.backup/cli/env/enums.py b/src/codegen.backup/cli/env/enums.py new file mode 100644 index 000000000..4b5ff813e --- /dev/null +++ b/src/codegen.backup/cli/env/enums.py @@ -0,0 +1,7 @@ +from enum import StrEnum + + +class Environment(StrEnum): + PRODUCTION = "prod" + STAGING = "staging" + DEVELOP = "develop" diff --git a/src/codegen.backup/cli/env/global_env.ipynb b/src/codegen.backup/cli/env/global_env.ipynb new file mode 100644 index 000000000..faf1dd677 --- /dev/null +++ b/src/codegen.backup/cli/env/global_env.ipynb @@ -0,0 +1,70 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# test: parse environment\n", + "from codegen.cli.env.enums import Environment\n", + "\n", + "\n", + "Environment(\"staging\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# test: ENV=prod loads .env.prod\n", + "import os\n", + "\n", + "os.environ[\"ENV\"] = \"prod\"\n", + "from codegen.cli.env.global_env import GlobalEnv\n", + "\n", + "global_env = GlobalEnv()\n", + "print(global_env.ALGOLIA_SEARCH_KEY)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# test: ENV=staging and .env.staging does not exist loads from .env\n", + "import os\n", + "\n", + "os.environ[\"ENV\"] = \"staging\"\n", + "from codegen.cli.env.global_env import GlobalEnv\n", + "\n", + "global_env = GlobalEnv()\n", + "print(global_env.ALGOLIA_SEARCH_KEY)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.0" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/codegen.backup/cli/env/global_env.py b/src/codegen.backup/cli/env/global_env.py new file mode 100644 index 000000000..57eaa02a7 --- /dev/null +++ b/src/codegen.backup/cli/env/global_env.py @@ -0,0 +1,61 @@ +import os + +from dotenv import find_dotenv, load_dotenv + +from codegen.cli.env.constants import DEFAULT_ENV +from codegen.cli.env.enums import Environment + + +class GlobalEnv: + def __init__(self) -> None: + self.ENV = self._parse_env() + self._load_dotenv() + + # =====[ DEV ]===== + self.DEBUG = self._get_env_var("DEBUG") + + # =====[ AUTH ]===== + self.CODEGEN_USER_ACCESS_TOKEN = self._get_env_var("CODEGEN_USER_ACCESS_TOKEN") + + # =====[ ALGOLIA ]===== + self.ALGOLIA_SEARCH_KEY = self._get_env_var("ALGOLIA_SEARCH_KEY") + + # =====[ POSTHOG ]===== + self.POSTHOG_PROJECT_API_KEY = self._get_env_var("POSTHOG_PROJECT_API_KEY") + + # =====[ MODAL ]===== + self.MODAL_ENVIRONMENT = self._get_env_var("MODAL_ENVIRONMENT") + + def _parse_env(self) -> Environment: + env_envvar = os.environ.get("ENV") + if not env_envvar: + return DEFAULT_ENV + if env_envvar not in Environment: + msg = f"Invalid environment: {env_envvar}" + raise ValueError(msg) + return Environment(env_envvar) + + def _load_dotenv(self) -> None: + env_file = find_dotenv(filename=f".env.{self.ENV}") + # if env specific .env file does not exist, try to load .env + load_dotenv(env_file or None, override=True) + + def _get_env_var(self, var_name, required: bool = False) -> str: + if self.ENV == "local": + return "" + + if value := os.environ.get(var_name): + return value + + if required: + msg = f"Environment variable {var_name} is not set with ENV={self.ENV}!" + raise ValueError(msg) + return "" + + def __repr__(self) -> str: + # Returns all env vars in a readable format + return "\n".join([f"{k}={v}" for k, v in self.__dict__.items()]) + + +# NOTE: load and store envvars once +global_env = GlobalEnv() diff --git a/src/codegen.backup/cli/errors.py b/src/codegen.backup/cli/errors.py new file mode 100644 index 000000000..a4f0fd536 --- /dev/null +++ b/src/codegen.backup/cli/errors.py @@ -0,0 +1,60 @@ +# TODO: refactor this file out +import functools + +import rich +import typer +from rich.panel import Panel + + +class AuthError(Exception): + """Error raised if authed user cannot be established.""" + + pass + + +class InvalidTokenError(AuthError): + """Error raised if the token is invalid.""" + + pass + + +class NoTokenError(AuthError): + """Error raised if no token is provided.""" + + pass + + +class CodegenError(Exception): + """Base class for Codegen-specific errors.""" + + pass + + +class ServerError(CodegenError): + """Error raised when the server encounters an error.""" + + pass + + +def format_error_message(error): + """Format error message based on error type.""" + if isinstance(error, AuthError): + return "[red]Authentication Error:[/red] Please run 'codegen login' first." + elif isinstance(error, ServerError): + return "[red]Server Error:[/red] The server encountered an error. Please try again later." + else: + return f"[red]Error:[/red] {error!s}" + + +def handle_auth_error(f): + """Decorator to handle authentication errors gracefully.""" + + @functools.wraps(f) + def wrapper(*args, **kwargs): + try: + return f(*args, **kwargs) + except AuthError: + rich.print(Panel("[red]Authentication Error:[/red] Please run 'codegen login' first.", title="Codegen Error", border_style="red")) + raise typer.Abort() + + return wrapper diff --git a/src/codegen.backup/cli/mcp/README.md b/src/codegen.backup/cli/mcp/README.md new file mode 100644 index 000000000..e41e09de1 --- /dev/null +++ b/src/codegen.backup/cli/mcp/README.md @@ -0,0 +1,40 @@ +# Codegen MCP server + +A MCP server implementation that provides tools and resources for interacting with the Codegen platform APIs, enabling AI agents to manage development workflows and access Codegen services. + +### Dependencies + +- [fastmcp](https://github.com/codegen-sh/fastmcp) + +## Usage + +Most AI Agents that support MCP will have some way to configure the server startup. + +### Cline + +Add this to your `cline_mcp_settings.json` file to get started: + +``` +{ + "mcpServers": { + "codegen-cli": { + "command": "uv", + "args": [ + "--directory", + "/codegen-sdk/src/codegen/cli/mcp", + "run", + "server.py" + ] + } + } +} +``` + +Cursor: +Under the `Settings` > `Feature` > `MCP Servers` section, click "Add New MCP Server" and add the following: + +``` +Name: codegen-mcp +Type: Command +Command: uv --directory /codegen-sdk/src/codegen/cli/mcp run server.py +``` diff --git a/src/codegen.backup/cli/mcp/__init__.py b/src/codegen.backup/cli/mcp/__init__.py new file mode 100644 index 000000000..52218e8fc --- /dev/null +++ b/src/codegen.backup/cli/mcp/__init__.py @@ -0,0 +1 @@ +"""MCP (Model Context Protocol) server for Codegen.""" diff --git a/src/codegen.backup/cli/mcp/api_client.py b/src/codegen.backup/cli/mcp/api_client.py new file mode 100644 index 000000000..8894dad18 --- /dev/null +++ b/src/codegen.backup/cli/mcp/api_client.py @@ -0,0 +1,54 @@ +"""API client management for the Codegen MCP server.""" + +import os + +# Import API client components +try: + from codegen_api_client import ApiClient, Configuration + from codegen_api_client.api import AgentsApi, OrganizationsApi, UsersApi + + API_CLIENT_AVAILABLE = True +except ImportError: + API_CLIENT_AVAILABLE = False + +# Global API client instances +_api_client = None +_agents_api = None +_organizations_api = None +_users_api = None + + +def get_api_client(): + """Get or create the API client instance.""" + global _api_client, _agents_api, _organizations_api, _users_api + + if not API_CLIENT_AVAILABLE: + msg = "codegen-api-client is not available" + raise RuntimeError(msg) + + if _api_client is None: + # Configure the API client + configuration = Configuration() + + # Set base URL from environment or use the CLI endpoint for consistency + # Prefer explicit env override; else match API_ENDPOINT used by CLI commands + from codegen.cli.api.endpoints import API_ENDPOINT + base_url = os.getenv("CODEGEN_API_BASE_URL", API_ENDPOINT.rstrip("/")) + configuration.host = base_url + + # Set authentication + api_key = os.getenv("CODEGEN_API_KEY") + if api_key: + configuration.api_key = {"Authorization": f"Bearer {api_key}"} + + _api_client = ApiClient(configuration) + _agents_api = AgentsApi(_api_client) + _organizations_api = OrganizationsApi(_api_client) + _users_api = UsersApi(_api_client) + + return _api_client, _agents_api, _organizations_api, _users_api + + +def is_api_client_available() -> bool: + """Check if the API client is available.""" + return API_CLIENT_AVAILABLE diff --git a/src/codegen.backup/cli/mcp/prompts.py b/src/codegen.backup/cli/mcp/prompts.py new file mode 100644 index 000000000..5f70fea70 --- /dev/null +++ b/src/codegen.backup/cli/mcp/prompts.py @@ -0,0 +1,14 @@ +"""Prompts and instructions for the Codegen MCP server.""" + +MCP_SERVER_INSTRUCTIONS = ( + "Codegen is an operating system for agents. " + "It allows organizations to run Claude Code instances with superpowers, including unified observability, " + "dynamic sandboxes, powerful MCP integrations, security and more.\n\n" + "This MCP server provides permissioned access to integrations configured by your organization. " + "All tools shown (GitHub, Linear, ClickUp, Notion, Sentry, etc.) are pre-configured and ready to use - " + "they've been provisioned based on your organization's setup and your role permissions. " + "You can confidently use any available tool without worrying about authentication or configuration.\n\n" + "Learn more at https://codegen.com.\n" + "For documentation, visit https://docs.codegen.com/integrations/mcp.\n" + "To install and authenticate this server, run: `uv tool install codegen` then `codegen login`." +) diff --git a/src/codegen.backup/cli/mcp/resources.py b/src/codegen.backup/cli/mcp/resources.py new file mode 100644 index 000000000..4192a0a1a --- /dev/null +++ b/src/codegen.backup/cli/mcp/resources.py @@ -0,0 +1,18 @@ +"""MCP resources for the Codegen server.""" + +from typing import Any + +from fastmcp import FastMCP + + +def register_resources(mcp: FastMCP): + """Register MCP resources with the server.""" + + @mcp.resource("system://manifest", mime_type="application/json") + def get_service_config() -> dict[str, Any]: + """Get the service config.""" + return { + "name": "mcp-codegen", + "version": "0.1.0", + "description": "The MCP server for the Codegen platform API integration.", + } diff --git a/src/codegen.backup/cli/mcp/runner.py b/src/codegen.backup/cli/mcp/runner.py new file mode 100644 index 000000000..1dedba45d --- /dev/null +++ b/src/codegen.backup/cli/mcp/runner.py @@ -0,0 +1,43 @@ +"""MCP server runner for the Codegen platform.""" + +from fastmcp import FastMCP + +from .resources import register_resources +from .tools.dynamic import register_dynamic_tools +from .tools.static import register_static_tools + + +def run_server(transport: str = "stdio", host: str = "localhost", port: int | None = None, available_tools: list | None = None): + """Run the MCP server with the specified transport.""" + from .prompts import MCP_SERVER_INSTRUCTIONS + + # Initialize FastMCP server + mcp = FastMCP( + "codegen-mcp", + instructions=MCP_SERVER_INSTRUCTIONS, + ) + + # Register all components + register_resources(mcp) + register_static_tools(mcp) + + # Register dynamic tools if provided + if available_tools: + print("🔧 Registering dynamic tools from API...") + register_dynamic_tools(mcp, available_tools) + print(f"✅ Registered {len(available_tools)} dynamic tools") + + if transport == "stdio": + print("🚀 MCP server running on stdio transport") + mcp.run(transport="stdio") + elif transport == "http": + if port is None: + port = 8000 + print(f"🚀 MCP server running on http://{host}:{port}") + # Note: FastMCP may not support HTTP transport directly + # This is a placeholder for future HTTP transport support + print(f"HTTP transport not yet implemented. Would run on {host}:{port}") + mcp.run(transport="stdio") # Fallback to stdio for now + else: + msg = f"Unsupported transport: {transport}" + raise ValueError(msg) diff --git a/src/codegen.backup/cli/mcp/server.py b/src/codegen.backup/cli/mcp/server.py new file mode 100644 index 000000000..dcf41514d --- /dev/null +++ b/src/codegen.backup/cli/mcp/server.py @@ -0,0 +1,18 @@ +"""Main MCP server entry point for the Codegen platform. + +This module provides the main entry point for the Codegen MCP server. +The actual server functionality is distributed across several modules: + +- api_client.py: API client management +- prompts.py: Server instructions and prompts +- resources.py: MCP resources +- tools/: Tool modules (static and dynamic) +- runner.py: Server runner and configuration +""" + +from .runner import run_server + +if __name__ == "__main__": + # Initialize and run the server + print("Starting codegen server...") + run_server() diff --git a/src/codegen.backup/cli/mcp/tools/__init__.py b/src/codegen.backup/cli/mcp/tools/__init__.py new file mode 100644 index 000000000..c2da70d78 --- /dev/null +++ b/src/codegen.backup/cli/mcp/tools/__init__.py @@ -0,0 +1 @@ +"""Tools module for the Codegen MCP server.""" diff --git a/src/codegen.backup/cli/mcp/tools/dynamic.py b/src/codegen.backup/cli/mcp/tools/dynamic.py new file mode 100644 index 000000000..0b350ac29 --- /dev/null +++ b/src/codegen.backup/cli/mcp/tools/dynamic.py @@ -0,0 +1,174 @@ +"""Dynamic tool registration for the Codegen MCP server.""" + +import json +from typing import Annotated + +from fastmcp import FastMCP + +from .executor import execute_tool_via_api + + +def register_dynamic_tools(mcp: FastMCP, available_tools: list): + """Register all available tools from the API as individual MCP tools.""" + import inspect + + for i, tool_info in enumerate(available_tools): + # Skip None or invalid tool entries + if not tool_info or not isinstance(tool_info, dict): + print(f"⚠️ Skipping invalid tool entry at index {i}: {tool_info}") + continue + + try: + tool_name = tool_info.get("name", "unknown_tool") + tool_description = tool_info.get("description", "No description available").replace("'", '"').replace('"', '\\"') + tool_parameters = tool_info.get("parameters", {}) + + # Parse the parameter schema + if tool_parameters is None: + tool_parameters = {} + properties = tool_parameters.get("properties", {}) + required = tool_parameters.get("required", []) + except Exception as e: + print(f"❌ Error processing tool at index {i}: {e}") + print(f"Tool data: {tool_info}") + continue + + def make_tool_function(name: str, description: str, props: dict, req: list): + # Create function dynamically with proper parameters + def create_dynamic_function(): + # Build parameter list for the function + param_list = [] + param_annotations = {} + + # Collect required and optional parameters separately + required_params = [] + optional_params = [] + + # Add other parameters from schema + for param_name, param_info in props.items(): + param_type = param_info.get("type", "string") + param_desc = param_info.get("description", f"Parameter {param_name}").replace("'", '"').replace('"', '\\"') + is_required = param_name in req + + # Special handling for tool_call_id - always make it optional + if param_name == "tool_call_id": + optional_params.append("tool_call_id: Annotated[str, 'Unique identifier for this tool call'] = 'mcp_call'") + continue + + # Convert JSON schema types to Python types + if param_type == "string": + py_type = "str" + elif param_type == "integer": + py_type = "int" + elif param_type == "number": + py_type = "float" + elif param_type == "boolean": + py_type = "bool" + elif param_type == "array": + items_type = param_info.get("items", {}).get("type", "string") + if items_type == "string": + py_type = "list[str]" + else: + py_type = "list" + else: + py_type = "str" # Default fallback + + # Handle optional parameters (anyOf with null) + if "anyOf" in param_info: + py_type = f"{py_type} | None" + if not is_required: + default_val = param_info.get("default", "None") + if isinstance(default_val, str) and default_val != "None": + default_val = f'"{default_val}"' + optional_params.append(f"{param_name}: Annotated[{py_type}, '{param_desc}'] = {default_val}") + else: + required_params.append(f"{param_name}: Annotated[{py_type}, '{param_desc}']") + elif is_required: + required_params.append(f"{param_name}: Annotated[{py_type}, '{param_desc}']") + else: + # Optional parameter with default + default_val = param_info.get("default", "None") + if isinstance(default_val, str) and default_val not in ["None", "null"]: + default_val = f'"{default_val}"' + elif isinstance(default_val, bool): + default_val = str(default_val) + elif default_val is None or default_val == "null": + default_val = "None" + optional_params.append(f"{param_name}: Annotated[{py_type}, '{param_desc}'] = {default_val}") + + # Only add tool_call_id if it wasn't already in the schema + tool_call_id_found = any("tool_call_id" in param for param in optional_params) + if not tool_call_id_found: + optional_params.append("tool_call_id: Annotated[str, 'Unique identifier for this tool call'] = 'mcp_call'") + + # Combine required params first, then optional params + param_list = required_params + optional_params + + # Create the function code + params_str = ", ".join(param_list) + + # Create a list of parameter names for the function + param_names = [] + for param in param_list: + # Extract parameter name from the type annotation + param_name = param.split(":")[0].strip() + param_names.append(param_name) + + param_names_str = repr(param_names) + + func_code = f""" +def tool_function({params_str}) -> str: + '''Dynamically created tool function: {description}''' + # Collect all parameters by name to avoid circular references + param_names = {param_names_str} + arguments = {{}} + + # Get the current frame's local variables + import inspect + frame = inspect.currentframe() + try: + locals_dict = frame.f_locals + for param_name in param_names: + if param_name in locals_dict: + value = locals_dict[param_name] + # Handle None values and ensure JSON serializable + if value is not None: + arguments[param_name] = value + finally: + del frame + + # Execute the tool via API + result = execute_tool_via_api('{name}', arguments) + + # Return formatted result + return json.dumps(result, indent=2) +""" + + # Execute the function code to create the function + namespace = {"Annotated": Annotated, "json": json, "execute_tool_via_api": execute_tool_via_api, "inspect": inspect} + try: + exec(func_code, namespace) + func = namespace["tool_function"] + except SyntaxError as e: + print(f"❌ Syntax error in tool {name}:") + print(f"Error: {e}") + print("Generated code:") + for i, line in enumerate(func_code.split("\n"), 1): + print(f"{i:3}: {line}") + raise + + # Set metadata + func.__name__ = name.replace("-", "_") + func.__doc__ = description + + return func + + return create_dynamic_function() + + # Create the tool function + tool_func = make_tool_function(tool_name, tool_description, properties, required) + + # Register with FastMCP using the decorator + decorated_func = mcp.tool()(tool_func) + + print(f"✅ Registered dynamic tool: {tool_name}") diff --git a/src/codegen.backup/cli/mcp/tools/executor.py b/src/codegen.backup/cli/mcp/tools/executor.py new file mode 100644 index 000000000..e9113e8e2 --- /dev/null +++ b/src/codegen.backup/cli/mcp/tools/executor.py @@ -0,0 +1,40 @@ +import json +import requests + +from codegen.cli.api.endpoints import API_ENDPOINT + +import requests + +from codegen.cli.api.endpoints import API_ENDPOINT +from codegen.cli.auth.token_manager import get_current_token +from codegen.cli.utils.org import resolve_org_id + + +def execute_tool_via_api(tool_name: str, arguments: dict): + """Execute a tool via the Codegen API.""" + try: + token = get_current_token() + if not token: + return {"error": "Not authenticated. Please run 'codegen login' first."} + + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + + # Determine org id: prefer explicit in arguments, else resolve from env/config/API + org_id = None + if isinstance(arguments, dict): + org_id = arguments.get("org_id") + org_id = resolve_org_id(org_id) + if org_id is None: + return {"error": "Organization ID not provided. Include org_id argument, or set CODEGEN_ORG_ID/REPOSITORY_ORG_ID."} + + url = f"{API_ENDPOINT.rstrip('/')}/v1/organizations/{org_id}/tools/execute" + + payload = {"tool_name": tool_name, "arguments": arguments} + + response = requests.post(url, json=payload, headers=headers) + response.raise_for_status() + + return response.json() + + except Exception as e: + return {"error": f"Error executing tool {tool_name}: {e}"} diff --git a/src/codegen.backup/cli/mcp/tools/static.py b/src/codegen.backup/cli/mcp/tools/static.py new file mode 100644 index 000000000..98c9ec98c --- /dev/null +++ b/src/codegen.backup/cli/mcp/tools/static.py @@ -0,0 +1,176 @@ +"""Static Codegen API tools for the MCP server.""" + +import json +from typing import Annotated + +from fastmcp import Context, FastMCP + +from ..api_client import get_api_client + + +def register_static_tools(mcp: FastMCP): + """Register static Codegen API tools with the MCP server.""" + + @mcp.tool() + def create_agent_run( + org_id: Annotated[int, "Organization ID"], + prompt: Annotated[str, "The prompt/task for the agent to execute"], + repo_name: Annotated[str | None, "Repository name (optional)"] = None, + branch_name: Annotated[str | None, "Branch name (optional)"] = None, + ctx: Context | None = None, + ) -> str: + """Create a new agent run in the specified organization.""" + try: + from codegen_api_client.models import CreateAgentRunInput + + _, agents_api, _, _ = get_api_client() + + # Create the input object + agent_input = CreateAgentRunInput(prompt=prompt) + # Make the API call + response = agents_api.create_agent_run_v1_organizations_org_id_agent_run_post(org_id=org_id, create_agent_run_input=agent_input) + + return json.dumps( + { + "id": response.id, + "status": response.status, + "created_at": response.created_at.isoformat() if response.created_at else None, + "prompt": response.prompt, + "repo_name": response.repo_name, + "branch_name": response.branch_name, + }, + indent=2, + ) + + except Exception as e: + return f"Error creating agent run: {e}" + + @mcp.tool() + def get_agent_run( + org_id: Annotated[int, "Organization ID"], + agent_run_id: Annotated[int, "Agent run ID"], + ctx: Context | None = None, + ) -> str: + """Get details of a specific agent run.""" + try: + _, agents_api, _, _ = get_api_client() + + response = agents_api.get_agent_run_v1_organizations_org_id_agent_run_agent_run_id_get(org_id=org_id, agent_run_id=agent_run_id) + + return json.dumps( + { + "id": response.id, + "status": response.status, + "created_at": response.created_at.isoformat() if response.created_at else None, + "updated_at": response.updated_at.isoformat() if response.updated_at else None, + "prompt": response.prompt, + "repo_name": response.repo_name, + "branch_name": response.branch_name, + "result": response.result, + }, + indent=2, + ) + + except Exception as e: + return f"Error getting agent run: {e}" + + @mcp.tool() + def get_organizations( + page: Annotated[int, "Page number (default: 1)"] = 1, + limit: Annotated[int, "Number of organizations per page (default: 10)"] = 10, + ctx: Context | None = None, + ) -> str: + """Get list of organizations the user has access to.""" + try: + _, _, organizations_api, _ = get_api_client() + + response = organizations_api.get_organizations_v1_organizations_get() + + # Format the response + organizations = [] + for org in response.items: + organizations.append( + { + "id": org.id, + "name": org.name, + "slug": org.slug, + "created_at": org.created_at.isoformat() if org.created_at else None, + } + ) + + return json.dumps( + { + "organizations": organizations, + "total": response.total, + "page": response.page, + "limit": response.limit, + }, + indent=2, + ) + + except Exception as e: + return f"Error getting organizations: {e}" + + @mcp.tool() + def get_users( + org_id: Annotated[int, "Organization ID"], + page: Annotated[int, "Page number (default: 1)"] = 1, + limit: Annotated[int, "Number of users per page (default: 10)"] = 10, + ctx: Context | None = None, + ) -> str: + """Get list of users in an organization.""" + try: + _, _, _, users_api = get_api_client() + + response = users_api.get_users_v1_organizations_org_id_users_get(org_id=org_id) + + # Format the response + users = [] + for user in response.items: + users.append( + { + "id": user.id, + "email": user.email, + "name": user.name, + "created_at": user.created_at.isoformat() if user.created_at else None, + } + ) + + return json.dumps( + { + "users": users, + "total": response.total, + "page": response.page, + "limit": response.limit, + }, + indent=2, + ) + + except Exception as e: + return f"Error getting users: {e}" + + @mcp.tool() + def get_user( + org_id: Annotated[int, "Organization ID"], + user_id: Annotated[int, "User ID"], + ctx: Context | None = None, + ) -> str: + """Get details of a specific user in an organization.""" + try: + _, _, _, users_api = get_api_client() + + response = users_api.get_user_v1_organizations_org_id_users_user_id_get(org_id=org_id, user_id=user_id) + + return json.dumps( + { + "id": response.id, + "email": response.email, + "name": response.name, + "created_at": response.created_at.isoformat() if response.created_at else None, + "updated_at": response.updated_at.isoformat() if response.updated_at else None, + }, + indent=2, + ) + + except Exception as e: + return f"Error getting user: {e}" diff --git a/src/codegen.backup/cli/rich/codeblocks.py b/src/codegen.backup/cli/rich/codeblocks.py new file mode 100644 index 000000000..f9fa10d87 --- /dev/null +++ b/src/codegen.backup/cli/rich/codeblocks.py @@ -0,0 +1,42 @@ +def format_command(command: str) -> str: + """Format a command in a consistent style. + + Args: + command: The command to format + + Returns: + The formatted command with consistent styling and spacing + + """ + return f"\n\t[cyan]{command}[/cyan]\n" + + +def format_codeblock(code: str) -> str: + """Format a code block in a consistent style. + + Args: + code: The code to format + + Returns: + The formatted code with consistent styling + + """ + return f"\n\t[cyan]{code}[/cyan]\n" + + +def format_code(code: str) -> str: + """Just blue for a span""" + return f"[cyan]{code}[/cyan]" + + +def format_path(path: str) -> str: + """Format a path in a consistent style. + + Args: + path: The path to format + + Returns: + The formatted path with consistent styling + + """ + return f"[cyan]{path}[/cyan]" diff --git a/src/codegen.backup/cli/rich/pretty_print.py b/src/codegen.backup/cli/rich/pretty_print.py new file mode 100644 index 000000000..fd60ffa6e --- /dev/null +++ b/src/codegen.backup/cli/rich/pretty_print.py @@ -0,0 +1,66 @@ +import rich +from rich import box +from rich.markdown import Markdown +from rich.panel import Panel + +from codegen.cli.api.schemas import RunCodemodOutput + + +def pretty_print_output(output: RunCodemodOutput): + """Pretty print the codemod run output with panels.""" + if output.web_link: + rich.print("\n• [blue underline]" + output.web_link + "[/blue underline]\n") + + if output.logs: + pretty_print_logs(output.logs) + + if output.error: + pretty_print_error(output.error) + + if output.observation: + pretty_print_diff(output.observation) + + +def pretty_print_logs(logs: str): + """Pretty print logs in a panel.""" + rich.print( + Panel( + logs, + title="[bold blue]Logs", + border_style="blue", + box=box.ROUNDED, + padding=(1, 2), + ) + ) + rich.print() # spacing + + +def pretty_print_error(error: str): + """Pretty print error in a panel.""" + rich.print( + Panel( + error, + title="[bold red]Error", + border_style="red", + box=box.ROUNDED, + padding=(1, 2), + ) + ) + rich.print() # spacing + + +def pretty_print_diff(diff: str): + """Pretty print diff in a panel.""" + rich.print( + Panel( + Markdown( + f"""```diff\n{diff}\n```""", + code_theme="monokai", + ), + title="[bold green]Diff", + border_style="green", + box=box.ROUNDED, + padding=(1, 2), + ) + ) + rich.print() # spacing diff --git a/src/codegen.backup/cli/rich/spinners.py b/src/codegen.backup/cli/rich/spinners.py new file mode 100644 index 000000000..d61ea00b2 --- /dev/null +++ b/src/codegen.backup/cli/rich/spinners.py @@ -0,0 +1,29 @@ +"""Consistent spinner styles for the CLI.""" + +from dataclasses import dataclass + +from rich.status import Status + + +@dataclass +class SpinnerConfig: + """Configuration for a consistent spinner style.""" + + text: str + spinner: str = "dots" + style: str = "bold" + spinner_style: str = "blue" + + +def create_spinner(text: str) -> Status: + """Create a spinner with consistent styling. + + Args: + text: The text to show next to the spinner + + Returns: + A rich Status object with consistent styling + + """ + config = SpinnerConfig(text) + return Status(f"[{config.style}]{config.text}", spinner=config.spinner, spinner_style=config.spinner_style) diff --git a/src/codegen.backup/cli/telemetry/__init__.py b/src/codegen.backup/cli/telemetry/__init__.py new file mode 100644 index 000000000..9de772a57 --- /dev/null +++ b/src/codegen.backup/cli/telemetry/__init__.py @@ -0,0 +1,17 @@ +"""CLI telemetry module for analytics and observability.""" + +from codegen.cli.telemetry.consent import ( + ensure_telemetry_consent, + update_telemetry_consent, +) +from codegen.cli.telemetry.exception_logger import ( + setup_global_exception_logging, + teardown_global_exception_logging, +) + +__all__ = [ + "ensure_telemetry_consent", + "setup_global_exception_logging", + "teardown_global_exception_logging", + "update_telemetry_consent", +] diff --git a/src/codegen.backup/cli/telemetry/consent.py b/src/codegen.backup/cli/telemetry/consent.py new file mode 100644 index 000000000..f6bf6b6d9 --- /dev/null +++ b/src/codegen.backup/cli/telemetry/consent.py @@ -0,0 +1,105 @@ +"""Telemetry consent management for the CLI.""" + +import uuid +from pathlib import Path + +import rich +import typer + +from codegen.configs.constants import GLOBAL_ENV_FILE +from codegen.configs.models.telemetry import TelemetryConfig + + +def prompt_telemetry_consent() -> bool: + """Prompt user for telemetry consent during first-time setup. + + Returns: + bool: True if user consents to telemetry, False otherwise + """ + # Display Codegen header + print("\033[38;2;82;19;217m" + "/" * 20 + " Codegen\033[0m") + print() + + rich.print("[bold]📊 Help Improve Codegen CLI[/bold]") + rich.print( + "We'd like to collect anonymous usage data to improve the CLI experience.\n" + "This includes:\n" + " • Command usage patterns\n" + " • Performance metrics\n" + " • Error diagnostics (no source code or PII)\n" + " • CLI version and platform info\n" + ) + rich.print("[dim]You can change this setting anytime with 'codegen config telemetry'[/dim]\n") + + consent = typer.confirm("Enable anonymous telemetry?", default=False) + return consent + + +def ensure_telemetry_consent() -> TelemetryConfig: + """Ensure telemetry consent has been obtained and configured. + + This function: + 1. Loads existing telemetry config + 2. If not previously prompted, asks for consent + 3. Saves the configuration + + Returns: + TelemetryConfig: The telemetry configuration + """ + # Load telemetry config (uses global config file) + telemetry = TelemetryConfig(env_filepath=GLOBAL_ENV_FILE) + + # If already prompted, return existing config + if telemetry.consent_prompted: + return telemetry + + # Prompt for consent + consent = prompt_telemetry_consent() + + # Update configuration + telemetry.enabled = consent + telemetry.consent_prompted = True + + if consent: + rich.print("[green]✓ Telemetry enabled. Thank you for helping improve Codegen![/green]") + else: + rich.print("[yellow]✓ Telemetry disabled. You can enable it later with 'codegen config telemetry'[/yellow]") + + # Save to global config + telemetry.write_to_file(GLOBAL_ENV_FILE) + + # Refresh logging configuration to apply the new settings + try: + from codegen.shared.logging.get_logger import refresh_telemetry_config + + refresh_telemetry_config() + except ImportError: + pass # Logging refresh not available + + return telemetry + + +def update_telemetry_consent(enabled: bool) -> None: + """Update telemetry consent preference. + + Args: + enabled: Whether to enable telemetry + """ + telemetry = TelemetryConfig(env_filepath=GLOBAL_ENV_FILE) + telemetry.enabled = enabled + telemetry.consent_prompted = True + + telemetry.write_to_file(GLOBAL_ENV_FILE) + + # Refresh logging configuration to apply the new settings + try: + from codegen.shared.logging.get_logger import refresh_telemetry_config + + refresh_telemetry_config() + except ImportError: + pass # Logging refresh not available + + if enabled: + rich.print("[green]✓ Telemetry enabled[/green]") + else: + rich.print("[yellow]✓ Telemetry disabled[/yellow]") diff --git a/src/codegen.backup/cli/telemetry/debug_exporter.py b/src/codegen.backup/cli/telemetry/debug_exporter.py new file mode 100644 index 000000000..ec0e46742 --- /dev/null +++ b/src/codegen.backup/cli/telemetry/debug_exporter.py @@ -0,0 +1,166 @@ +"""Debug exporter for OpenTelemetry that writes spans to local files. + +This module provides a debug exporter that writes telemetry data to disk +for easy inspection and debugging of CLI telemetry. +""" + +import json +import os +from collections.abc import Sequence +from datetime import datetime +from pathlib import Path + +from opentelemetry.sdk.trace import ReadableSpan +from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult +from opentelemetry.trace import format_span_id, format_trace_id + +from codegen.configs.constants import GLOBAL_CONFIG_DIR + + +class DebugFileSpanExporter(SpanExporter): + """Exports spans to JSON files for debugging.""" + + def __init__(self, output_dir: Path | None = None): + """Initialize the debug exporter. + + Args: + output_dir: Directory to write debug files. Defaults to ~/.config/codegen-sh/telemetry_debug + """ + if output_dir is None: + output_dir = GLOBAL_CONFIG_DIR / "telemetry_debug" + + self.output_dir = Path(output_dir) + self.output_dir.mkdir(parents=True, exist_ok=True) + + # Create a session file for this CLI run + self.session_id = datetime.now().strftime("%Y%m%d_%H%M%S") + self.session_file = self.output_dir / f"session_{self.session_id}.jsonl" + + # Write session header + with open(self.session_file, "w") as f: + f.write( + json.dumps( + { + "type": "session_start", + "timestamp": datetime.now().isoformat(), + "pid": os.getpid(), + } + ) + + "\n" + ) + + def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: + """Export spans to file. + + Args: + spans: Spans to export + + Returns: + Export result status + """ + try: + with open(self.session_file, "a") as f: + for span in spans: + # Convert span to JSON-serializable format + span_data = { + "type": "span", + "name": span.name, + "trace_id": format_trace_id(span.context.trace_id), + "span_id": format_span_id(span.context.span_id), + "parent_span_id": format_span_id(span.parent.span_id) if span.parent else None, + "start_time": span.start_time, + "end_time": span.end_time, + "duration_ms": (span.end_time - span.start_time) / 1_000_000 if span.end_time else None, + "status": { + "status_code": span.status.status_code.name, + "description": span.status.description, + }, + "attributes": dict(span.attributes or {}), + "events": [ + { + "name": event.name, + "timestamp": event.timestamp, + "attributes": dict(event.attributes or {}), + } + for event in span.events + ], + "resource": dict(span.resource.attributes), + } + + # Handle exceptions + if span.status.status_code.name == "ERROR" and span.events: + for event in span.events: + if event.name == "exception": + span_data["exception"] = dict(event.attributes or {}) + + f.write(json.dumps(span_data, default=str) + "\n") + + return SpanExportResult.SUCCESS + + except Exception as e: + print(f"[Telemetry Debug] Failed to write spans: {e}") + return SpanExportResult.FAILURE + + def shutdown(self) -> None: + """Shutdown the exporter.""" + # Write session end marker + try: + with open(self.session_file, "a") as f: + f.write( + json.dumps( + { + "type": "session_end", + "timestamp": datetime.now().isoformat(), + } + ) + + "\n" + ) + except Exception: + pass + + +class DebugConsoleSpanExporter(SpanExporter): + """Exports spans to console for debugging.""" + + def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: + """Export spans to console. + + Args: + spans: Spans to export + + Returns: + Export result status + """ + try: + for span in spans: + duration_ms = (span.end_time - span.start_time) / 1_000_000 if span.end_time else 0 + + print(f"\n[Telemetry] {span.name}") + print(f" Duration: {duration_ms:.2f}ms") + print(f" Status: {span.status.status_code.name}") + + if span.attributes: + print(" Attributes:") + for key, value in span.attributes.items(): + print(f" {key}: {value}") + + if span.events: + print(" Events:") + for event in span.events: + print(f" - {event.name}") + if event.attributes: + for key, value in event.attributes.items(): + print(f" {key}: {value}") + + if span.status.description: + print(f" Error: {span.status.description}") + + return SpanExportResult.SUCCESS + + except Exception as e: + print(f"[Telemetry Debug] Console export failed: {e}") + return SpanExportResult.FAILURE + + def shutdown(self) -> None: + """Shutdown the exporter.""" + pass diff --git a/src/codegen.backup/cli/telemetry/exception_logger.py b/src/codegen.backup/cli/telemetry/exception_logger.py new file mode 100644 index 000000000..72ecc2a15 --- /dev/null +++ b/src/codegen.backup/cli/telemetry/exception_logger.py @@ -0,0 +1,176 @@ +"""Global exception logging for CLI telemetry. + +This module provides a global exception handler that captures unhandled exceptions +and logs them through the existing OpenTelemetry telemetry system. +""" + +import sys +import traceback +from typing import Any + +from codegen.shared.logging.get_logger import get_logger +from codegen.cli.telemetry.otel_setup import get_session_uuid, get_otel_logging_handler +from codegen.cli.telemetry.consent import ensure_telemetry_consent + +# Initialize logger for exception handling +logger = get_logger(__name__) + +# Store the original excepthook to allow chaining +_original_excepthook = sys.excepthook + + +def _get_exception_context(exc_type: type[BaseException], exc_value: BaseException, tb: Any) -> dict[str, Any]: + """Extract relevant context from an exception for logging. + + Args: + exc_type: The exception type + exc_value: The exception instance + tb: The traceback object + + Returns: + Dictionary with exception context for structured logging + """ + context = { + "operation": "cli.unhandled_exception", + "exception_type": exc_type.__name__, + "exception_message": str(exc_value), + "session_id": get_session_uuid(), + } + + # Add module and function information from the traceback + if tb is not None: + # Get the last frame (where the exception occurred) + last_frame = tb + while last_frame.tb_next is not None: + last_frame = last_frame.tb_next + + frame = last_frame.tb_frame + context.update( + { + "exception_file": frame.f_code.co_filename, + "exception_function": frame.f_code.co_name, + "exception_line": last_frame.tb_lineno, + } + ) + + # Get the full stack trace as a string + context["stack_trace"] = "".join(traceback.format_exception(exc_type, exc_value, tb)) + + # Add command context if available from CLI args + try: + # Try to extract command information from sys.argv + if len(sys.argv) > 1: + context["cli_command"] = sys.argv[1] + context["cli_args"] = sys.argv[2:] if len(sys.argv) > 2 else [] + except Exception: + # Don't let context extraction break exception logging + pass + + return context + + +def global_exception_handler(exc_type: type[BaseException], exc_value: BaseException, tb: Any) -> None: + """Global exception handler that logs unhandled exceptions. + + This function is designed to be set as sys.excepthook to capture all unhandled + exceptions in the CLI and log them through the telemetry system. + + Args: + exc_type: The exception type + exc_value: The exception instance + tb: The traceback object + """ + # Skip logging for KeyboardInterrupt (Ctrl+C) - this is expected user behavior + if issubclass(exc_type, KeyboardInterrupt): + # Call the original excepthook for normal handling + _original_excepthook(exc_type, exc_value, tb) + return + + # Skip logging for SystemExit with code 0 (normal exit) + if issubclass(exc_type, SystemExit) and getattr(exc_value, "code", None) == 0: + _original_excepthook(exc_type, exc_value, tb) + return + + try: + # Check telemetry configuration to determine logging behavior + telemetry_config = ensure_telemetry_consent() + + # Extract context for structured logging + context = _get_exception_context(exc_type, exc_value, tb) + + # Always send to telemetry backend if enabled (regardless of debug mode) + if telemetry_config.enabled: + # Get the OpenTelemetry handler for backend logging + otel_handler = get_otel_logging_handler() + if otel_handler: + # Create a separate logger that only sends to OTEL backend + import logging + + telemetry_logger = logging.getLogger("codegen.telemetry.exceptions") + telemetry_logger.setLevel(logging.ERROR) + + # Remove any existing handlers to avoid console output + telemetry_logger.handlers.clear() + telemetry_logger.addHandler(otel_handler) + telemetry_logger.propagate = False # Don't propagate to parent loggers + + # Log to telemetry backend only + telemetry_logger.error(f"Unhandled CLI exception: {exc_type.__name__}: {exc_value}", extra=context, exc_info=(exc_type, exc_value, tb)) + + # Only log to console if debug mode is enabled + if telemetry_config.debug: + logger.error(f"Unhandled CLI exception: {exc_type.__name__}: {exc_value}", extra=context, exc_info=(exc_type, exc_value, tb)) + logger.debug("Exception details logged for telemetry", extra={"operation": "cli.exception_logging", "session_id": get_session_uuid()}) + + except Exception as logging_error: + # If logging itself fails, at least print to stderr in debug mode or if telemetry is disabled + try: + telemetry_config = ensure_telemetry_consent() + if telemetry_config.debug or not telemetry_config.enabled: + print(f"Failed to log exception: {logging_error}", file=sys.stderr) + print(f"Original exception: {exc_type.__name__}: {exc_value}", file=sys.stderr) + except Exception: + # If even the telemetry config check fails, always print to stderr + print(f"Failed to log exception: {logging_error}", file=sys.stderr) + print(f"Original exception: {exc_type.__name__}: {exc_value}", file=sys.stderr) + + # Always call the original excepthook to preserve normal error handling behavior + _original_excepthook(exc_type, exc_value, tb) + + +def setup_global_exception_logging() -> None: + """Set up global exception logging by installing the custom excepthook. + + This should be called early in the CLI initialization to ensure all unhandled + exceptions are captured and logged. + """ + # Only install if not already installed (avoid double installation) + if sys.excepthook != global_exception_handler: + sys.excepthook = global_exception_handler + + # Only log setup message to console if debug mode is enabled + try: + telemetry_config = ensure_telemetry_consent() + if telemetry_config.debug: + logger.debug("Global exception logging enabled", extra={"operation": "cli.exception_logging_setup", "session_id": get_session_uuid()}) + except Exception: + # If we can't check telemetry config, silently continue + pass + + +def teardown_global_exception_logging() -> None: + """Restore the original exception handler. + + This can be called during cleanup to restore normal exception handling. + """ + if sys.excepthook == global_exception_handler: + sys.excepthook = _original_excepthook + + # Only log teardown message to console if debug mode is enabled + try: + telemetry_config = ensure_telemetry_consent() + if telemetry_config.debug: + logger.debug("Global exception logging disabled", extra={"operation": "cli.exception_logging_teardown", "session_id": get_session_uuid()}) + except Exception: + # If we can't check telemetry config, silently continue + pass diff --git a/src/codegen.backup/cli/telemetry/otel_setup.py b/src/codegen.backup/cli/telemetry/otel_setup.py new file mode 100644 index 000000000..e3acd934d --- /dev/null +++ b/src/codegen.backup/cli/telemetry/otel_setup.py @@ -0,0 +1,300 @@ +"""Simple OpenTelemetry logging setup for CLI telemetry. + +This module provides a clean, minimal setup for sending CLI logs to the +OTLP collector when telemetry is enabled by the user. +""" + +import logging +import os +import platform +import subprocess +import sys +import uuid +from typing import Any + +from opentelemetry import _logs as logs +from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter +from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler +from opentelemetry.sdk._logs.export import BatchLogRecordProcessor +from opentelemetry.sdk.resources import Resource + +from codegen import __version__ +from codegen.cli.api.modal import get_modal_prefix +from codegen.cli.auth.token_manager import TokenManager +from codegen.cli.env.enums import Environment +from codegen.cli.env.global_env import global_env +from codegen.cli.utils.org import resolve_org_id +from codegen.configs.models.telemetry import TelemetryConfig + +# Global logger provider instance +_logger_provider: LoggerProvider | None = None + +# Global session UUID for this CLI invocation +_session_uuid: str = str(uuid.uuid4()) + + +def _get_otlp_logs_endpoint() -> tuple[str, dict[str, str]]: + """Get the OTLP logs endpoint and headers based on environment. + + This replicates the backend logic for determining the correct collector endpoint + based on whether we're running in Kubernetes or Modal environment. + + Returns: + Tuple of (endpoint_url, headers_dict) + """ + # Check if we're running in Kubernetes by looking for K8S_POD_NAME env var + k8s_pod_name = os.environ.get("K8S_POD_NAME") + if k8s_pod_name: + # Running in Kubernetes - use Grafana Alloy + return "http://grafana-monitoring-staging-alloy-receiver.monitoring.svc.cluster.local:4318/v1/logs", {} + + # Running in Modal - use Modal OTEL collector + modal_prefix = get_modal_prefix() + suffix = "otel-collector.modal.run" + + if global_env.ENV == Environment.PRODUCTION: + collector_endpoint = f"https://{modal_prefix}--{suffix}/cli/v1/logs" + elif global_env.ENV == Environment.STAGING: + collector_endpoint = f"https://{modal_prefix}--{suffix}/cli/v1/logs" + else: # DEVELOPMENT + collector_endpoint = f"https://{modal_prefix}--{suffix}/cli/v1/logs" + + # Create basic auth header for Modal collector + token_manager = TokenManager() + token = token_manager.get_token() + if not token: + # Return empty headers if no auth configured + return collector_endpoint, {} + + return collector_endpoint, {"Authorization": f"Bearer {token}"} + + +def _get_claude_info() -> dict[str, str]: + """Get Claude Code path and version information quickly.""" + claude_info = {} + + try: + # Use the same logic as the Claude command to find the CLI + # Import here to avoid circular imports + try: + from codegen.cli.commands.claude.utils import resolve_claude_path + + claude_path = resolve_claude_path() + except ImportError: + # Fallback to basic path detection if utils not available + claude_path = None + + # Quick check in PATH first + import shutil + + claude_path = shutil.which("claude") + + # If not found, check common local paths + if not claude_path: + local_path = os.path.expanduser("~/.claude/local/claude") + if os.path.isfile(local_path) and os.access(local_path, os.X_OK): + claude_path = local_path + + if claude_path: + claude_info["claude.path"] = claude_path + + # Only get version if we found the path - use short timeout + try: + version_result = subprocess.run( + [claude_path, "--version"], + capture_output=True, + text=True, + timeout=3, # Short timeout for telemetry setup + ) + if version_result.returncode == 0: + version_output = version_result.stdout.strip() + claude_info["claude.version"] = version_output if version_output else "unknown" + else: + claude_info["claude.version"] = "check_failed" + except (subprocess.TimeoutExpired, Exception): + claude_info["claude.version"] = "check_timeout" + else: + claude_info["claude.available"] = "false" + + except Exception: + # If anything fails, mark as error but don't break telemetry setup + claude_info["claude.available"] = "detection_error" + + return claude_info + + +def _create_cli_resource(telemetry_config: TelemetryConfig) -> Resource: + """Create OpenTelemetry resource with CLI-specific attributes.""" + global _session_uuid + + # Base service attributes + resource_attributes: dict[str, Any] = { + "service.name": "codegen-cli", + "service.version": __version__, + "session.id": _session_uuid, # Unique UUID for this CLI invocation + "os.type": platform.system().lower(), + "os.version": platform.version(), + "python.version": sys.version.split()[0], + } + + # Add user context if logged in + try: + # Try to get the current user ID (if authenticated) + auth_data = TokenManager().get_auth_data() + if auth_data: + user = auth_data.get("user") + if user: + resource_attributes["user.id"] = str(user.get("id")) + + organization = auth_data.get("organization") + if organization: + resource_attributes["organization.id"] = str(organization.get("id")) + resource_attributes["cli_session_id"] = _session_uuid + + except Exception: + # If user ID lookup fails, continue without it + pass + + # Add organization context if available + try: + org_id = resolve_org_id() + if org_id: + resource_attributes["org.id"] = str(org_id) + except Exception: + # If org ID lookup fails, continue without it + pass + + # Add environment context + if os.environ.get("CI"): + resource_attributes["deployment.environment"] = "ci" + elif os.environ.get("CODESPACES"): + resource_attributes["deployment.environment"] = "codespaces" + elif os.environ.get("GITPOD_WORKSPACE_ID"): + resource_attributes["deployment.environment"] = "gitpod" + else: + resource_attributes["deployment.environment"] = "local" + + # Add Claude Code information + claude_info = _get_claude_info() + resource_attributes.update(claude_info) + + return Resource.create(resource_attributes) + + +def setup_otel_logging() -> LoggerProvider | None: + """Set up OpenTelemetry logging if telemetry is enabled. + + Returns: + LoggerProvider if telemetry is enabled and setup succeeds, None otherwise + """ + global _logger_provider + + # Return cached provider if already set up + if _logger_provider is not None: + return _logger_provider + + # Ensure telemetry consent and load configuration + from codegen.cli.telemetry.consent import ensure_telemetry_consent + + telemetry_config = ensure_telemetry_consent() + + # Only set up if explicitly enabled + if not telemetry_config.enabled: + return None + + try: + # Create resource with CLI metadata + resource = _create_cli_resource(telemetry_config) + + # Create logger provider + logger_provider = LoggerProvider(resource=resource) + + # Get OTLP endpoint and headers + endpoint, headers = _get_otlp_logs_endpoint() + + # Create OTLP log exporter + log_exporter = OTLPLogExporter( + endpoint=endpoint, + headers=headers, + timeout=10, # 10 second timeout + ) + + # Create batch processor for performance + log_processor = BatchLogRecordProcessor( + log_exporter, + max_queue_size=1024, + max_export_batch_size=256, + export_timeout_millis=10000, # 10 seconds + schedule_delay_millis=2000, # Export every 2 seconds + ) + + logger_provider.add_log_record_processor(log_processor) + + # Set as global provider + logs.set_logger_provider(logger_provider) + _logger_provider = logger_provider + + # Debug output if enabled + if telemetry_config.debug: + print(f"[Telemetry] Logging initialized with endpoint: {endpoint}") + print(f"[Telemetry] Session UUID: {_session_uuid}") + # Show key resource attributes + resource_attrs = resource.attributes + if "user.id" in resource_attrs: + print(f"[Telemetry] User ID: {resource_attrs['user.id']}") + if "org.id" in resource_attrs: + print(f"[Telemetry] Org ID: {resource_attrs['org.id']}") + if "claude.path" in resource_attrs: + print(f"[Telemetry] Claude Path: {resource_attrs['claude.path']}") + if "claude.version" in resource_attrs: + print(f"[Telemetry] Claude Version: {resource_attrs['claude.version']}") + elif "claude.available" in resource_attrs: + print(f"[Telemetry] Claude Available: {resource_attrs['claude.available']}") + + return logger_provider + + except Exception as e: + if telemetry_config.debug: + print(f"[Telemetry] Failed to initialize logging: {e}") + return None + + +def get_otel_logging_handler() -> logging.Handler | None: + """Get an OpenTelemetry logging handler. + + This handler will send logs to the OTLP collector when telemetry is enabled. + + Returns: + LoggingHandler if telemetry is enabled, None otherwise + """ + logger_provider = setup_otel_logging() + if logger_provider is None: + return None + + # Create handler that bridges Python logging to OpenTelemetry + handler = LoggingHandler(level=logging.NOTSET, logger_provider=logger_provider) + return handler + + +def get_session_uuid() -> str: + """Get the session UUID for this CLI invocation. + + Returns: + The session UUID string that uniquely identifies this CLI run + """ + global _session_uuid + return _session_uuid + + +def shutdown_otel_logging(): + """Gracefully shutdown OpenTelemetry logging and flush pending data.""" + global _logger_provider + + if _logger_provider is not None: + try: + # Type checker workaround: assert that provider is not None after the check + assert _logger_provider is not None + _logger_provider.shutdown() + except Exception: + pass # Ignore shutdown errors + _logger_provider = None diff --git a/src/codegen.backup/cli/telemetry/viewer.py b/src/codegen.backup/cli/telemetry/viewer.py new file mode 100644 index 000000000..c9e229bb8 --- /dev/null +++ b/src/codegen.backup/cli/telemetry/viewer.py @@ -0,0 +1,129 @@ +"""Simple telemetry log viewer for debugging. + +This script provides utilities for analyzing telemetry debug logs. +""" + +import json +from pathlib import Path +from typing import Any + +from rich.console import Console +from rich.tree import Tree + +from codegen.configs.constants import GLOBAL_CONFIG_DIR + + +def load_session(session_file: Path) -> list[dict[str, Any]]: + """Load all records from a session file.""" + records = [] + with open(session_file) as f: + for line in f: + records.append(json.loads(line)) + return records + + +def print_span_tree(spans: list[dict[str, Any]], console: Console): + """Print spans as a tree structure.""" + # Build parent-child relationships + span_by_id = {span["span_id"]: span for span in spans} + root_spans = [] + + for span in spans: + if not span.get("parent_span_id") or span["parent_span_id"] not in span_by_id: + root_spans.append(span) + + # Create tree + tree = Tree("Telemetry Trace Tree") + + def add_span_to_tree(span: dict[str, Any], parent_node): + """Recursively add span and its children to tree.""" + duration = span.get("duration_ms", 0) + status = span["status"]["status_code"] + status_icon = "✅" if status == "OK" else "❌" + + label = f"{status_icon} {span['name']} ({duration:.2f}ms)" + node = parent_node.add(label) + + # Add key attributes + attrs = span.get("attributes", {}) + for key, value in attrs.items(): + if key.startswith("cli.") or key.startswith("event."): + node.add(f"[dim]{key}: {value}[/dim]") + + # Find children + for other_span in spans: + if other_span.get("parent_span_id") == span["span_id"]: + add_span_to_tree(other_span, node) + + # Add root spans + for root_span in root_spans: + add_span_to_tree(root_span, tree) + + console.print(tree) + + +def analyze_session(session_file: Path): + """Analyze a telemetry session file.""" + console = Console() + + console.print(f"\n[bold]Analyzing session:[/bold] {session_file.name}\n") + + records = load_session(session_file) + spans = [r for r in records if r["type"] == "span"] + + if not spans: + console.print("[yellow]No spans found in session[/yellow]") + return + + # Basic stats + total_duration = sum(s.get("duration_ms", 0) for s in spans) + error_count = sum(1 for s in spans if s["status"]["status_code"] == "ERROR") + + console.print(f"[cyan]Total spans:[/cyan] {len(spans)}") + console.print(f"[cyan]Total duration:[/cyan] {total_duration:.2f}ms") + console.print(f"[cyan]Errors:[/cyan] {error_count}") + console.print() + + # Show errors if any + if error_count > 0: + console.print("[bold red]Errors:[/bold red]") + for span in spans: + if span["status"]["status_code"] == "ERROR": + console.print(f" - {span['name']}: {span['status'].get('description', 'Unknown error')}") + console.print() + + # Show span tree + print_span_tree(spans, console) + + # Show slowest operations + console.print("\n[bold]Slowest Operations:[/bold]") + sorted_spans = sorted(spans, key=lambda s: s.get("duration_ms", 0), reverse=True) + for span in sorted_spans[:5]: + duration = span.get("duration_ms", 0) + console.print(f" - {span['name']}: {duration:.2f}ms") + + +def latest_session() -> Path | None: + """Get the latest session file.""" + debug_dir = GLOBAL_CONFIG_DIR / "telemetry_debug" + if not debug_dir.exists(): + return None + + session_files = sorted(debug_dir.glob("session_*.jsonl"), reverse=True) + return session_files[0] if session_files else None + + +if __name__ == "__main__": + # Simple CLI for viewing logs + import sys + + if len(sys.argv) > 1: + session_file = Path(sys.argv[1]) + else: + session_file = latest_session() + + if session_file and session_file.exists(): + analyze_session(session_file) + else: + print("No session file found. Run with debug enabled first.") + print("Usage: python -m codegen.cli.telemetry.viewer [session_file.jsonl]") diff --git a/src/codegen.backup/cli/tui/__init__.py b/src/codegen.backup/cli/tui/__init__.py new file mode 100644 index 000000000..70f88a592 --- /dev/null +++ b/src/codegen.backup/cli/tui/__init__.py @@ -0,0 +1 @@ +"""TUI (Terminal User Interface) module for Codegen CLI.""" diff --git a/src/codegen.backup/cli/tui/agent_detail.py b/src/codegen.backup/cli/tui/agent_detail.py new file mode 100644 index 000000000..d1ddd931b --- /dev/null +++ b/src/codegen.backup/cli/tui/agent_detail.py @@ -0,0 +1,309 @@ +"""Agent Detail TUI screen for viewing individual agent runs.""" + +import asyncio +import json +from pathlib import Path +from typing import Any, Dict + +import requests +from textual.app import ComposeResult +from textual.binding import Binding +from textual.containers import Container, Vertical, Horizontal +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Static, DataTable + +from codegen.cli.auth.token_manager import get_current_token +from codegen.cli.utils.org import resolve_org_id +from codegen.cli.api.endpoints import API_ENDPOINT +from codegen.git.repo_operator.local_git_repo import LocalGitRepo + + +class AgentDetailTUI(Screen): + """TUI screen for viewing agent run details and performing actions.""" + + CSS_PATH = "codegen_theme.tcss" + BINDINGS = [ + Binding("escape,q", "back", "Back", show=True), + Binding("j", "view_json", "View JSON", show=True), + Binding("p", "pull_branch", "Pull Branch", show=True), + Binding("w", "open_web", "Open Web", show=True), + ] + + def __init__(self, agent_run: Dict[str, Any], org_id: int | None = None): + super().__init__() + self.agent_run = agent_run + self.org_id = org_id or resolve_org_id() + self.agent_data: Dict[str, Any] | None = None + self.is_loading = False + + def compose(self) -> ComposeResult: + """Create child widgets for the agent detail screen.""" + run_id = self.agent_run.get("id", "Unknown") + summary = self.agent_run.get("summary", "No summary available") + + yield Header() + + with Vertical(): + yield Static(f"🤖 Agent Run Details - ID: {run_id}", classes="title", id="detail-title") + yield Static("Use J for JSON, P to pull branch, W for web, Q/Esc to go back", classes="help") + + # Basic info section + info_table = DataTable(id="info-table", cursor_type="none") + info_table.add_columns("Property", "Value") + yield info_table + + # Actions section + with Horizontal(id="actions-section"): + yield Button("📄 View JSON", id="json-btn", variant="primary") + yield Button("🔀 Pull Branch", id="pull-btn", variant="default") + yield Button("🌐 Open Web", id="web-btn", variant="default") + yield Button("⬅️ Back", id="back-btn", variant="default") + + # Status/loading area + yield Static("", id="status-text") + + yield Footer() + + def on_mount(self) -> None: + """Called when the screen is mounted.""" + self._populate_basic_info() + # Load detailed data in background + task = asyncio.create_task(self._load_detailed_data()) + self._load_task = task + + def _populate_basic_info(self) -> None: + """Populate the info table with basic agent run information.""" + info_table = self.query_one("#info-table", DataTable) + + # Basic info from the agent run data + run_id = self.agent_run.get("id", "Unknown") + status = self.agent_run.get("status", "Unknown") + created_at = self.agent_run.get("created_at", "Unknown") + summary = self.agent_run.get("summary", "No summary available") + web_url = self.agent_run.get("web_url", "Not available") + + # Format status with emoji + status_display = status + if status == "COMPLETE": + status_display = "✅ Complete" + elif status == "RUNNING": + status_display = "🏃 Running" + elif status == "FAILED": + status_display = "❌ Failed" + elif status == "STOPPED": + status_display = "⏹️ Stopped" + elif status == "PENDING": + status_display = "⏳ Pending" + + # Add rows to info table + info_table.add_row("ID", str(run_id)) + info_table.add_row("Status", status_display) + info_table.add_row("Created", created_at) + info_table.add_row("Summary", summary) + info_table.add_row("Web URL", web_url) + + async def _load_detailed_data(self) -> None: + """Load detailed agent run data from the API.""" + if self.is_loading: + return + + self.is_loading = True + status_text = self.query_one("#status-text", Static) + status_text.update("🔄 Loading detailed agent data...") + + try: + token = get_current_token() + if not token: + status_text.update("❌ Not authenticated") + return + + run_id = self.agent_run.get("id") + if not run_id: + status_text.update("❌ No agent run ID available") + return + + headers = {"Authorization": f"Bearer {token}"} + url = f"{API_ENDPOINT.rstrip('/')}/v1/organizations/{self.org_id}/agent/run/{run_id}" + + response = requests.get(url, headers=headers) + response.raise_for_status() + self.agent_data = response.json() + + # Update info table with additional details + self._update_info_with_detailed_data() + status_text.update("✅ Agent data loaded successfully") + + except requests.HTTPError as e: + if e.response.status_code == 404: + status_text.update(f"❌ Agent run {run_id} not found") + elif e.response.status_code == 403: + status_text.update(f"❌ Access denied to agent run {run_id}") + else: + status_text.update(f"❌ HTTP {e.response.status_code}: {e}") + except Exception as e: + status_text.update(f"❌ Error loading data: {e}") + finally: + self.is_loading = False + + def _update_info_with_detailed_data(self) -> None: + """Update the info table with detailed data from the API.""" + if not self.agent_data: + return + + info_table = self.query_one("#info-table", DataTable) + + # Check for GitHub PRs + github_prs = self.agent_data.get("github_pull_requests", []) + if github_prs: + pr_info = f"{len(github_prs)} PR(s) available" + for i, pr in enumerate(github_prs[:3]): # Show up to 3 PRs + branch = pr.get("head", {}).get("ref", "unknown") + pr_info += f"\n • {branch}" + if len(github_prs) > 3: + pr_info += f"\n • ... and {len(github_prs) - 3} more" + else: + pr_info = "No PRs available" + + info_table.add_row("PR Branches", pr_info) + + # Add model info if available + model = self.agent_data.get("model", "Unknown") + info_table.add_row("Model", model) + + # Action handlers + def action_back(self) -> None: + """Go back to the main screen.""" + self.app.pop_screen() + + def action_view_json(self) -> None: + """View the full JSON data for the agent run.""" + if not self.agent_data: + self.notify("❌ Detailed data not loaded yet", severity="error") + return + + # Create a JSON viewer screen + json_screen = JSONViewerTUI(self.agent_data) + self.app.push_screen(json_screen) + + def action_pull_branch(self) -> None: + """Pull the PR branch for this agent run.""" + if not self.agent_data: + self.notify("❌ Detailed data not loaded yet", severity="error") + return + + # Check if we're in a git repository + try: + current_repo = LocalGitRepo(Path.cwd()) + if not current_repo.has_remote(): + self.notify("❌ Not in a git repository with remotes", severity="error") + return + except Exception: + self.notify("❌ Not in a valid git repository", severity="error") + return + + # Check for GitHub PRs + github_prs = self.agent_data.get("github_pull_requests", []) + if not github_prs: + self.notify("❌ No PR branches available for this agent run", severity="error") + return + + # For now, take the first PR - in the future we could show a selector + pr = github_prs[0] + branch_name = pr.get("head", {}).get("ref") + repo_clone_url = pr.get("head", {}).get("repo", {}).get("clone_url") + + if not branch_name or not repo_clone_url: + self.notify("❌ Invalid PR data", severity="error") + return + + # Start the pull process + task = asyncio.create_task(self._pull_branch_async(branch_name, repo_clone_url)) + self._pull_task = task + + async def _pull_branch_async(self, branch_name: str, repo_clone_url: str) -> None: + """Asynchronously pull the PR branch.""" + status_text = self.query_one("#status-text", Static) + status_text.update(f"🔄 Pulling branch {branch_name}...") + + try: + current_repo = LocalGitRepo(Path.cwd()) + + # Add remote if it doesn't exist + remote_name = "codegen-pr" + try: + current_repo.add_remote(remote_name, repo_clone_url) + except Exception: + # Remote might already exist + pass + + # Fetch and checkout the branch + current_repo.fetch_remote(remote_name) + current_repo.checkout_branch(f"{remote_name}/{branch_name}", branch_name) + + status_text.update(f"✅ Successfully checked out branch: {branch_name}") + self.notify(f"✅ Switched to branch: {branch_name}") + + except Exception as e: + error_msg = f"❌ Failed to pull branch: {e}" + status_text.update(error_msg) + self.notify(error_msg, severity="error") + + def action_open_web(self) -> None: + """Open the agent run in the web browser.""" + web_url = self.agent_run.get("web_url") + if not web_url: + run_id = self.agent_run.get("id") + web_url = f"https://codegen.com/traces/{run_id}" + + try: + import webbrowser + webbrowser.open(web_url) + self.notify(f"🌐 Opened {web_url}") + except Exception as e: + self.notify(f"❌ Failed to open URL: {e}", severity="error") + + # Button event handlers + def on_button_pressed(self, event: Button.Pressed) -> None: + """Handle button press events.""" + if event.button.id == "json-btn": + self.action_view_json() + elif event.button.id == "pull-btn": + self.action_pull_branch() + elif event.button.id == "web-btn": + self.action_open_web() + elif event.button.id == "back-btn": + self.action_back() + + +class JSONViewerTUI(Screen): + """TUI screen for viewing JSON data.""" + + CSS_PATH = "codegen_theme.tcss" + BINDINGS = [ + Binding("escape,q", "back", "Back", show=True), + ] + + def __init__(self, data: Dict[str, Any]): + super().__init__() + self.data = data + + def compose(self) -> ComposeResult: + """Create child widgets for the JSON viewer.""" + yield Header() + + with Vertical(): + yield Static("📄 Agent Run JSON Data", classes="title") + yield Static("Use Q/Esc to go back", classes="help") + + # Format JSON with pretty printing + try: + json_text = json.dumps(self.data, indent=2, sort_keys=True) + yield Static(json_text, id="json-content") + except Exception as e: + yield Static(f"Error formatting JSON: {e}", id="json-content") + + yield Footer() + + def action_back(self) -> None: + """Go back to the agent detail screen.""" + self.app.pop_screen() \ No newline at end of file diff --git a/src/codegen.backup/cli/tui/app.py b/src/codegen.backup/cli/tui/app.py new file mode 100644 index 000000000..d47ffa559 --- /dev/null +++ b/src/codegen.backup/cli/tui/app.py @@ -0,0 +1,1372 @@ +"""Minimal TUI interface for Codegen CLI.""" + +import signal +import sys +import threading +import time +import tty +from datetime import datetime +from typing import Any + +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 +from codegen.cli.commands.claude.main import _run_claude_interactive +from codegen.cli.utils.org import resolve_org_id +from codegen.cli.utils.url import generate_webapp_url, get_domain +from codegen.shared.logging.get_logger import get_logger + +# Initialize logger for TUI telemetry +logger = get_logger(__name__) + + +class MinimalTUI: + """Minimal non-full-screen TUI for browsing agent runs.""" + + def __init__(self): + # Log TUI initialization + 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, + }, + ) + else: + logger.warning( + "TUI started without authentication", + extra={"operation": "tui.auth", "authenticated": False}, + ) + + self.agent_runs: list[dict[str, Any]] = [] + self.selected_index = 0 + self.running = True + self.show_action_menu = False + self.action_menu_selection = 0 + + # Tab management + self.tabs = ["recent", "claude", "new", "kanban"] + self.current_tab = 0 + + # Refresh state + self.is_refreshing = False + self.initial_loading = True # Track if we're still doing the initial load + self._auto_refresh_interval_seconds = 10 + self._refresh_lock = threading.Lock() + + # New tab state + self.prompt_input = "" + + self.cursor_position = 0 + self.input_mode = False # When true, we're typing in the input box + + # Set up signal handler for Ctrl+C + 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.start() + + 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.""" + while True: + # Sleep first so we don't immediately spam a refresh on start + time.sleep(self._auto_refresh_interval_seconds) + + if not self.running: + break + + # Only refresh when on recent tab and not currently refreshing + if self.current_tab == 0 and not self.is_refreshing: + # Try background refresh; if lock is busy, skip this tick + acquired = self._refresh_lock.acquire(blocking=False) + if not acquired: + continue + try: + # Double-check state after acquiring lock + if ( + self.running + and self.current_tab == 0 + and not self.is_refreshing + ): + self._background_refresh() + finally: + self._refresh_lock.release() + + def _background_refresh(self): + """Refresh data without disrupting selection/menu state; redraw if still on recent.""" + self.is_refreshing = True + # Do not redraw immediately to reduce flicker; header shows indicator on next paint + + previous_index = self.selected_index + try: + 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) + ) + else: + self.selected_index = 0 + finally: + self.is_refreshing = False + + # Redraw only if still on recent and app running + if self.running and self.current_tab == 0: + self._clear_and_redraw() + + def _get_webapp_domain(self) -> str: + """Get the webapp domain based on environment.""" + return get_domain() + + def _generate_agent_url(self, agent_id: str) -> str: + """Generate the complete agent URL.""" + return generate_webapp_url(f"x/{agent_id}") + + def _signal_handler(self, signum, frame): + """Handle Ctrl+C gracefully without clearing screen.""" + self.running = False + print("\n") # Just add a newline and exit + sys.exit(0) + + def _format_status_line(self, left_text: str) -> str: + """Format status line with instructions and org info on a new line below.""" + # 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" + ) + + # Use the same purple color as the Codegen logo + purple_color = "\033[38;2;82;19;217m" + reset_color = "\033[0m" + + # Return instructions on first line, org on second line (bottom left) + instructions_line = f"\033[90m{left_text}\033[0m" + org_line = f"{purple_color}• {org_name}{reset_color}" + + # Append a subtle refresh indicator when a refresh is in progress + if getattr(self, "is_refreshing", False): + org_line += " \033[90m■ Refreshing…\033[0m" + + return f"{instructions_line}\n{org_line}" + + 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)), + }, + ) + return False + + start_time = time.time() + + # 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, + }, + ) + + try: + import requests + + from codegen.cli.api.endpoints import API_ENDPOINT + + 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.raise_for_status() + user_data = user_response.json() + user_id = user_data.get("id") + + # Fetch agent runs - limit to 10 + params = { + "source_type": "API", + "limit": 10, + } + + if user_id: + params["user_id"] = user_id + + 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() + + self.agent_runs = response_data.get("items", []) + self.initial_loading = False # Mark initial loading as complete + + duration_ms = (time.time() - start_time) * 1000 + + # Only log the initial load, not refreshes to avoid noise + is_initial_load = not hasattr(self, "_has_loaded_before") + if is_initial_load: + logger.info( + "Agent runs loaded successfully", + extra={ + "operation": "tui.load_agent_runs", + "org_id": self.org_id, + "user_id": user_id, + "agent_count": len(self.agent_runs), + "duration_ms": duration_ms, + "is_initial_load": True, + }, + ) + + # Mark that we've loaded at least once + self._has_loaded_before = True + return True + + except Exception as e: + duration_ms = (time.time() - start_time) * 1000 + # 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, + }, + 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]: + """Format status with colored indicators matching kanban style.""" + # Check if this agent has a merged PR (done status) + is_done = False + if agent_run: + github_prs = agent_run.get("github_pull_requests", []) + for pr in github_prs: + if pr.get("state") == "closed" and pr.get("merged", False): + is_done = True + break + + if is_done: + 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 + "ACTIVE": "\033[38;2;162;119;255m○\033[0m", # aura purple #a277ff (hollow circle) + "RUNNING": "\033[38;2;162;119;255m●\033[0m", # aura purple #a277ff + "ERROR": "\033[38;2;255;103;103m○\033[0m", # aura red #ff6767 (empty circle) + "FAILED": "\033[38;2;255;103;103m○\033[0m", # aura red #ff6767 (empty circle) + "CANCELLED": "\033[38;2;109;109;109m○\033[0m", # aura gray #6d6d6d + "STOPPED": "\033[38;2;109;109;109m○\033[0m", # aura gray #6d6d6d + "PENDING": "\033[38;2;109;109;109m○\033[0m", # aura gray #6d6d6d + "TIMEOUT": "\033[38;2;255;202;133m●\033[0m", # aura orange #ffca85 + "MAX_ITERATIONS_REACHED": "\033[38;2;255;202;133m●\033[0m", # aura orange #ffca85 + "OUT_OF_TOKENS": "\033[38;2;255;202;133m●\033[0m", # aura orange #ffca85 + "EVALUATION": "\033[38;2;246;148;255m●\033[0m", # aura pink #f694ff + } + + status_text_map = { + "COMPLETE": "complete", + "ACTIVE": "active", + "RUNNING": "running", + "ERROR": "error", + "FAILED": "failed", + "CANCELLED": "cancelled", + "STOPPED": "stopped", + "PENDING": "pending", + "TIMEOUT": "timeout", + "MAX_ITERATIONS_REACHED": "max iterations", + "OUT_OF_TOKENS": "out of tokens", + "EVALUATION": "evaluation", + } + + circle = status_map.get(status, "\033[37m○\033[0m") + text = status_text_map.get(status, status.lower() if status else "unknown") + return circle, text + + def _format_pr_info(self, agent_run: dict) -> str: + """Format PR information as 'PR #123' or empty string.""" + github_prs = agent_run.get("github_pull_requests", []) + if not github_prs: + return "" + + pr = github_prs[0] # Take the first PR + pr_url = pr.get("url", "") + if not pr_url: + return "" + + # Extract PR number from URL like "https://github.com/org/repo/pull/123" + try: + pr_number = pr_url.split("/pull/")[-1].split("/")[0] + return f"PR #{pr_number}" + except (IndexError, AttributeError): + return "" + + def _strip_ansi_codes(self, text: str) -> str: + """Strip ANSI color codes from text.""" + import re + + ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") + return ansi_escape.sub("", text) + + def _format_date(self, created_at: str) -> str: + """Format creation date.""" + if not created_at or created_at == "Unknown": + return "Unknown" + + try: + dt = datetime.fromisoformat(created_at.replace("Z", "+00:00")) + return dt.strftime("%m/%d %H:%M") + except (ValueError, TypeError): + return created_at[:16] if len(created_at) > 16 else created_at + + def _display_header(self): + """Display the header with tabs.""" + # Simple header with indigo slashes and Codegen text + print("\033[38;2;82;19;217m" + "/" * 20 + " Codegen\033[0m") + print() # Add blank line between header and tabs + + # Display tabs + tab_line = "" + for i, tab in enumerate(self.tabs): + if i == self.current_tab: + tab_line += f"\033[38;2;255;202;133m/{tab}\033[0m " # Orange for active tab with slash + else: + tab_line += f"\033[90m{tab}\033[0m " # Gray for inactive tabs + + print(tab_line) + print() + + def _display_agent_list(self): + """Display the list of agent runs, fixed to 10 lines of main content.""" + if not self.agent_runs: + if self.initial_loading: + print("Loading...") + else: + print("No agent runs found.") + self._pad_to_lines(1) + return + + # Determine how many extra lines the inline action menu will print (if open) + menu_lines = 0 + if self.show_action_menu and 0 <= self.selected_index < len(self.agent_runs): + selected_run = self.agent_runs[self.selected_index] + github_prs = selected_run.get("github_pull_requests", []) + options_count = 1 # "open in web" + if github_prs: + options_count += 1 # "pull locally" + if github_prs and github_prs[0].get("url"): + options_count += 1 # "open PR" + menu_lines = options_count + 1 # +1 for the hint line + + # We want total printed lines (rows + menu) to be 10 + window_size = max(1, 10 - menu_lines) + + total = len(self.agent_runs) + if total <= window_size: + start = 0 + end = total + else: + 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 " " + ) + + 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" + + # Append PR info to summary if available + pr_info = self._format_pr_info(agent_run) + if pr_info: + summary = f"{summary} ({pr_info})" + + if len(summary) > 60: + summary = summary[:57] + "..." + + # Calculate display width of status (without ANSI codes) for alignment + status_display = f"{status_circle} {status_text}" + status_display_width = len(self._strip_ansi_codes(status_display)) + status_padding = " " * max(0, 17 - status_display_width) + + if i == self.selected_index and not self.show_action_menu: + line = f"\033[37m{prefix}{created:<10}\033[0m {status_circle} \033[37m{status_text}\033[0m{status_padding}\033[37m{summary}\033[0m" + else: + line = f"\033[90m{prefix}{created:<10}\033[0m {status_circle} \033[90m{status_text}\033[0m{status_padding}\033[90m{summary}\033[0m" + + print(line) + printed_rows += 1 + + # Show action menu right below the selected row if it's expanded + if i == self.selected_index and self.show_action_menu: + self._display_inline_action_menu(agent_run) + + # If fewer than needed to reach 10 lines, pad blank lines + total_printed = printed_rows + menu_lines + if total_printed < 10: + self._pad_to_lines(total_printed) + + def _display_new_tab(self): + """Display the new agent creation interface.""" + print("Create new background agent (Claude Code):") + print() + + # Get terminal width, default to 80 if can't determine + try: + import os + + terminal_width = os.get_terminal_size().columns + except (OSError, AttributeError): + terminal_width = 80 + + # Calculate input box width (leave some margin) + box_width = max(60, terminal_width - 4) + + # Input box with cursor + input_display = self.prompt_input + 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 :] + ) + + # Handle long input that exceeds box width + if len(input_display) > box_width - 4: + # Show portion around cursor + start_pos = max(0, self.cursor_position - (box_width // 2)) + 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 + 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 + "└" + "─" * (box_width - 2) + "┘" + reset) + print() + + # The new tab main content area should be a fixed 10 lines + self._pad_to_lines(6) + + 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", + }, + ) + + 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)), + }, + ) + 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), + }, + ) + 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" + ) + + start_time = time.time() + try: + payload = {"prompt": prompt.strip()} + headers = { + "Authorization": f"Bearer {self.token}", + "Content-Type": "application/json", + "x-codegen-client": "codegen__claude_code", + } + url = f"{API_ENDPOINT.rstrip('/')}/v1/organizations/{self.org_id}/agent/run" + + # API request details not needed in logs - focus on user actions and results + + response = requests.post(url, headers=headers, json=payload, timeout=30) + response.raise_for_status() + agent_run_data = response.json() + + run_id = agent_run_data.get("id", "Unknown") + status = agent_run_data.get("status", "Unknown") + web_url = self._generate_agent_url(run_id) + + 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()), + }, + ) + + print("\n\033[90mAgent run created successfully!\033[0m") + print(f"\033[90m Run ID: {run_id}\033[0m") + print(f"\033[90m Status: {status}\033[0m") + print(f"\033[90m Web URL: \033[38;2;255;202;133m{web_url}\033[0m") + + # Clear the input + self.prompt_input = "" + self.cursor_position = 0 + self.input_mode = False + + # Show post-creation menu + self._show_post_creation_menu(web_url) + + except Exception as e: + 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), + }, + exc_info=True, + ) + print(f"\n❌ Failed to create agent run: {e}") + input("\nPress Enter to continue...") + + def _show_post_creation_menu(self, web_url: str): + """Show menu after successful agent creation.""" + from codegen.cli.utils.inplace_print import inplace_print + + print("\n\033[90mWhat would you like to do next?\033[0m") + options = ["Open Trace ↗", "Go to Recent"] + selected = 0 + prev_lines = 0 + + def build_lines(): + menu_lines = [] + # Options + for i, option in enumerate(options): + if i == selected: + menu_lines.append(f" \033[37m→ {option}\033[0m") + 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" + ) + return menu_lines + + # Initial render + prev_lines = inplace_print(build_lines(), prev_lines) + + while True: + key = self._get_char() + if key == "\x1b[A" or key.lower() == "w": # Up arrow or W + selected = (selected - 1) % len(options) + prev_lines = inplace_print(build_lines(), prev_lines) + elif key == "\x1b[B" or key.lower() == "s": # Down arrow or S + selected = (selected + 1) % len(options) + prev_lines = inplace_print(build_lines(), prev_lines) + elif key == "\r" or key == "\n": # Enter - select option + if selected == 0: # Open Trace + try: + import webbrowser + + webbrowser.open(web_url) + except Exception as e: + print(f"\n❌ Failed to open browser: {e}") + input("Press Enter to continue...") + elif selected == 1: # Go to Recent + self.current_tab = 0 # Switch to recent tab + self.input_mode = False + self._load_agent_runs() # Refresh the data + break + elif key == "B": # Back to new tab + self.current_tab = 2 # 'new' tab index + self.input_mode = True + break + + def _display_dashboard_tab(self): + """Display the kanban interface access tab.""" + # Generate the proper domain-based URL for display + me_url = generate_webapp_url("me") + display_url = me_url.replace("https://", "").replace("http://", "") + + print(f" \033[37m→ Open Kanban ({display_url})\033[0m") + print() + print("Press Enter to open web kanban.") + # The kanban tab main content area should be a fixed 10 lines + self._pad_to_lines(7) + + def _display_claude_tab(self): + """Display the Claude Code interface tab.""" + print(" \033[37m→ Run Claude Code\033[0m") + print() + print("Press Enter to launch Claude Code with session tracking.") + # The claude tab main content area should be a fixed 10 lines + self._pad_to_lines(7) + + 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), + }, + ) + + print(f"\n🔄 Pulling PR branch for agent {agent_id}...") + print("─" * 50) + + start_time = time.time() + try: + # Call the existing pull command with the agent_id + 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, + }, + ) + + except typer.Exit as e: + duration_ms = (time.time() - start_time) * 1000 + # typer.Exit is expected for both success and failure cases + 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, + }, + ) + 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, + }, + ) + 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", + }, + ) + print(f"\n❌ Invalid agent ID: {agent_id}") + except Exception as e: + duration_ms = (time.time() - start_time) * 1000 + logger.error( + "Unexpected error during pull", + extra={ + "operation": "tui.pull_branch", + "agent_id": agent_id, + "org_id": getattr(self, "org_id", None), + "duration_ms": duration_ms, + "error_type": type(e).__name__, + "error_message": str(e), + }, + exc_info=True, + ) + print(f"\n❌ Unexpected error during pull: {e}") + + print("─" * 50) + input("Press Enter to continue...") + + def _display_content(self): + """Display content based on current tab.""" + if self.current_tab == 0: # recent + self._display_agent_list() + elif self.current_tab == 1: # claude + self._display_claude_tab() + elif self.current_tab == 2: # new + self._display_new_tab() + elif self.current_tab == 3: # kanban + self._display_dashboard_tab() + + def _pad_to_lines(self, lines_printed: int, target: int = 10): + """Pad the main content area with blank lines to reach a fixed height.""" + for _ in range(max(0, target - lines_printed)): + print() + + def _display_inline_action_menu(self, agent_run: dict): + """Display action menu inline below the selected row.""" + agent_id = agent_run.get("id", "unknown") + web_url = self._generate_agent_url(agent_id) + + # Check if there are GitHub PRs associated with this agent run + github_prs = agent_run.get("github_pull_requests", []) + + # Build options in the requested order + options = [] + + # 1. Open PR (if available) + if github_prs: + pr_url = github_prs[0].get("url", "") + if pr_url: + # Extract PR number for display + try: + pr_number = pr_url.split("/pull/")[-1].split("/")[0] + options.append(f"Open PR #{pr_number} ↗") + except (IndexError, AttributeError): + options.append("Open PR ↗") + + # 2. Pull locally (if PRs available) + if github_prs: + options.append("Pull locally") + + # 3. Open Trace (always available) + options.append("Open Trace ↗") + + for i, option in enumerate(options): + if i == self.action_menu_selection: + # Highlight selected option in white + print(f" \033[37m→ {option}\033[0m") + else: + # All other options in gray + print(f" \033[90m {option}\033[0m") + + print("\033[90m [Enter] select • [↑↓] navigate • [C] close\033[0m") + + def _get_char(self): + """Get a single character from stdin, handling arrow keys.""" + try: + 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 + # Read the rest of the escape sequence synchronously + ch2 = sys.stdin.read(1) + if ch2 == "[": + ch3 = sys.stdin.read(1) + return f"\x1b[{ch3}" + else: + # Return combined sequence (e.g., Alt+) + 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 details, R refresh, Q quit") + try: + return input("> ").strip()[:1].lower() or "\n" + except KeyboardInterrupt: + return "q" + + def _handle_keypress(self, key: str): + """Handle key presses for navigation.""" + # Global quit (but not when typing in new tab) + if key == "\x03": # Ctrl+C + logger.info( + "TUI session ended by user", + extra={ + "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", + }, + ) + 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 + 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", + }, + ) + self.running = False + return + + # Tab switching (works even in input mode) + if key == "\t": # Tab key + old_tab = self.current_tab + self.current_tab = (self.current_tab + 1) % len(self.tabs) + + # Log significant tab switches but at info level since it's user action + logger.info( + 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", + }, + ) + + # Reset state when switching tabs + self.show_action_menu = False + self.action_menu_selection = 0 + self.selected_index = 0 + # Auto-focus prompt when switching to new tab + if self.current_tab == 2: # new tab + self.input_mode = True + self.cursor_position = len(self.prompt_input) + else: + self.input_mode = False + return + + # Handle based on current context + if self.input_mode: + self._handle_input_mode_keypress(key) + elif self.show_action_menu: + self._handle_action_menu_keypress(key) + elif self.current_tab == 0: # recent tab + self._handle_recent_keypress(key) + elif self.current_tab == 1: # claude tab + self._handle_claude_tab_keypress(key) + elif self.current_tab == 2: # new tab + self._handle_new_tab_keypress(key) + elif self.current_tab == 3: # kanban tab + self._handle_dashboard_tab_keypress(key) + + def _handle_input_mode_keypress(self, key: str): + """Handle keypresses when in text input mode.""" + if key == "B": # Back action in new tab + self.input_mode = False + elif key == "\r" or key == "\n": # Enter - create agent run + if self.prompt_input.strip(): # Only create if there's actual content + self._create_background_agent(self.prompt_input) + else: + 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.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.cursor_position += 1 + + def _handle_action_menu_keypress(self, key: str): + """Handle action menu keypresses.""" + if key == "\r" or key == "\n": # Enter + self._execute_inline_action() + self.show_action_menu = False # Close menu after action + elif key.lower() == "c" or key == "\x1b[D": # 'C' key or Left arrow to close + self.show_action_menu = False # Close menu + self.action_menu_selection = 0 # Reset selection + elif key == "\x1b[A" or key.lower() == "w": # Up arrow or W + # Get available options count + if 0 <= self.selected_index < len(self.agent_runs): + agent_run = self.agent_runs[self.selected_index] + github_prs = agent_run.get("github_pull_requests", []) + options_count = 1 # Always have "Open Trace" + if github_prs: + options_count += 1 # "Pull locally" + if github_prs and github_prs[0].get("url"): + options_count += 1 # "Open PR" + + self.action_menu_selection = max(0, self.action_menu_selection - 1) + elif key == "\x1b[B" or key.lower() == "s": # Down arrow or S + # Get available options count + if 0 <= self.selected_index < len(self.agent_runs): + agent_run = self.agent_runs[self.selected_index] + github_prs = agent_run.get("github_pull_requests", []) + options_count = 1 # Always have "Open Trace" + if github_prs: + options_count += 1 # "Pull locally" + 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 + ) + + def _handle_recent_keypress(self, key: str): + """Handle keypresses in the recent tab.""" + if key == "\x1b[A" or key.lower() == "w": # Up arrow or W + self.selected_index = max(0, self.selected_index - 1) + self.show_action_menu = False # Close any open menu + self.action_menu_selection = 0 + elif key == "\x1b[B" or key.lower() == "s": # Down arrow or S + self.selected_index = min(len(self.agent_runs) - 1, self.selected_index + 1) + self.show_action_menu = False # Close any open menu + self.action_menu_selection = 0 + elif key == "\x1b[C": # Right arrow - open action menu + self.show_action_menu = True # Open action menu + self.action_menu_selection = 0 # Reset to first option + elif key == "\x1b[D": # Left arrow - close action menu + self.show_action_menu = False # Close action menu + self.action_menu_selection = 0 + elif key == "\r" or key == "\n" or key.lower() == "e": # Enter or E + self.show_action_menu = True # Open action menu + self.action_menu_selection = 0 # Reset to first option + elif key.lower() == "r": + self._refresh() + self.show_action_menu = False # Close menu on refresh + self.action_menu_selection = 0 + + def _handle_new_tab_keypress(self, key: str): + """Handle keypresses in the new tab.""" + if key == "\r" or key == "\n": # Enter - start input mode + if not self.input_mode: + self.input_mode = True + self.cursor_position = len(self.prompt_input) + else: + # If already in input mode, Enter should create the agent + self._create_background_agent(self.prompt_input) + + 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), + }, + ) + try: + import webbrowser + + me_url = generate_webapp_url("me") + 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)}, + ) + print(f"\n❌ Failed to open browser: {e}") + input("Press Enter to continue...") + + def _handle_claude_tab_keypress(self, key: str): + """Handle keypresses in the claude tab.""" + if key == "\r" or key == "\n": # Enter - run Claude Code + self._run_claude_code() + + 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", + }, + ) + + 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)), + }, + ) + print("\n❌ Not authenticated or no organization configured.") + input("Press Enter to continue...") + return + + # Show immediate feedback in orange + print("\n\033[38;2;255;202;133m> claude code mode\033[0m") + + # Stop the TUI and clear screen completely after brief moment + self.running = False + print("\033[2J\033[H", end="") # Clear entire screen and move cursor to top + + start_time = time.time() + try: + # Transition details not needed - the launch and completion logs are sufficient + + # Call the interactive claude function with the current org_id + # The function handles all the session tracking and launching + _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", + }, + ) + + 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", + }, + ) + 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, + }, + 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", + }, + ) + sys.exit(0) + + def _execute_inline_action(self): + """Execute the selected action from the inline menu.""" + if not (0 <= self.selected_index < len(self.agent_runs)): + return + + agent_run = self.agent_runs[self.selected_index] + agent_id = agent_run.get("id", "unknown") + web_url = self._generate_agent_url(agent_id) + + # Build options in the same order as display + github_prs = agent_run.get("github_pull_requests", []) + options = [] + + # 1. Open PR (if available) + if github_prs and github_prs[0].get("url"): + options.append("open PR") + + # 2. Pull locally (if PRs available) + if github_prs: + options.append("pull locally") + + # 3. Open Trace (always available) + options.append("open trace") + + # Execute the currently selected option + if len(options) > self.action_menu_selection: + 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), + }, + ) + + if selected_option == "open PR": + pr_url = github_prs[0]["url"] + try: + import webbrowser + + webbrowser.open(pr_url) + # 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), + }, + ) + print(f"\n❌ Failed to open PR: {e}") + input("Press Enter to continue...") # Only pause on errors + elif selected_option == "pull locally": + self._pull_agent_branch(agent_id) + elif selected_option == "open trace": + try: + import webbrowser + + webbrowser.open(web_url) + # 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), + }, + ) + print(f"\n❌ Failed to open browser: {e}") + input("Press Enter to continue...") # Only pause on errors + + def _open_agent_details(self): + """Toggle the inline action menu.""" + self.show_action_menu = not self.show_action_menu + if not self.show_action_menu: + self.action_menu_selection = 0 # Reset selection when closing + + def _refresh(self): + """Refresh the agent runs list.""" + # Indicate refresh and redraw immediately so the user sees it + self.is_refreshing = True + self._clear_and_redraw() + + if self._load_agent_runs(): + self.selected_index = 0 # Reset selection + + # Clear refresh indicator and redraw with updated data + self.is_refreshing = False + self._clear_and_redraw() + + def _clear_and_redraw(self): + """Clear screen and redraw everything.""" + # Move cursor to top and clear screen from cursor down + print("\033[H\033[J", end="") + self._display_header() + self._display_content() + + # 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')}" + ) + elif self.input_mode: # other input modes + 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')}" + ) + 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')}" + ) + elif self.current_tab == 1: # claude + 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')}" + ) + elif self.current_tab == 3: # kanban + print( + f"\n{self._format_status_line('[Tab] switch tabs • [Enter] open web kanban • [Q] quit')}" + ) + + def run(self): + """Run the minimal TUI.""" + if not self.is_authenticated: + # Automatically start login flow for first-time users + from codegen.cli.auth.login import login_routine + + try: + login_routine() + # login_routine will launch TUI after successful authentication + return + except Exception: + # If login fails, just exit gracefully + return + + # Show UI immediately + self._clear_and_redraw() + + # Start initial data load in background (non-blocking) + def initial_load(): + self._load_agent_runs() + if self.running: # Only redraw if still running + self._clear_and_redraw() + + load_thread = threading.Thread(target=initial_load, daemon=True) + load_thread.start() + + # Main event loop + while self.running: + try: + key = self._get_char() + self._handle_keypress(key) + if self.running: # Only redraw if we're still running + self._clear_and_redraw() + except KeyboardInterrupt: + # This should be handled by the signal handler, but just in case + break + + print() # Add newline before exiting + + +def run_tui(): + """Run the minimal Codegen 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, + ) + raise + finally: + logger.info( + "TUI session ended", extra={"operation": "tui.end", "component": "run_tui"} + ) diff --git a/src/codegen.backup/cli/tui/codegen_theme.tcss b/src/codegen.backup/cli/tui/codegen_theme.tcss new file mode 100644 index 000000000..cfddfd602 --- /dev/null +++ b/src/codegen.backup/cli/tui/codegen_theme.tcss @@ -0,0 +1,185 @@ +/* Codegen Custom Theme - Indigo, Black, White, Teal */ + +/* +Color Palette: +- Indigo: #4f46e5 (primary), #6366f1 (light), #3730a3 (dark) +- Black/Charcoal: #000000, #1a1a1a +- White: #ffffff, #f8fafc +- Teal: #14b8a6 (accent), #2dd4bf (light), #0f766e (dark) +- Grays: #111827, #1f2937, #374151, #4b5563, #9ca3af, #d1d5db, #e5e7eb, #f3f4f6 +*/ + +/* Main app background */ +Screen { + background: #1a1a1a; +} + +/* Header and Footer - Primary Indigo */ +Header { + dock: top; + height: 3; + background: #4f46e5; + color: #ffffff; +} + +Footer { + dock: bottom; + height: 3; + background: #4f46e5; + color: #ffffff; +} + +/* Title styling - Light Indigo with bold text */ +.title { + text-style: bold; + margin: 1; + color: #6366f1; + text-align: center; +} + +/* Help text - Muted gray */ +.help { + margin-bottom: 1; + color: #9ca3af; + text-align: center; +} + +/* Warning messages - Black background with white text and teal border */ +.warning-message { + text-align: center; + margin: 2; + padding: 2; + background: #000000; + color: #ffffff; + text-style: bold; + border: solid #14b8a6; +} + +/* DataTable styling */ +DataTable { + height: 1fr; + margin-top: 1; + background: #111827; +} + +DataTable > .datatable--header { + text-style: bold; + background: #4f46e5; + color: #ffffff; +} + +DataTable > .datatable--odd-row { + background: #1f2937; + color: #ffffff; +} + +DataTable > .datatable--even-row { + background: #111827; + color: #ffffff; +} + +DataTable > .datatable--cursor { + background: #14b8a6; + color: #000000; + text-style: bold; +} + +DataTable > .datatable--hover { + background: #0f766e; + color: #ffffff; +} + +/* Organization selector specific styling */ +#auth-warning { + height: 100%; + align: center middle; +} + +Vertical { + height: 100%; + background: #1a1a1a; +} + +/* Button styling */ +Button { + background: #4f46e5; + color: #ffffff; + border: solid #14b8a6; +} + +Button:hover { + background: #6366f1; + color: #ffffff; +} + +Button.-active { + background: #14b8a6; + color: #000000; +} + +/* Static widget styling */ +Static { + color: #ffffff; +} + +/* Container styling */ +Container { + background: #1a1a1a; +} + +/* Focus styling */ +*:focus { + border: solid #14b8a6; +} + +/* Success/Error/Info colors using our palette */ +.success { + background: #14b8a6; + color: #000000; +} + +.error { + background: #000000; + color: #ffffff; + border: solid #14b8a6; +} + +.info { + background: #4f46e5; + color: #ffffff; +} + +/* Notification styling */ +.notification { + background: #1f2937; + color: #ffffff; + border: solid #14b8a6; +} + +/* Organization-specific styling for current org indicator */ +.org-current { + color: #14b8a6; + text-style: bold; +} + +.org-name { + color: #6366f1; +} + +/* Status indicators */ +.status-active { + color: #14b8a6; +} + +.status-complete { + color: #2dd4bf; +} + +.status-error { + color: #ffffff; + background: #000000; +} + +.status-pending { + color: #9ca3af; +} \ No newline at end of file diff --git a/src/codegen.backup/cli/tui/codegen_tui.tcss b/src/codegen.backup/cli/tui/codegen_tui.tcss new file mode 100644 index 000000000..3d8c92795 --- /dev/null +++ b/src/codegen.backup/cli/tui/codegen_tui.tcss @@ -0,0 +1,74 @@ +/* Codegen TUI Styles */ + +Screen { + background: $background; +} + +Header { + dock: top; + height: 3; + background: $primary; + color: $text; +} + +Footer { + dock: bottom; + height: 3; + background: $primary; + color: $text; +} + +.title { + text-style: bold; + margin: 1; + color: $primary; + text-align: center; +} + +.help { + margin-bottom: 1; + color: $text-muted; + text-align: center; +} + +.warning-message { + text-align: center; + margin: 2; + padding: 2; + background: $warning; + color: $text; + text-style: bold; +} + +DataTable { + height: 1fr; + margin-top: 1; +} + +DataTable > .datatable--header { + text-style: bold; + background: $primary; + color: $text; +} + +DataTable > .datatable--odd-row { + background: $surface; +} + +DataTable > .datatable--even-row { + background: $background; +} + +DataTable > .datatable--cursor { + background: $accent; + color: $text; +} + +#auth-warning { + height: 100%; + align: center middle; +} + +Vertical { + height: 100%; +} diff --git a/src/codegen.backup/cli/tui/widows_app.py b/src/codegen.backup/cli/tui/widows_app.py new file mode 100644 index 000000000..6a3b98e27 --- /dev/null +++ b/src/codegen.backup/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.backup/cli/utils/codemod_manager.py b/src/codegen.backup/cli/utils/codemod_manager.py new file mode 100644 index 000000000..06ee6871d --- /dev/null +++ b/src/codegen.backup/cli/utils/codemod_manager.py @@ -0,0 +1,146 @@ +import builtins +from pathlib import Path + +import rich +import typer + +from codegen.cli.utils.function_finder import DecoratedFunction, find_codegen_functions + + +def _might_have_decorators(file_path: Path) -> bool: + """Quick check if a file might contain codegen decorators. + + This is a fast pre-filter that checks if '@codegen' appears anywhere in the file. + Much faster than parsing the AST for files that definitely don't have decorators. + """ + try: + # Read in binary mode and check for b'@codegen' to handle any encoding + with open(file_path, "rb") as f: + return b"@codegen" in f.read() + except Exception: + return False + + +class CodemodManager: + """Manages codemod operations in the local filesystem.""" + + @staticmethod + def get_valid_name(name: str) -> str: + return name.lower().replace(" ", "_").replace("-", "_") + + @classmethod + def get_codemod(cls, name: str, start_path: Path | None = None) -> DecoratedFunction: + """Get and validate a codemod by name. + + Args: + name: Name of the codemod to find + start_path: Directory to start searching from (default: current directory) + + Returns: + The validated DecoratedFunction + + Raises: + typer.Exit: If codemod can't be found or loaded + """ + # First try to find the codemod + codemod = cls.get(name, start_path) + if not codemod: + # If not found, check if any codemods exist + all_codemods = cls.list(start_path) + if not all_codemods: + rich.print("[red]Error:[/red] No codemods found. Create one with:") + rich.print(" codegen create my-codemod") + raise typer.Exit(1) + else: + available = "\n ".join(f"- {c.name}" for c in all_codemods) + rich.print(f"[red]Error:[/red] Codemod '{name}' not found. Available codemods:") + rich.print(f" {available}") + raise typer.Exit(1) + + # Verify we can import it + try: + # This will raise ValueError if function can't be imported + codemod.validate() + return codemod + except Exception as e: + rich.print(f"[red]Error:[/red] Error loading codemod '{name}': {e!s}") + raise typer.Exit(1) + + @classmethod + def list(cls, start_path: Path | None = None) -> builtins.list[DecoratedFunction]: + """List all codegen decorated functions in Python files under the given path. + + This is an alias for get_decorated for better readability. + """ + return cls.get_decorated(start_path) + + @classmethod + def get(cls, name: str, start_path: Path | None = None) -> DecoratedFunction | None: + """Get a specific codegen decorated function by name. + + Args: + name: Name of the function to find (case-insensitive, spaces/hyphens converted to underscores) + start_path: Directory or file to start searching from. Defaults to current working directory. + + Returns: + The DecoratedFunction if found, None otherwise + + """ + valid_name = cls.get_valid_name(name) + functions = cls.get_decorated(start_path) + + for func in functions: + if cls.get_valid_name(func.name) == valid_name: + return func + return None + + @classmethod + def exists(cls, name: str, start_path: Path | None = None) -> bool: + """Check if a codegen decorated function with the given name exists. + + Args: + name: Name of the function to check (case-insensitive, spaces/hyphens converted to underscores) + start_path: Directory or file to start searching from. Defaults to current working directory. + + Returns: + True if the function exists, False otherwise + + """ + return cls.get(name, start_path) is not None + + @classmethod + def get_decorated(cls, start_path: Path | None = None) -> builtins.list[DecoratedFunction]: + """Find all codegen decorated functions in Python files under the given path. + + Args: + start_path: Directory or file to start searching from. Defaults to current working directory. + + Returns: + List of DecoratedFunction objects found in the files + + """ + if start_path is None: + start_path = Path.cwd() + + # Look only in .codegen/codemods + codemods_dir = start_path / ".codegen" / "codemods" + if not codemods_dir.exists(): + return [] + + all_functions = [] + seen_paths = set() # Track unique file paths + + for path in codemods_dir.rglob("*.py"): + # Skip if we've already processed this file + if path in seen_paths: + continue + seen_paths.add(path) + + if _might_have_decorators(path): + try: + functions = find_codegen_functions(path) + all_functions.extend(functions) + except Exception: + pass # Skip files we can't parse + + return all_functions diff --git a/src/codegen.backup/cli/utils/codemods.py b/src/codegen.backup/cli/utils/codemods.py new file mode 100644 index 000000000..c704d9629 --- /dev/null +++ b/src/codegen.backup/cli/utils/codemods.py @@ -0,0 +1,41 @@ +from dataclasses import dataclass +from pathlib import Path + +from codegen.cli.api.webapp_routes import generate_webapp_url +from codegen.cli.utils.schema import CodemodConfig + + +@dataclass +class Codemod: + """Represents a codemod in the local filesystem.""" + + name: str + path: Path + config: CodemodConfig | None = None + + def get_url(self) -> str: + """Get the URL for this codemod.""" + if self.config is None: + return "" + return generate_webapp_url(path=f"codemod/{self.config.codemod_id}") + + def relative_path(self) -> str: + """Get the relative path to this codemod.""" + return str(self.path.relative_to(Path.cwd())) + + def get_current_source(self) -> str: + """Get the current source code for this codemod.""" + text = self.path.read_text() + text = text.strip() + return text + + def get_system_prompt_path(self) -> Path: + """Get the path to the system prompt for this codemod.""" + return self.path.parent / "system-prompt.md" + + def get_system_prompt(self) -> str: + """Get the system prompt for this codemod.""" + path = self.get_system_prompt_path() + if not path.exists(): + return "" + return path.read_text() diff --git a/src/codegen.backup/cli/utils/count_functions_2.py b/src/codegen.backup/cli/utils/count_functions_2.py new file mode 100644 index 000000000..9cf93f0ec --- /dev/null +++ b/src/codegen.backup/cli/utils/count_functions_2.py @@ -0,0 +1 @@ +NumberType = int | float diff --git a/src/codegen.backup/cli/utils/default_code.py b/src/codegen.backup/cli/utils/default_code.py new file mode 100644 index 000000000..5d42596e0 --- /dev/null +++ b/src/codegen.backup/cli/utils/default_code.py @@ -0,0 +1,20 @@ +DEFAULT_CODEMOD = '''import codegen +from codegen.sdk.core.codebase import Codebase + + +@codegen.function("{name}") +def run(codebase: Codebase): + """Add a description of what this codemod does.""" + # Add your code here + print('Total files: ', len(codebase.files)) + print('Total functions: ', len(codebase.functions)) + print('Total imports: ', len(codebase.imports)) + + +if __name__ == "__main__": + print('Parsing codebase...') + codebase = Codebase("./") + + print('Running...') + run(codebase) +''' diff --git a/src/codegen.backup/cli/utils/function_finder.py b/src/codegen.backup/cli/utils/function_finder.py new file mode 100644 index 000000000..6cf616482 --- /dev/null +++ b/src/codegen.backup/cli/utils/function_finder.py @@ -0,0 +1,316 @@ +import ast +import dataclasses +import importlib +import importlib.util +from dataclasses import dataclass +from pathlib import Path + +from codegen.shared.enums.programming_language import ProgrammingLanguage + + +@dataclass +class DecoratedFunction: + """Represents a function decorated with @codegen.""" + + name: str + source: str + lint_mode: bool + lint_user_whitelist: list[str] + subdirectories: list[str] | None = None + language: ProgrammingLanguage | None = None + filepath: Path | None = None + parameters: list[tuple[str, str | None]] = dataclasses.field(default_factory=list) + arguments_type_schema: dict | None = None + + def run(self, codebase) -> str | None: + """Import and run the actual function from its file. + + Args: + codebase: The codebase to run the function on + + Returns: + The result of running the function (usually a diff string) + """ + if not self.filepath: + msg = "Cannot run function without filepath" + raise ValueError(msg) + + # Import the module containing the function + spec = importlib.util.spec_from_file_location("module", self.filepath) + if not spec or not spec.loader: + msg = f"Could not load module from {self.filepath}" + raise ImportError(msg) + + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + # Find the decorated function + for item_name in dir(module): + item = getattr(module, item_name) + if hasattr(item, "__codegen_name__") and item.__codegen_name__ == self.name: + # Found our function, run it + return item(codebase) + + msg = f"Could not find function '{self.name}' in {self.filepath}" + raise ValueError(msg) + + def validate(self) -> None: + """Verify that this function can be imported and accessed. + + Raises: + ValueError: If the function can't be found or imported + """ + if not self.filepath: + msg = "Cannot validate function without filepath" + raise ValueError(msg) + + # Import the module containing the function + spec = importlib.util.spec_from_file_location("module", self.filepath) + if not spec or not spec.loader: + msg = f"Could not load module from {self.filepath}" + raise ImportError(msg) + + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + # Find the decorated function + for item_name in dir(module): + item = getattr(module, item_name) + if hasattr(item, "__codegen_name__") and item.__codegen_name__ == self.name: + return # Found it! + + msg = f"Could not find function '{self.name}' in {self.filepath}" + raise ValueError(msg) + + +class CodegenFunctionVisitor(ast.NodeVisitor): + def __init__(self): + self.functions: list[DecoratedFunction] = [] + self.file_content: str = "" + + def get_function_name(self, node: ast.Call) -> str: + keywords = {k.arg: k.value for k in node.keywords} + if "name" in keywords: + return ast.literal_eval(keywords["name"]) + return ast.literal_eval(node.args[0]) + + def get_subdirectories(self, node: ast.Call) -> list[str] | None: + keywords = {k.arg: k.value for k in node.keywords} + if "subdirectories" in keywords: + return ast.literal_eval(keywords["subdirectories"]) + if len(node.args) > 1: + return ast.literal_eval(node.args[1]) + return None + + def get_language(self, node: ast.Call) -> ProgrammingLanguage | None: + keywords = {k.arg: k.value for k in node.keywords} + if "language" in keywords: + lang_node = keywords["language"] + if hasattr(lang_node, "attr"): + return ProgrammingLanguage(lang_node.attr) + else: + return ProgrammingLanguage(ast.literal_eval(lang_node)) + if len(node.args) > 2: + return ast.literal_eval(node.args[2]) + return None + + def get_function_body(self, node: ast.FunctionDef) -> str: + """Extract and unindent the function body.""" + # Get the start and end positions of the function body + first_stmt = node.body[0] + last_stmt = node.body[-1] + + # Get the line numbers (1-based in source lines) + start_line = first_stmt.lineno - 1 # Convert to 0-based + end_line = last_stmt.end_lineno if hasattr(last_stmt, "end_lineno") else last_stmt.lineno + + # Get the raw source lines for the entire body + source_lines = self.source.splitlines()[start_line:end_line] + + # Find the minimum indentation of non-empty lines + indents = [len(line) - len(line.lstrip()) for line in source_lines if line.strip()] + if not indents: + return "" + + min_indent = min(indents) + + # Remove the minimum indentation from each line + unindented_lines = [] + for line in source_lines: + if line.strip(): # Non-empty line + unindented_lines.append(line[min_indent:]) + else: # Empty line + unindented_lines.append("") + + return "\n".join(unindented_lines) + + def _get_annotation(self, annotation) -> str: + """Helper function to retrieve the string representation of an annotation. + + Args: + annotation: The annotation node. + + Returns: + str: The string representation of the annotation. + + """ + if isinstance(annotation, ast.Name): + return annotation.id + elif isinstance(annotation, ast.Subscript): + return f"{self._get_annotation(annotation.value)}[{self._get_annotation(annotation.slice)}]" + elif isinstance(annotation, ast.Attribute): + return f"{self._get_annotation(annotation.value)}.{annotation.attr}" + elif isinstance(annotation, ast.Tuple): + return ", ".join(self._get_annotation(elt) for elt in annotation.elts) + else: + return "Any" + + def get_function_parameters(self, node: ast.FunctionDef) -> list[tuple[str, str | None]]: + """Extracts the parameters and their types from an AST FunctionDef node. + + Args: + node (ast.FunctionDef): The AST node of the function. + + Returns: + List[Tuple[str, Optional[str]]]: A list of tuples containing parameter names and their type annotations. + The type is `None` if no annotation is present. + + """ + parameters = [] + for arg in node.args.args: + param_name = arg.arg + if arg.annotation: + param_type = ast.unparse(arg.annotation) if hasattr(ast, "unparse") else self._get_annotation(arg.annotation) + else: + param_type = None + parameters.append((param_name, param_type)) + + # Handle *args + if node.args.vararg: + param_name = f"*{node.args.vararg.arg}" + if node.args.vararg.annotation: + param_type = ast.unparse(node.args.vararg.annotation) if hasattr(ast, "unparse") else self._get_annotation(node.args.vararg) + else: + param_type = None + parameters.append((param_name, param_type)) + + # Handle **kwargs + if node.args.kwarg: + param_name = f"**{node.args.kwarg.arg}" + if node.args.kwarg.annotation: + param_type = ast.unparse(node.args.kwarg.annotation) if hasattr(ast, "unparse") else self._get_annotation(node.args.kwarg) + else: + param_type = None + parameters.append((param_name, param_type)) + + return parameters + + def visit_FunctionDef(self, node): + for decorator in node.decorator_list: + if ( + isinstance(decorator, ast.Call) + and (len(decorator.args) > 0 or len(decorator.keywords) > 0) + and ( + # Check if it's a direct codegen.X call + (isinstance(decorator.func, ast.Attribute) and isinstance(decorator.func.value, ast.Name) and decorator.func.value.id == "codegen") + or + # Check if it starts with codegen.anything.anything... + (isinstance(decorator.func, ast.Attribute) and isinstance(decorator.func.value, ast.Attribute) and self._has_codegen_root(decorator.func.value)) + ) + ): + # Get additional metadata for webhook + lint_mode = decorator.func.attr == "webhook" + lint_user_whitelist = [] + if lint_mode and len(decorator.keywords) > 0: + for keyword in decorator.keywords: + if keyword.arg == "users" and isinstance(keyword.value, ast.List): + lint_user_whitelist = [ast.literal_eval(elt).lstrip("@") for elt in keyword.value.elts] + + self.functions.append( + DecoratedFunction( + name=self.get_function_name(decorator), + subdirectories=self.get_subdirectories(decorator), + language=self.get_language(decorator), + source=self.get_function_body(node), + lint_mode=lint_mode, + lint_user_whitelist=lint_user_whitelist, + parameters=self.get_function_parameters(node), + ) + ) + + def _has_codegen_root(self, node): + """Recursively check if an AST node chain starts with codegen.""" + if isinstance(node, ast.Name): + return node.id == "codegen" + elif isinstance(node, ast.Attribute): + return self._has_codegen_root(node.value) + return False + + def _get_decorator_attrs(self, node): + """Get all attribute names in a decorator chain.""" + attrs = [] + while isinstance(node, ast.Attribute): + attrs.append(node.attr) + node = node.value + return attrs + + def visit_Module(self, node): + # Store the full source code for later use + self.source = self.file_content + self.generic_visit(node) + + +def _extract_arguments_type_schema(func: DecoratedFunction) -> dict | None: + """Extracts the arguments type schema from a DecoratedFunction object.""" + try: + spec = importlib.util.spec_from_file_location("module", func.filepath) + if spec is None or spec.loader is None: + return None + module = importlib.util.module_from_spec(spec) + + fn_arguments_param_type = None + for p in func.parameters: + if p[0] == "arguments": + fn_arguments_param_type = p[1] + + if fn_arguments_param_type is not None: + spec.loader.exec_module(module) + + schema = getattr(module, fn_arguments_param_type).model_json_schema() + return schema + return None + except Exception as e: + print(f"Error parsing {func.filepath}, could not introspect for arguments parameter") + print(e) + return None + + +def find_codegen_functions(filepath: Path) -> list[DecoratedFunction]: + """Find all codegen functions in a Python file. + + Args: + filepath: Path to the Python file to search + + Returns: + List of DecoratedFunction objects found in the file + + Raises: + Exception: If the file cannot be parsed + + """ + # Read and parse the file + with open(filepath) as f: + file_content = f.read() + tree = ast.parse(file_content) + + # Find all codegen.function decorators + visitor = CodegenFunctionVisitor() + visitor.file_content = file_content + visitor.visit(tree) + + # Add filepath to each function + for func in visitor.functions: + func.filepath = filepath + func.arguments_type_schema = _extract_arguments_type_schema(func) + + return visitor.functions diff --git a/src/codegen.backup/cli/utils/inplace_print.py b/src/codegen.backup/cli/utils/inplace_print.py new file mode 100644 index 000000000..22a9c4228 --- /dev/null +++ b/src/codegen.backup/cli/utils/inplace_print.py @@ -0,0 +1,27 @@ +import sys +from typing import Iterable + + +def inplace_print(lines: Iterable[str], prev_lines_rendered: int) -> int: + """Redraw a small block of text in-place without scrolling. + + Args: + lines: The lines to render (each should NOT include a trailing newline) + prev_lines_rendered: How many lines were rendered in the previous frame. Pass 0 on first call. + + Returns: + The number of lines rendered this call. Use as prev_lines_rendered on the next call. + """ + # Move cursor up to the start of the previous block (if any) + if prev_lines_rendered > 0: + sys.stdout.write(f"\x1b[{prev_lines_rendered}F") # Cursor up N lines + + # Rewrite each line, clearing it first to avoid remnants from previous content + count = 0 + for line in lines: + sys.stdout.write("\x1b[2K\r") # Clear entire line and return carriage + sys.stdout.write(f"{line}\n") + count += 1 + + sys.stdout.flush() + return count diff --git a/src/codegen.backup/cli/utils/json_schema.py b/src/codegen.backup/cli/utils/json_schema.py new file mode 100644 index 000000000..1e228bdca --- /dev/null +++ b/src/codegen.backup/cli/utils/json_schema.py @@ -0,0 +1,41 @@ +import json +from pathlib import Path +from tempfile import TemporaryDirectory +from typing import Any + +from datamodel_code_generator import DataModelType, InputFileType, generate +from pydantic import BaseModel + +# This utility contains functions for utilizing, transforming and validating JSON schemas generated by Pydantic models. + + +def get_schema(model: BaseModel) -> dict: + return model.model_json_schema() + + +def validate_json(schema: dict, json_data: str) -> bool: + json_schema = json.dumps(schema) + exec_scope: dict[str, Any] = {} + model_name = schema["title"] + with TemporaryDirectory() as temporary_directory_name: + temporary_directory = Path(temporary_directory_name) + output = Path(temporary_directory / "model.py") + generate( + json_schema, + input_file_type=InputFileType.JsonSchema, + input_filename="temp.json", + output=output, + # set up the output model types + output_model_type=DataModelType.PydanticV2BaseModel, + ) + + exec(output.read_text(), exec_scope, exec_scope) + print(f"exec_scope: {exec_scope}") + model = exec_scope.get(model_name) + if model is None: + return False + try: + model.model_validate_json(json_data) + return True + except Exception as e: + return False diff --git a/src/codegen.backup/cli/utils/notebooks.py b/src/codegen.backup/cli/utils/notebooks.py new file mode 100644 index 000000000..f859adc0f --- /dev/null +++ b/src/codegen.backup/cli/utils/notebooks.py @@ -0,0 +1,216 @@ +import json +from pathlib import Path +from typing import Any + +DEFAULT_CELLS = [ + { + "cell_type": "code", + "source": """from codegen.sdk.core.codebase import Codebase + +# Initialize codebase +codebase = Codebase('../../') + +# Print out stats +print("🔍 Codebase Analysis") +print("=" * 50) +print(f"📚 Total Files: {len(codebase.files)}") +print(f"⚡ Total Functions: {len(codebase.functions)}") +print(f"🔄 Total Imports: {len(codebase.imports)}")""".strip(), + } +] + +DEMO_CELLS = [ + ##### [ CODEGEN DEMO ] ##### + { + "cell_type": "markdown", + "source": """# Codegen Demo: FastAPI + +Welcome to [Codegen](https://docs.codegen.com)! + +This demo notebook will walk you through some features of Codegen applied to [FastAPI](https://github.com/fastapi/fastapi). + +See the [getting started](https://docs.codegen.com/introduction/getting-started) guide to learn more.""".strip(), + }, + { + "cell_type": "code", + "source": """from codegen.sdk.core.codebase import Codebase + +# Initialize FastAPI codebase +print('Cloning and parsing FastAPI to /tmp/codegen/fastapi...') +codebase = Codebase.from_repo('fastapi/fastapi', commit="eab0653a346196bff6928710410890a300aee4ae") + +# To initialize a local codebase, use this constructor +# codebase = Codebase("path/to/git/repo")""".strip(), + }, + ##### [ CODEBASE ANALYSIS ] ##### + { + "cell_type": "markdown", + "source": """# Codebase Analysis + +Let's do a quick codebase analysis! + +- Grab codebase content with [codebase.functions](https://docs.codegen.com/building-with-codegen/symbol-api) et al. +- View inheritance hierarchies with [inhertance APIs](https://docs.codegen.com/building-with-codegen/class-api#working-with-inheritance) +- Identify recursive functions by looking at [FunctionCalls](https://docs.codegen.com/building-with-codegen/function-calls-and-callsites)""".strip(), + }, + { + "cell_type": "code", + "source": """# Print overall stats +print("🔍 FastAPI Analysis") +print("=" * 50) +print(f"📚 Total Classes: {len(codebase.classes)}") +print(f"⚡ Total Functions: {len(codebase.functions)}") +print(f"🔄 Total Imports: {len(codebase.imports)}") + +# Find class with most inheritance +if codebase.classes: + deepest_class = max(codebase.classes, key=lambda x: len(x.superclasses)) + print(f"\\n🌳 Class with most inheritance: {deepest_class.name}") + print(f" 📊 Chain Depth: {len(deepest_class.superclasses)}") + print(f" ⛓️ Chain: {' -> '.join(s.name for s in deepest_class.superclasses)}") + +# Find first 5 recursive functions +recursive = [f for f in codebase.functions + if any(call.name == f.name for call in f.function_calls)][:5] +if recursive: + print(f"\\n🔄 Recursive functions:") + for func in recursive: + print(f" - {func.name} ({func.file.filepath})")""".strip(), + }, + ##### [ TEST DRILL DOWN ] ##### + { + "cell_type": "markdown", + "source": """# Drilling Down on Tests + +Let's specifically drill into large test files, which can be cumbersome to manage:""".strip(), + }, + { + "cell_type": "code", + "source": """from collections import Counter + +# Filter to all test functions and classes +test_functions = [x for x in codebase.functions if x.name.startswith('test_')] + +print("🧪 Test Analysis") +print("=" * 50) +print(f"📝 Total Test Functions: {len(test_functions)}") +print(f"📊 Tests per File: {len(test_functions) / len(codebase.files):.1f}") + +# Find files with the most tests +print("\\n📚 Top Test Files by Count") +print("-" * 50) +file_test_counts = Counter([x.file for x in test_functions]) +for file, num_tests in file_test_counts.most_common()[:5]: + print(f"🔍 {num_tests} test functions: {file.filepath}") + print(f" 📏 File Length: {len(file.source.split('\\n'))} lines") + print(f" 💡 Functions: {len(file.functions)}")""".strip(), + }, + ##### [ TEST SPLITTING ] ##### + { + "cell_type": "markdown", + "source": """# Splitting Up Large Test Files + +Lets split up the largest test files into separate modules for better organization. + +This uses Codegen's [codebase.move_to_file(...)](https://docs.codegen.com/building-with-codegen/moving-symbols), which will: +- update all imports +- (optionally) move depenencies +- do so very fast ⚡️ + +While maintaining correctness.""", + }, + ##### [ TEST SPLITTING ] ##### + { + "cell_type": "code", + "source": """filename = 'tests/test_path.py' +print(f"📦 Splitting Test File: {filename}") +print("=" * 50) + +# Grab a file +file = codebase.get_file(filename) +base_name = filename.replace('.py', '') + +# Group tests by subpath +test_groups = {} +for test_function in file.functions: + if test_function.name.startswith('test_'): + test_subpath = '_'.join(test_function.name.split('_')[:3]) + if test_subpath not in test_groups: + test_groups[test_subpath] = [] + test_groups[test_subpath].append(test_function) + +# Print and process each group +for subpath, tests in test_groups.items(): + print(f"\\n{subpath}/") + new_filename = f"{base_name}/{subpath}.py" + + # Create file if it doesn't exist + if not codebase.has_file(new_filename): + new_file = codebase.create_file(new_filename) + file = codebase.get_file(new_filename) + + # Move each test in the group + for test_function in tests: + print(f" - {test_function.name}") + test_function.move_to_file(new_file, strategy="add_back_edge") + +# Commit changes to disk +codebase.commit()""".strip(), + }, + ##### [ RESET ] ##### + { + "cell_type": "markdown", + "source": """## View Changes + +You can now view changes by `cd /tmp/codegen/fastapi && git diff` + +Enjoy! + +# Reset + +Reset your codebase to it's initial state, discarding all changes + +Learn more in [commit and reset](https://docs.codegen.com/building-with-codegen/commit-and-reset).""".strip(), + }, + { + "cell_type": "code", + "source": """codebase.reset()""".strip(), + }, +] + + +def create_cells(cells_data: list[dict[str, str]]) -> list[dict[str, Any]]: + """Convert cell data into Jupyter notebook cell format.""" + return [ + { + "cell_type": cell["cell_type"], + "source": cell["source"], + "metadata": {}, + "execution_count": None, + "outputs": [] if cell["cell_type"] == "code" else None, + } + for cell in cells_data + ] + + +def create_notebook(jupyter_dir: Path, demo: bool = False) -> Path: + """Create a new Jupyter notebook if it doesn't exist. + + Args: + jupyter_dir: Directory where the notebook should be created + demo: Whether to create a demo notebook with FastAPI example code + + Returns: + Path to the created or existing notebook + """ + notebook_path = jupyter_dir / ("demo.ipynb" if demo else "tmp.ipynb") + if not notebook_path.exists(): + cells = create_cells(DEMO_CELLS if demo else DEFAULT_CELLS) + notebook_content = { + "cells": cells, + "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}}, + "nbformat": 4, + "nbformat_minor": 4, + } + notebook_path.write_text(json.dumps(notebook_content, indent=2)) + return notebook_path diff --git a/src/codegen.backup/cli/utils/org.py b/src/codegen.backup/cli/utils/org.py new file mode 100644 index 000000000..4dab35a9c --- /dev/null +++ b/src/codegen.backup/cli/utils/org.py @@ -0,0 +1,117 @@ +"""Organization resolution utilities for CLI commands.""" + +import os +import time + +import requests + +from codegen.cli.api.endpoints import API_ENDPOINT +from codegen.cli.auth.token_manager import ( + get_cached_organizations, + get_current_org_id, + get_current_token, + get_org_name_from_cache, + is_org_id_cached, +) +from codegen.cli.commands.claude.quiet_console import console + +# Cache for org resolution to avoid repeated API calls +_org_cache = {} +_cache_timeout = 300 # 5 minutes + + +def resolve_org_id(explicit_org_id: int | None = None) -> int | None: + """Resolve organization ID with fallback strategy and cache validation. + + Order of precedence: + 1) explicit_org_id passed by the caller (validated against cache) + 2) CODEGEN_ORG_ID environment variable (validated against cache if available) + 3) REPOSITORY_ORG_ID environment variable (validated against cache if available) + 4) stored org ID from auth data (fast, no API call) + 5) API auto-detection (uses first organization from user's organizations) + + Returns None if not found. + """ + global _org_cache + + def _validate_org_id_with_cache(org_id: int, source: str) -> int | None: + """Validate an org ID against the cache and show helpful errors.""" + if is_org_id_cached(org_id): + return org_id + + # If we have a cache but the org ID is not in it, show helpful error + cached_orgs = get_cached_organizations() + if cached_orgs: + org_list = ", ".join([f"{org['name']} ({org['id']})" for org in cached_orgs]) + console.print(f"[red]Error:[/red] Organization ID {org_id} from {source} not found in your accessible organizations.") + console.print(f"[yellow]Available organizations:[/yellow] {org_list}") + return None + + # If no cache available, trust the org ID (will be validated by API) + return org_id + + if explicit_org_id is not None: + return _validate_org_id_with_cache(explicit_org_id, "command line") + + env_val = os.environ.get("CODEGEN_ORG_ID") + if env_val is not None and env_val != "": + try: + env_org_id = int(env_val) + return _validate_org_id_with_cache(env_org_id, "CODEGEN_ORG_ID") + except ValueError: + console.print(f"[red]Error:[/red] Invalid CODEGEN_ORG_ID value: {env_val}") + return None + + # Try repository-scoped org id from .env + repo_org = os.environ.get("REPOSITORY_ORG_ID") + if repo_org: + try: + repo_org_id = int(repo_org) + return _validate_org_id_with_cache(repo_org_id, "REPOSITORY_ORG_ID") + except ValueError: + console.print(f"[red]Error:[/red] Invalid REPOSITORY_ORG_ID value: {repo_org}") + return None + + # Try stored org ID from auth data (fast, no API call) + stored_org_id = get_current_org_id() + if stored_org_id: + return stored_org_id + + # Attempt auto-detection via API: if user belongs to organizations, use the first + try: + token = get_current_token() + if not token: + return None + + # Check cache first + cache_key = f"org_auto_detect_{token[:10]}" # Use first 10 chars as key + current_time = time.time() + + if cache_key in _org_cache: + cached_data, cache_time = _org_cache[cache_key] + if current_time - cache_time < _cache_timeout: + return cached_data + + headers = {"Authorization": f"Bearer {token}"} + url = f"{API_ENDPOINT.rstrip('/')}/v1/organizations" + resp = requests.get(url, headers=headers, timeout=10) + resp.raise_for_status() + data = resp.json() + items = data.get("items") or [] + + org_id = None + if isinstance(items, list) and len(items) >= 1: + org = items[0] + org_id_raw = org.get("id") + try: + org_id = int(org_id_raw) + except Exception: + org_id = None + + # Cache the result + _org_cache[cache_key] = (org_id, current_time) + return org_id + + except Exception as e: + console.print(f"Error during organization auto-detection: {e}") + return None diff --git a/src/codegen.backup/cli/utils/repo.py b/src/codegen.backup/cli/utils/repo.py new file mode 100644 index 000000000..5e0149af0 --- /dev/null +++ b/src/codegen.backup/cli/utils/repo.py @@ -0,0 +1,236 @@ +"""Repository utilities for managing repository ID resolution and environment variables.""" + +import os +from typing import Dict, List, Any + +from rich.console import Console + +console = Console() + + +def resolve_repo_id(explicit_repo_id: int | None = None) -> int | None: + """Resolve repository ID with fallback strategy. + + Order of precedence: + 1) explicit_repo_id passed by the caller + 2) CODEGEN_REPO_ID environment variable + 3) REPOSITORY_ID environment variable + + Returns None if not found. + """ + if explicit_repo_id is not None: + return explicit_repo_id + + # Check CODEGEN_REPO_ID environment variable + env_val = os.environ.get("CODEGEN_REPO_ID") + if env_val is not None and env_val != "": + try: + return int(env_val) + except ValueError: + console.print(f"[red]Error:[/red] Invalid CODEGEN_REPO_ID value: {env_val}") + return None + + # Check REPOSITORY_ID environment variable + repo_id_env = os.environ.get("REPOSITORY_ID") + if repo_id_env is not None and repo_id_env != "": + try: + return int(repo_id_env) + except ValueError: + console.print(f"[red]Error:[/red] Invalid REPOSITORY_ID value: {repo_id_env}") + return None + + return None + + +def get_current_repo_id() -> int | None: + """Get the current repository ID from environment variables.""" + return resolve_repo_id() + + +def get_repo_env_status() -> Dict[str, str]: + """Get the status of repository-related environment variables.""" + return { + "CODEGEN_REPO_ID": os.environ.get("CODEGEN_REPO_ID", "Not set"), + "REPOSITORY_ID": os.environ.get("REPOSITORY_ID", "Not set"), + } + + +def set_repo_env_variable(repo_id: int, var_name: str = "CODEGEN_REPO_ID") -> bool: + """Set repository ID in environment variable. + + Args: + repo_id: Repository ID to set + var_name: Environment variable name (default: CODEGEN_REPO_ID) + + Returns: + True if successful, False otherwise + """ + try: + os.environ[var_name] = str(repo_id) + return True + except Exception as e: + console.print(f"[red]Error setting {var_name}:[/red] {e}") + return False + + +def clear_repo_env_variables() -> None: + """Clear all repository-related environment variables.""" + env_vars = ["CODEGEN_REPO_ID", "REPOSITORY_ID"] + for var in env_vars: + if var in os.environ: + del os.environ[var] + + +def update_env_file_with_repo(repo_id: int, env_file_path: str = ".env") -> bool: + """Update .env file with repository ID.""" + try: + lines = [] + key_updated = False + key_to_update = "CODEGEN_REPO_ID" + + # Read existing .env file if it exists + if os.path.exists(env_file_path): + with open(env_file_path, "r") as f: + lines = f.readlines() + + # Update or add the key + for i, line in enumerate(lines): + if line.strip().startswith(f"{key_to_update}="): + lines[i] = f"{key_to_update}={repo_id}\n" + key_updated = True + break + + # If key wasn't found, add it + if not key_updated: + if lines and not lines[-1].endswith('\n'): + lines.append('\n') + lines.append(f"{key_to_update}={repo_id}\n") + + # Write back to file + with open(env_file_path, "w") as f: + f.writelines(lines) + + return True + + except Exception as e: + console.print(f"[red]Error updating .env file:[/red] {e}") + return False + + +def get_repo_display_info() -> List[Dict[str, str]]: + """Get repository information for display in TUI.""" + repo_id = get_current_repo_id() + env_status = get_repo_env_status() + + info = [] + + # Current repository ID + if repo_id: + info.append({ + "label": "Current Repository ID", + "value": str(repo_id), + "status": "active" + }) + else: + info.append({ + "label": "Current Repository ID", + "value": "Not configured", + "status": "inactive" + }) + + # Environment variables status + for var_name, value in env_status.items(): + info.append({ + "label": f"{var_name}", + "value": value, + "status": "active" if value != "Not set" else "inactive" + }) + + return info + + +def fetch_repositories_for_org(org_id: int) -> List[Dict[str, Any]]: + """Fetch repositories for an organization. + + Args: + org_id: Organization ID to fetch repositories for + + Returns: + List of repository dictionaries + """ + try: + import requests + from codegen.cli.api.endpoints import API_ENDPOINT + from codegen.cli.auth.token_manager import get_current_token + + token = get_current_token() + if not token: + return [] + + headers = {"Authorization": f"Bearer {token}"} + + # Try the repository endpoint (may not exist yet) + url = f"{API_ENDPOINT.rstrip('/')}/v1/organizations/{org_id}/repositories" + response = requests.get(url, headers=headers) + + if response.status_code == 200: + data = response.json() + return data.get("items", []) + else: + # API endpoint doesn't exist yet, return mock data for demo + return get_mock_repositories() + + except Exception: + # If API fails, return mock data + return get_mock_repositories() + + +def get_mock_repositories() -> List[Dict[str, Any]]: + """Get mock repository data for demonstration. + + Returns: + List of mock repository dictionaries + """ + return [ + {"id": 1, "name": "codegen-sdk", "description": "Codegen SDK repository"}, + {"id": 2, "name": "web-frontend", "description": "Frontend web application"}, + {"id": 3, "name": "api-backend", "description": "Backend API service"}, + {"id": 4, "name": "mobile-app", "description": "Mobile application"}, + {"id": 5, "name": "docs-site", "description": "Documentation website"}, + {"id": 6, "name": "cli-tools", "description": "Command line tools"}, + {"id": 7, "name": "data-pipeline", "description": "Data processing pipeline"}, + {"id": 8, "name": "ml-models", "description": "Machine learning models"}, + ] + + +def ensure_repositories_cached(org_id: int | None = None) -> List[Dict[str, Any]]: + """Ensure repositories are cached for the given organization. + + Args: + org_id: Organization ID (will resolve if not provided) + + Returns: + List of cached repositories + """ + from codegen.cli.auth.token_manager import get_cached_repositories, cache_repositories + from codegen.cli.utils.org import resolve_org_id + + # Get cached repositories first + cached_repos = get_cached_repositories() + if cached_repos: + return cached_repos + + # If no cache, try to fetch from API + if org_id is None: + org_id = resolve_org_id() + + if org_id: + repositories = fetch_repositories_for_org(org_id) + if repositories: + cache_repositories(repositories) + return repositories + + # Fallback to mock data + mock_repos = get_mock_repositories() + cache_repositories(mock_repos) + return mock_repos \ No newline at end of file diff --git a/src/codegen.backup/cli/utils/schema.ipynb b/src/codegen.backup/cli/utils/schema.ipynb new file mode 100644 index 000000000..0a2d34807 --- /dev/null +++ b/src/codegen.backup/cli/utils/schema.ipynb @@ -0,0 +1,74 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# with model that should validate\n", + "from codegen.cli.api.schemas import RunCodemodOutput\n", + "\n", + "\n", + "res = RunCodemodOutput.model_validate(\n", + " {\n", + " \"success\": True,\n", + " \"web_link\": \"https://chadcode.sh/codemod/13264/code/21435/run/28349/playground/diff\",\n", + " \"logs\": \"\",\n", + " \"observation\": 'diff --git a/codegen-backend/app/utils/slack/interactive/handlers/BaseSlackActionHandler.py b/codegen-backend/app/utils/slack/interactive/handlers/BaseSlackActionHandler.py\\nindex 2dce963196b418caad011d175fb3110e03a8fe73..77c430a5acfb1613ac42c5712cc2859f31d7d773 100644\\n--- a/codegen-backend/app/utils/slack/interactive/handlers/BaseSlackActionHandler.py\\n+++ b/codegen-backend/app/utils/slack/interactive/handlers/BaseSlackActionHandler.py\\n@@ -1,12 +1,1 @@\\n-from sqlalchemy.orm import Session\\n-\\n-\\n-class BaseSlackInteractiveActionHandler:\\n- db: Session\\n-\\n- def __init__(self, db: Session) -> None:\\n- self.db = db\\n-\\n- # TODO: convert the message information into a shared schema\\n- def process_action(self, action_payload: str, channel_id: str, thread_ts: str):\\n- raise NotImplementedError(f\"process_action not implemented for {self.__class__.__name__}!\")\\n+update\\n\\\\ No newline at end of file\\n',\n", + " },\n", + ")\n", + "print(res)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# with model that should validate\n", + "from codegen.cli.api.schemas import RunCodemodOutput\n", + "\n", + "\n", + "res = RunCodemodOutput.model_validate(\n", + " {\n", + " \"success\": \"testz\",\n", + " \"web_link\": \"https://chadcode.sh/codemod/13264/code/21435/run/28349/playground/diff\",\n", + " \"logs\": \"\",\n", + " \"observation\": 'diff --git a/codegen-backend/app/utils/slack/interactive/handlers/BaseSlackActionHandler.py b/codegen-backend/app/utils/slack/interactive/handlers/BaseSlackActionHandler.py\\nindex 2dce963196b418caad011d175fb3110e03a8fe73..77c430a5acfb1613ac42c5712cc2859f31d7d773 100644\\n--- a/codegen-backend/app/utils/slack/interactive/handlers/BaseSlackActionHandler.py\\n+++ b/codegen-backend/app/utils/slack/interactive/handlers/BaseSlackActionHandler.py\\n@@ -1,12 +1,1 @@\\n-from sqlalchemy.orm import Session\\n-\\n-\\n-class BaseSlackInteractiveActionHandler:\\n- db: Session\\n-\\n- def __init__(self, db: Session) -> None:\\n- self.db = db\\n-\\n- # TODO: convert the message information into a shared schema\\n- def process_action(self, action_payload: str, channel_id: str, thread_ts: str):\\n- raise NotImplementedError(f\"process_action not implemented for {self.__class__.__name__}!\")\\n+update\\n\\\\ No newline at end of file\\n',\n", + " },\n", + ")\n", + "print(res)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.0" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/codegen.backup/cli/utils/schema.py b/src/codegen.backup/cli/utils/schema.py new file mode 100644 index 000000000..6187166d5 --- /dev/null +++ b/src/codegen.backup/cli/utils/schema.py @@ -0,0 +1,29 @@ +from typing import Any, Self + +from pydantic import BaseModel + + +class SafeBaseModel(BaseModel): + @classmethod + def model_validate( + cls, obj: Any, *, strict: bool | None = None, from_attributes: bool | None = None, context: Any | None = None, by_alias: bool | None = None, by_name: bool | None = None + ) -> "Self": + try: + return super().model_validate(obj, strict=strict, from_attributes=from_attributes, context=context, by_alias=by_alias, by_name=by_name) + except Exception as e: + print(e) + # Return a default instance instead of None to maintain compatibility + return cls() + + def __str__(self) -> str: + return self.model_dump_json(indent=4) + + +class CodemodConfig(BaseModel): + """Configuration for a codemod.""" + + name: str + codemod_id: int + description: str | None = None + created_at: str + created_by: str diff --git a/src/codegen.backup/cli/utils/simple_selector.py b/src/codegen.backup/cli/utils/simple_selector.py new file mode 100644 index 000000000..575a1149a --- /dev/null +++ b/src/codegen.backup/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.backup/cli/utils/url.py b/src/codegen.backup/cli/utils/url.py new file mode 100644 index 000000000..ba9a9648f --- /dev/null +++ b/src/codegen.backup/cli/utils/url.py @@ -0,0 +1,57 @@ +from enum import Enum + +from codegen.cli.env.enums import Environment +from codegen.cli.env.global_env import global_env + + +class DomainRegistry(Enum): + STAGING = "chadcode.sh" + PRODUCTION = "codegen.com" + LOCAL = "localhost:3000" + + +def get_domain() -> str: + """Get the appropriate domain based on the current environment.""" + if global_env.ENV == Environment.PRODUCTION: + return DomainRegistry.PRODUCTION.value + elif global_env.ENV == Environment.STAGING: + return DomainRegistry.STAGING.value + else: + return DomainRegistry.LOCAL.value + + +def generate_webapp_url(path: str = "", params: dict | None = None, protocol: str = "https") -> str: + """Generate a complete URL for the web application. + + Args: + path: The path component of the URL (without leading slash) + params: Optional query parameters as a dictionary + protocol: URL protocol (defaults to https, will be http for localhost) + + Returns: + Complete URL string + + Example: + generate_webapp_url("projects/123", {"tab": "settings"}) + # In staging: https://chadcode.sh/projects/123?tab=settings + + """ + domain = get_domain() + # Use http for localhost + if domain == DomainRegistry.LOCAL.value: + protocol = "http" + + # Build base URL + base_url = f"{protocol}://{domain}" + + # Add path if provided + if path: + path = path.lstrip("/") # Remove leading slash if present + base_url = f"{base_url}/{path}" + + # Add query parameters if provided + if params: + query_string = "&".join(f"{k}={v}" for k, v in params.items()) + base_url = f"{base_url}?{query_string}" + + return base_url diff --git a/src/codegen.backup/compat.py b/src/codegen.backup/compat.py new file mode 100644 index 000000000..89b36e93e --- /dev/null +++ b/src/codegen.backup/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.backup/configs/constants.py b/src/codegen.backup/configs/constants.py new file mode 100644 index 000000000..047aeb07c --- /dev/null +++ b/src/codegen.backup/configs/constants.py @@ -0,0 +1,20 @@ +from pathlib import Path + +CODEGEN_DIR_NAME = ".codegen" +ENV_FILENAME = ".env" + +# ====[ Codegen internal config ]==== +CODEGEN_REPO_ROOT = Path(__file__).parent.parent.parent.parent.parent +CODEGEN_DIR_PATH = CODEGEN_REPO_ROOT / CODEGEN_DIR_NAME + +# ====[ User session config ]==== +PROMPTS_DIR = Path(CODEGEN_DIR_NAME) / "prompts" +DOCS_DIR = Path(CODEGEN_DIR_NAME) / "docs" +EXAMPLES_DIR = Path(CODEGEN_DIR_NAME) / "examples" + + +# ====[ User global config paths ]==== +GLOBAL_CONFIG_DIR = Path("~/.config/codegen-sh").expanduser() +AUTH_FILE = GLOBAL_CONFIG_DIR / "auth.json" +SESSION_FILE = GLOBAL_CONFIG_DIR / "session.json" +GLOBAL_ENV_FILE = GLOBAL_CONFIG_DIR / ENV_FILENAME diff --git a/src/codegen.backup/configs/models/base_config.py b/src/codegen.backup/configs/models/base_config.py new file mode 100644 index 000000000..3a82223de --- /dev/null +++ b/src/codegen.backup/configs/models/base_config.py @@ -0,0 +1,57 @@ +from abc import ABC +from pathlib import Path + +from dotenv import load_dotenv, set_key +from pydantic_settings import BaseSettings, SettingsConfigDict + +from codegen.configs.constants import ENV_FILENAME, GLOBAL_ENV_FILE +from codegen.shared.path import get_git_root_path + + +class BaseConfig(BaseSettings, ABC): + """Base class for all config classes. + Handles loading and saving of configuration values from environment files. + Supports both global and local config files. + """ + + model_config = SettingsConfigDict(extra="ignore", case_sensitive=False) + + def __init__(self, prefix: str, env_filepath: Path | None = None, *args, **kwargs) -> None: + if env_filepath is None: + root_path = get_git_root_path() + if root_path is not None: + env_filepath = root_path / ENV_FILENAME + + # Only include env files that exist + if GLOBAL_ENV_FILE.exists(): + load_dotenv(GLOBAL_ENV_FILE, override=True) + + if env_filepath and env_filepath.exists() and env_filepath != GLOBAL_ENV_FILE: + load_dotenv(env_filepath, override=True) + + self.model_config["env_prefix"] = f"{prefix.upper()}_" if len(prefix) > 0 else "" + super().__init__(*args, **kwargs) + + @property + def env_prefix(self) -> str: + return self.model_config["env_prefix"] + + def set(self, env_filepath: Path, key: str, value: str) -> None: + """Update configuration values""" + if key.lower() in self.model_fields: + setattr(self, key.lower(), value) + set_key(env_filepath, f"{self.model_config['env_prefix']}{key.upper()}", str(value)) + + def write_to_file(self, env_filepath: Path) -> None: + """Dump environment variables to a file""" + env_filepath.parent.mkdir(parents=True, exist_ok=True) + + if not env_filepath.exists(): + with open(env_filepath, "w") as f: + f.write("") + + # Update with new values + for key, value in self.model_dump().items(): + if value is None: + continue + set_key(env_filepath, f"{self.model_config['env_prefix']}{key.upper()}", str(value)) diff --git a/src/codegen.backup/configs/models/codebase.py b/src/codegen.backup/configs/models/codebase.py new file mode 100644 index 000000000..3f5c0a4e3 --- /dev/null +++ b/src/codegen.backup/configs/models/codebase.py @@ -0,0 +1,43 @@ +from enum import IntEnum, auto + +from pydantic import Field + +from codegen.configs.models.base_config import BaseConfig + + +class PinkMode(IntEnum): + # Use the python SDK for all files + OFF = auto() + # Use the Rust SDK for all files. Make sure to install the pink extra + ALL_FILES = auto() + # Use the Rust SDK for files the python SDK can't parse (non-source files). Make sure to install the pink extra + NON_SOURCE_FILES = auto() + + +class CodebaseConfig(BaseConfig): + def __init__(self, prefix: str = "CODEBASE", *args, **kwargs) -> None: + super().__init__(prefix=prefix, *args, **kwargs) + + debug: bool = False + verify_graph: bool = False + track_graph: bool = False + method_usages: bool = True + sync_enabled: bool = False + full_range_index: bool = False + ignore_process_errors: bool = True + disable_graph: bool = False + disable_file_parse: bool = False + exp_lazy_graph: bool = False + generics: bool = True + import_resolution_paths: list[str] = Field(default_factory=lambda: []) + import_resolution_overrides: dict[str, str] = Field(default_factory=lambda: {}) + py_resolve_syspath: bool = False + allow_external: bool = False + ts_dependency_manager: bool = False + ts_language_engine: bool = False + v8_ts_engine: bool = False + unpacking_assignment_partial_removal: bool = True + use_pink: PinkMode = PinkMode.OFF + + +DefaultCodebaseConfig = CodebaseConfig() diff --git a/src/codegen.backup/configs/models/repository.py b/src/codegen.backup/configs/models/repository.py new file mode 100644 index 000000000..d4960c503 --- /dev/null +++ b/src/codegen.backup/configs/models/repository.py @@ -0,0 +1,39 @@ +import os + +from codegen.configs.models.base_config import BaseConfig + + +class RepositoryConfig(BaseConfig): + """Configuration for the repository context to run codegen. + To automatically populate this config, call `codegen init` from within a git repository. + """ + + path: str | None = None + owner: str | None = None + language: str | None = None + user_name: str | None = None + user_email: str | None = None + + def __init__(self, prefix: str = "REPOSITORY", *args, **kwargs) -> None: + super().__init__(prefix=prefix, *args, **kwargs) + + def _initialize( + self, + ) -> None: + """Initialize the repository config""" + if self.path is None: + self.path = os.getcwd() + + @property + def base_dir(self) -> str: + return os.path.dirname(self.path) + + @property + def name(self) -> str: + return os.path.basename(self.path) + + @property + def full_name(self) -> str | None: + if self.owner is not None: + return f"{self.owner}/{self.name}" + return None diff --git a/src/codegen.backup/configs/models/secrets.py b/src/codegen.backup/configs/models/secrets.py new file mode 100644 index 000000000..208999ebe --- /dev/null +++ b/src/codegen.backup/configs/models/secrets.py @@ -0,0 +1,16 @@ +from codegen.configs.models.base_config import BaseConfig + + +class SecretsConfig(BaseConfig): + """Configuration for various API secrets and tokens. + + Loads from environment variables. + Falls back to .env file for missing values. + """ + + def __init__(self, prefix: str = "", *args, **kwargs) -> None: + super().__init__(prefix=prefix, *args, **kwargs) + + github_token: str | None = None + openai_api_key: str | None = None + linear_api_key: str | None = None diff --git a/src/codegen.backup/configs/models/telemetry.py b/src/codegen.backup/configs/models/telemetry.py new file mode 100644 index 000000000..6a4173996 --- /dev/null +++ b/src/codegen.backup/configs/models/telemetry.py @@ -0,0 +1,29 @@ +"""Telemetry configuration for CLI usage analytics and debugging.""" + +from codegen.configs.models.base_config import BaseConfig + + +class TelemetryConfig(BaseConfig): + """Configuration for CLI telemetry. + + Telemetry is opt-in by default and helps improve the CLI experience + by collecting usage analytics, performance metrics, and error diagnostics. + """ + + # Whether telemetry is enabled (opt-in by default) + enabled: bool = False + + # Whether user has been prompted for telemetry consent + consent_prompted: bool = False + + # Anonymous user ID for telemetry correlation + anonymous_id: str | None = None + + # Telemetry endpoint (defaults to production collector) + endpoint: str | None = None + + # Debug mode for verbose telemetry logging + debug: bool = False + + def __init__(self, env_filepath=None, **kwargs): + super().__init__(prefix="TELEMETRY", env_filepath=env_filepath, **kwargs) diff --git a/src/codegen.backup/configs/models/utils.py b/src/codegen.backup/configs/models/utils.py new file mode 100644 index 000000000..97e237bee --- /dev/null +++ b/src/codegen.backup/configs/models/utils.py @@ -0,0 +1,9 @@ +from pydantic_settings import SettingsConfigDict + + +def get_setting_config(prefix: str) -> SettingsConfigDict: + return SettingsConfigDict( + env_prefix=f"{prefix}_", + case_sensitive=False, + extra="ignore", + ) diff --git a/src/codegen.backup/configs/session_manager.py b/src/codegen.backup/configs/session_manager.py new file mode 100644 index 000000000..f69a9b479 --- /dev/null +++ b/src/codegen.backup/configs/session_manager.py @@ -0,0 +1,64 @@ +"""Global config to manage different codegen sessions, as well as user auth.""" + +import json +from pathlib import Path + +from codegen.configs.constants import SESSION_FILE + + +class SessionManager: + active_session_path: str | None + sessions: list[str] + + def __init__(self, **kwargs) -> None: + if SESSION_FILE.exists(): + with open(SESSION_FILE) as f: + json_config = json.load(f) + self.sessions = json_config["sessions"] + self.active_session_path = json_config["active_session_path"] + else: + self.sessions = [] + self.active_session_path = None + super().__init__(**kwargs) + + def get_session(self, session_root_path: Path) -> str | None: + return next((s for s in self.sessions if s == str(session_root_path)), None) + + def get_active_session(self) -> Path | None: + if not self.active_session_path: + return None + + return Path(self.active_session_path) + + def set_active_session(self, session_root_path: Path) -> None: + if not session_root_path.exists(): + msg = f"Session path does not exist: {session_root_path}" + raise ValueError(msg) + + self.active_session_path = str(session_root_path) + if self.active_session_path not in self.sessions: + self.sessions.append(self.active_session_path) + + self.save() + + def save(self) -> None: + if not SESSION_FILE.parent.exists(): + SESSION_FILE.parent.mkdir(parents=True, exist_ok=True) + + with open(SESSION_FILE, "w") as f: + json.dump(self.__dict__(), f) + + def __dict__(self) -> dict: + return { + "active_session_path": self.active_session_path, + "sessions": self.sessions, + } + + def __str__(self) -> str: + active = self.active_session_path or "None" + sessions_str = "\n ".join(self.sessions) if self.sessions else "None" + + return f"GlobalConfig:\n Active Session: {active}\n Sessions:\n {sessions_str}\n Global Session:\n {self.session_config}" + + +session_manager = SessionManager() diff --git a/src/codegen.backup/configs/user_config.py b/src/codegen.backup/configs/user_config.py new file mode 100644 index 000000000..ecebec4d1 --- /dev/null +++ b/src/codegen.backup/configs/user_config.py @@ -0,0 +1,69 @@ +import json +from pathlib import Path + +from pydantic import Field + +from codegen.configs.models.codebase import CodebaseConfig +from codegen.configs.models.repository import RepositoryConfig +from codegen.configs.models.secrets import SecretsConfig + + +class UserConfig: + env_filepath: Path + repository: RepositoryConfig = Field(default_factory=RepositoryConfig) + codebase: CodebaseConfig = Field(default_factory=CodebaseConfig) + secrets: SecretsConfig = Field(default_factory=SecretsConfig) + + def __init__(self, env_filepath: Path) -> None: + self.env_filepath = env_filepath + self.secrets = SecretsConfig(env_filepath=env_filepath) + self.repository = RepositoryConfig(env_filepath=env_filepath) + self.codebase = CodebaseConfig(env_filepath=env_filepath) + + def save(self) -> None: + """Save configuration to the config file.""" + self.env_filepath.parent.mkdir(parents=True, exist_ok=True) + self.repository.write_to_file(self.env_filepath) + self.secrets.write_to_file(self.env_filepath) + self.codebase.write_to_file(self.env_filepath) + + def to_dict(self) -> dict: + """Return a dictionary representation of the config.""" + config_dict = {} + # Add repository configs with 'repository_' prefix + for key, value in self.repository.model_dump().items(): + config_dict[f"{self.repository.env_prefix}{key}".upper()] = value + + # Add feature flags configs with 'feature_flags_' prefix + for key, value in self.codebase.model_dump().items(): + config_dict[f"{self.codebase.env_prefix}{key}".upper()] = value + + # Add secrets configs + for key, value in self.secrets.model_dump().items(): + config_dict[f"{self.secrets.env_prefix}{key}".upper()] = value + return config_dict + + def has_key(self, full_key: str) -> bool: + """Check if a configuration key exists""" + return full_key.upper() in self.to_dict() + + def get(self, full_key: str) -> str | None: + """Get a configuration value""" + return self.to_dict().get(full_key.upper(), None) + + def set(self, full_key: str, value: str) -> None: + """Update a configuration value and save it to the .env file.""" + key_segments = full_key.split("_") + prefix = key_segments[0].upper() + key = "_".join(key_segments[1:]) + match f"{prefix}_": + case self.repository.env_prefix: + self.repository.set(self.env_filepath, key, value) + case self.codebase.env_prefix: + self.codebase.set(self.env_filepath, key, value) + case _: + self.secrets.set(self.env_filepath, full_key, value) + + def __str__(self) -> str: + """Return a pretty-printed string representation of the config.""" + return json.dumps(self.to_dict(), indent=2) diff --git a/src/codegen.backup/exports.py b/src/codegen.backup/exports.py new file mode 100644 index 000000000..fe9bba50c --- /dev/null +++ b/src/codegen.backup/exports.py @@ -0,0 +1,18 @@ +"""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. +""" + +from codegen.agents.agent import Agent +from codegen.sdk.core.codebase import Codebase # type: ignore[import-untyped] +from codegen.sdk.core.function import Function # type: ignore[import-untyped] +from codegen.shared.enums.programming_language import ProgrammingLanguage + +__all__ = [ + "Agent", + "Codebase", + "Function", + "ProgrammingLanguage", +] diff --git a/src/codegen.backup/git/README.md b/src/codegen.backup/git/README.md new file mode 100644 index 000000000..c2e4d7e84 --- /dev/null +++ b/src/codegen.backup/git/README.md @@ -0,0 +1,8 @@ +# Codegen Git + +A codegen module to supports git operations on codebase. + +### Dependencies + +- [codegen.sdk](https://github.com/codegen-sh/codegen-sdk/tree/develop/src/codegen/sdk) +- [codegen.shared](https://github.com/codegen-sh/codegen-sdk/tree/develop/src/codegen/shared) diff --git a/src/codegen.backup/git/__init__.py b/src/codegen.backup/git/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/codegen.backup/git/clients/git_repo_client.py b/src/codegen.backup/git/clients/git_repo_client.py new file mode 100644 index 000000000..1da2eebf7 --- /dev/null +++ b/src/codegen.backup/git/clients/git_repo_client.py @@ -0,0 +1,463 @@ +import time +from datetime import UTC, datetime + +from github.Branch import Branch +from github.CheckRun import CheckRun +from github.CheckSuite import CheckSuite +from github.Commit import Commit +from github.GithubException import GithubException, UnknownObjectException +from github.GithubObject import NotSet, Opt +from github.Issue import Issue +from github.IssueComment import IssueComment +from github.Label import Label +from github.PullRequest import PullRequest +from github.Repository import Repository +from github.Tag import Tag +from github.Workflow import Workflow + +from codegen.configs.models.secrets import SecretsConfig +from codegen.git.clients.github_client import GithubClient +from codegen.git.schemas.repo_config import RepoConfig +from codegen.git.utils.format import format_comparison +from codegen.shared.logging.get_logger import get_logger + +logger = get_logger(__name__) + + +class GitRepoClient: + """Wrapper around PyGithub's Remote Repository.""" + + repo_config: RepoConfig + gh_client: GithubClient + _repo: Repository + + def __init__(self, repo_config: RepoConfig, access_token: str | None = None) -> None: + self.repo_config = repo_config + self.gh_client = self._create_github_client(token=access_token or SecretsConfig().github_token) + self._repo = self._create_client() + + def _create_github_client(self, token: str) -> GithubClient: + return GithubClient(token=token) + + def _create_client(self) -> Repository: + client = self.gh_client.get_repo_by_full_name(self.repo_config.full_name) + if not client: + msg = f"Repo {self.repo_config.full_name} not found!" + raise ValueError(msg) + return client + + @property + def repo(self) -> Repository: + return self._repo + + #################################################################################################################### + # PROPERTIES + #################################################################################################################### + + @property + def default_branch(self) -> str: + return self.repo.default_branch + + #################################################################################################################### + # CONTENTS + #################################################################################################################### + + def get_contents(self, file_path: str, ref: str | None = None) -> str | None: + """Returns string file content on a given ref""" + if not ref: + ref = self.default_branch + try: + file = self.repo.get_contents(file_path, ref=ref) + file_contents = file.decoded_content.decode("utf-8") # type: ignore[union-attr] + return file_contents + except UnknownObjectException: + logger.info(f"File: {file_path} not found in ref: {ref}") + return None + except GithubException as e: + if e.status == 404: + logger.info(f"File: {file_path} not found in ref: {ref}") + return None + raise + + def get_last_modified_date_of_path(self, path: str) -> datetime: + """Uses the GitHub API to return the last modified date of a given directory or file. + + Args: + ---- + path (str): The path to the directory within the repository. + + Returns: + ------- + str: The last modified date of the directory in ISO format (YYYY-MM-DDTHH:MM:SSZ). + + """ + commits = self.repo.get_commits(path=path) + if commits.totalCount > 0: + # Get the date of the latest commit + last_modified_date = commits[0].commit.committer.date + return last_modified_date + else: + print("Directory has not been modified or does not exist.") + return datetime.min.replace(tzinfo=UTC) + + #################################################################################################################### + # COMMENTS + #################################################################################################################### + + def create_review_comment( + self, + pull: PullRequest, + body: str, + commit: Commit, + path: str, + line: Opt[int] = NotSet, + side: Opt[str] = NotSet, + start_line: Opt[int] = NotSet, + ) -> None: + # TODO: add protections (ex: can write to PR) + writeable_pr = self.repo.get_pull(pull.number) + writeable_pr.create_review_comment( + body=body, + commit=commit, + path=path, + line=line, + side=side, + start_line=start_line, + ) + + def create_issue_comment( + self, + pull: PullRequest, + body: str, + ) -> IssueComment: + # TODO: add protections (ex: can write to PR) + writeable_pr = self.repo.get_pull(pull.number) + return writeable_pr.create_issue_comment(body=body) + + #################################################################################################################### + # PULL REQUESTS + #################################################################################################################### + + def get_pull_by_branch_and_state( + self, + head_branch_name: str | None = None, + base_branch_name: str | None = None, + state: str = "all", + ) -> PullRequest | None: + """Returns the first PR for the head/base/state filter""" + if not head_branch_name: + logger.info("No head branch name provided. Unable to find PR.") + return None + if not base_branch_name: + base_branch_name = self.default_branch + + head_branch_name = f"{self.repo_config.organization_name}:{head_branch_name}" + + # retrieve all pulls ordered by created descending + prs = self.repo.get_pulls(base=base_branch_name, head=head_branch_name, state=state, sort="created", direction="desc") + if prs.totalCount > 0: + return prs[0] + else: + return None + + def get_pull_safe(self, number: int) -> PullRequest | None: + """Returns a PR by its number + TODO: catching UnknownObjectException is common enough to create a decorator + """ + try: + pr = self.repo.get_pull(number) + return pr + except UnknownObjectException as e: + return None + except Exception as e: + logger.warning(f"Error getting PR by number: {number}\n\t{e}") + return None + + def get_issue_safe(self, number: int) -> Issue | None: + """Returns an issue by its number + TODO: catching UnknownObjectException is common enough to create a decorator + """ + try: + pr = self.repo.get_issue(number) + return pr + except UnknownObjectException as e: + return None + except Exception as e: + logger.warning(f"Error getting issue by number: {number}\n\t{e}") + return None + + def get_or_create_pull( + self, + head_branch_name: str, + base_branch_name: str | None = None, # type: ignore[assignment] + title: str | None = None, # type: ignore[assignment] + body: str | None = None, # type: ignore[assignment] + ) -> PullRequest | None: + pull = self.get_pull_by_branch_and_state(head_branch_name=head_branch_name, base_branch_name=base_branch_name) + if pull: + logger.info(f"Pull request for head branch: {head_branch_name} already exists. Skip creation.") + else: + logger.info(f"Creating pull request base: {base_branch_name} head: {head_branch_name} ...") + pull = self.create_pull(head_branch_name=head_branch_name, base_branch_name=base_branch_name, title=title, body=body) + return pull + + def create_pull( + self, + head_branch_name: str, + base_branch_name: str | None = None, + title: str | None = None, + body: str | None = None, + draft: bool = True, + ) -> PullRequest | None: + if base_branch_name is None: + base_branch_name = self.default_branch + + # draft PRs are not supported on all private repos + # TODO: check repo plan features instead of this heuristic + if self.repo.visibility == "private": + logger.info(f"Repo {self.repo.name} is private. Disabling draft PRs.") + draft = False + + try: + pr = self.repo.create_pull(title=title or f"Draft PR for {head_branch_name}", body=body or "", head=head_branch_name, base=base_branch_name, draft=draft) + logger.info(f"Created pull request for head branch: {head_branch_name} at {pr.html_url}") + # NOTE: return a read-only copy to prevent people from editing it + return self.repo.get_pull(pr.number) + except GithubException as ge: + logger.warning(f"Failed to create PR got GithubException\n\t{ge}") + except Exception as e: + logger.warning(f"Failed to create PR:\n\t{e}") + + return None + + def squash_and_merge(self, base_branch_name: str, head_branch_name: str, squash_commit_msg: str | None = None, squash_commit_title: str | None = None) -> None: + # =====[ Step 1: Make a squash PR ]===== + # We will do a squash merge via a pull request, since regular + # merges in PyGithub do not support `squash` + squash_pr = self.create_pull( + base_branch_name=base_branch_name, + head_branch_name=head_branch_name, + draft=False, + title=squash_commit_title, + body="", + ) + # TODO: handle PR not mergeable due to merge conflicts + merge = squash_pr.merge(commit_message=squash_commit_msg, commit_title=squash_commit_title, merge_method="squash") # type: ignore[arg-type] + + def edit_pull(self, pull: PullRequest, title: Opt[str] = NotSet, body: Opt[str] = NotSet, state: Opt[str] = NotSet) -> None: + writable_pr = self.repo.get_pull(pull.number) + writable_pr.edit(title=title, body=body, state=state) + + def add_label_to_pull(self, pull: PullRequest, label: Label) -> None: + writeable_pr = self.repo.get_pull(pull.number) + writeable_pr.add_to_labels(label) + + def remove_label_from_pull(self, pull: PullRequest, label: Label) -> None: + writeable_pr = self.repo.get_pull(pull.number) + writeable_pr.remove_from_labels(label) + + #################################################################################################################### + # BRANCHES + #################################################################################################################### + + def get_or_create_branch(self, new_branch_name: str, base_branch_name: str | None = None) -> Branch | None: + try: + existing_branch = self.get_branch_safe(new_branch_name) + if existing_branch: + return existing_branch + new_branch = self.create_branch(new_branch_name, base_branch_name=base_branch_name) + return new_branch + except Exception as e: + logger.exception(f"Unexpected error creating branch: {new_branch_name}\n\t{e}") + return None + + def get_branch_safe(self, branch_name: str, attempts: int = 1, wait_seconds: int = 1) -> Branch | None: + for i in range(attempts): + try: + return self.repo.get_branch(branch_name) + except GithubException as e: + if e.status == 404 and i < attempts - 1: + time.sleep(wait_seconds) + except Exception as e: + logger.warning(f"Unexpected error getting branch: {branch_name}\n\t{e}") + return None + + def create_branch(self, new_branch_name: str, base_branch_name: str | None = None) -> Branch | None: + if base_branch_name is None: + base_branch_name = self.default_branch + + base_branch = self.repo.get_branch(base_branch_name) + # TODO: also wrap git ref. low pri b/c the only write operation on refs is creating one + self.repo.create_git_ref(sha=base_branch.commit.sha, ref=f"refs/heads/{new_branch_name}") + branch = self.get_branch_safe(new_branch_name) + return branch + + def create_branch_from_sha(self, new_branch_name: str, base_sha: str) -> Branch | None: + self.repo.create_git_ref(ref=f"refs/heads/{new_branch_name}", sha=base_sha) + branch = self.get_branch_safe(new_branch_name) + return branch + + def delete_branch(self, branch_name: str) -> None: + if branch_name == self.default_branch: + logger.warning("Deleting the default branch is not allowed! Skipping delete.") + return + # TODO: log event + + branch_to_delete = self.get_branch_safe(branch_name) + if branch_to_delete: + ref_to_delete = self.repo.get_git_ref(f"heads/{branch_name}") + ref_to_delete.delete() + logger.info(f"Branch: {branch_name} deleted successfully!") + else: + logger.info(f"Branch: {branch_name} does not exist. Skipping delete.") + + #################################################################################################################### + # COMMITS + #################################################################################################################### + + def get_commit_safe(self, commit_sha: str) -> Commit | None: + try: + return self.repo.get_commit(commit_sha) + except UnknownObjectException as e: + logger.warning(f"Commit {commit_sha} not found:\n\t{e}") + return None + except Exception as e: + logger.warning(f"Error getting commit {commit_sha}:\n\t{e}") + return None + + #################################################################################################################### + # DIFFS + #################################################################################################################### + + def get_commit_diff(self, commit: Commit, show_commits: bool = False) -> str: + """Diff of a single commit""" + return self.compare_commits(commit.parents[0], commit, show_commits=show_commits) + + def get_pr_diff(self, pr: PullRequest, show_commits: bool = False) -> str: + return self.compare(pr.base.sha, pr.head.sha, show_commits=show_commits) + + def compare_commits(self, base_commit: Commit, head_commit: Commit, show_commits: bool = False) -> str: + return self.compare(base_commit.sha, head_commit.sha, show_commits=show_commits) + + # TODO: make base_branch param optional + def compare_branches(self, base_branch_name: str | None, head_branch_name: str, show_commits: bool = False) -> str: + """Comparison between two branches""" + if base_branch_name is None: + base_branch_name = self.default_branch + return self.compare(base_branch_name, head_branch_name, show_commits=show_commits) + + # NOTE: base utility that other compare functions should try to use + def compare(self, base: str, head: str, show_commits: bool = False) -> str: + comparison = self.repo.compare(base, head) + return format_comparison(comparison, show_commits=show_commits) + + #################################################################################################################### + # LABELS + #################################################################################################################### + + # TODO: also wrap labels in safe wrapper to allow making edits + def get_label_safe(self, label_name: str) -> Label | None: + try: + label_name = label_name.strip() + label = self.repo.get_label(label_name) + return label + except UnknownObjectException as e: + return None + except Exception as e: + logger.warning(f"Error getting label by name: {label_name}\n\t{e}") + return None + + def create_label(self, label_name: str, color: str) -> Label: + # TODO: also offer description field + label_name = label_name.strip() + self.repo.create_label(label_name, color) + # TODO: is there a way to convert new_label to a read-only label without making another API call? + # NOTE: return a read-only label to prevent people from editing it + return self.repo.get_label(label_name) + + def get_or_create_label(self, label_name: str, color: str) -> Label: + existing_label = self.get_label_safe(label_name) + if existing_label: + return existing_label + return self.create_label(label_name=label_name, color=color) + + #################################################################################################################### + # CHECK SUITES + #################################################################################################################### + + def get_check_suite_safe(self, check_suite_id: int) -> CheckSuite | None: + try: + return self.repo.get_check_suite(check_suite_id) + except UnknownObjectException as e: + return None + except Exception as e: + logger.warning(f"Error getting check suite by id: {check_suite_id}\n\t{e}") + return None + + #################################################################################################################### + # CHECK RUNS + #################################################################################################################### + + def get_check_run_safe(self, check_run_id: int) -> CheckRun | None: + try: + return self.repo.get_check_run(check_run_id) + except UnknownObjectException as e: + return None + except Exception as e: + logger.warning(f"Error getting check run by id: {check_run_id}\n\t{e}") + return None + + def create_check_run( + self, + name: str, + head_sha: str, + details_url: Opt[str] = NotSet, + status: Opt[str] = NotSet, + conclusion: Opt[str] = NotSet, + output: Opt[dict[str, str | list[dict[str, str | int]]]] = NotSet, + ) -> CheckRun: + new_check_run = self.repo.create_check_run(name=name, head_sha=head_sha, details_url=details_url, status=status, conclusion=conclusion, output=output) + return self.repo.get_check_run(new_check_run.id) + + #################################################################################################################### + # WORKFLOW + #################################################################################################################### + + def get_workflow_safe(self, file_name: str) -> Workflow | None: + try: + return self.repo.get_workflow(file_name) + except UnknownObjectException as e: + return None + except Exception as e: + logger.warning(f"Error getting workflow by file name: {file_name}\n\t{e}") + return None + + def create_workflow_dispatch(self, workflow: Workflow, ref: Branch | Tag | Commit | str, inputs: Opt[dict] = NotSet): + writeable_workflow = self.repo.get_workflow(workflow.id) + writeable_workflow.create_dispatch(ref=ref, inputs=inputs) + + #################################################################################################################### + # FORKS + #################################################################################################################### + + def merge_upstream(self, branch_name: str) -> bool: + """:calls: `POST /repos/{owner}/{repo}/merge-upstream `_ + :param branch: string + :rtype: bool + + Copied from: https://github.com/PyGithub/PyGithub/pull/2066. Remove after this change is merged into PyGithub. + """ + assert isinstance(branch_name, str), branch_name + post_parameters = {"branch": branch_name} + status, _, _ = self.repo._requester.requestJson("POST", f"{self.repo.url}/merge-upstream", input=post_parameters) + return status == 200 + + #################################################################################################################### + # SEARCH + #################################################################################################################### + + def search_issues(self, query: str, **kwargs) -> list[Issue]: + return self.gh_client.client.search_issues(query, **kwargs) + + def search_prs(self, query: str, **kwargs) -> list[PullRequest]: + return self.gh_client.client.search_issues(query, **kwargs) diff --git a/src/codegen.backup/git/clients/github_client.py b/src/codegen.backup/git/clients/github_client.py new file mode 100644 index 000000000..100c6281a --- /dev/null +++ b/src/codegen.backup/git/clients/github_client.py @@ -0,0 +1,46 @@ +from github import Consts +from github.GithubException import UnknownObjectException +from github.MainClass import Github +from github.Organization import Organization +from github.Repository import Repository + +from codegen.shared.logging.get_logger import get_logger + +logger = get_logger(__name__) + + +class GithubClient: + """Manages interaction with GitHub""" + + base_url: str + _client: Github + + def __init__(self, token: str | None = None, base_url: str = Consts.DEFAULT_BASE_URL): + self.base_url = base_url + self._client = Github(token, base_url=base_url) + + @property + def client(self) -> Github: + return self._client + + #################################################################################################################### + # CHECK RUNS + #################################################################################################################### + + def get_repo_by_full_name(self, full_name: str) -> Repository | None: + try: + return self._client.get_repo(full_name) + except UnknownObjectException as e: + return None + except Exception as e: + logger.warning(f"Error getting repo {full_name}:\n\t{e}") + return None + + def get_organization(self, org_name: str) -> Organization | None: + try: + return self._client.get_organization(org_name) + except UnknownObjectException as e: + return None + except Exception as e: + logger.warning(f"Error getting org {org_name}:\n\t{e}") + return None diff --git a/src/codegen.backup/git/configs/constants.py b/src/codegen.backup/git/configs/constants.py new file mode 100644 index 000000000..94ed8c9cc --- /dev/null +++ b/src/codegen.backup/git/configs/constants.py @@ -0,0 +1,5 @@ +"""Git related constants""" + +CODEGEN_BOT_NAME = "codegen-sh[bot]" +CODEGEN_BOT_EMAIL = "131295404+codegen-sh[bot]@users.noreply.github.com" +CODEOWNERS_FILEPATHS = [".github/CODEOWNERS", "CODEOWNERS", "docs/CODEOWNERS"] diff --git a/src/codegen.backup/git/models/codemod_context.py b/src/codegen.backup/git/models/codemod_context.py new file mode 100644 index 000000000..d074efb84 --- /dev/null +++ b/src/codegen.backup/git/models/codemod_context.py @@ -0,0 +1,36 @@ +from importlib.metadata import version +from typing import Any + +from pydantic import BaseModel +from pydantic.fields import Field + +from codegen.git.models.pull_request_context import PullRequestContext +from codegen.shared.logging.get_logger import get_logger + +logger = get_logger(__name__) + + +class CodemodContext(BaseModel): + CODEGEN_VERSION: str = version("codegen") + CODEMOD_ID: int | None = None + CODEMOD_LINK: str | None = None + CODEMOD_AUTHOR: str | None = None + TEMPLATE_ARGS: dict[str, Any] = Field(default_factory=dict) + + # TODO: add fields for version + # CODEMOD_VERSION_ID: int | None = None + # CODEMOD_VERSION_AUTHOR: str | None = None + + PULL_REQUEST: PullRequestContext | None = None + + @classmethod + def _render_template(cls, template_schema: dict[str, str], template_values: dict[str, Any]) -> dict[str, Any]: + template_data: dict[str, Any] = {} + for var_name, var_value in template_values.items(): + var_type = template_schema.get(var_name) + + if var_type == "list": + template_data[var_name] = [str(v).strip() for v in var_value.split(",")] + else: + template_data[var_name] = str(var_value) + return template_data diff --git a/src/codegen.backup/git/models/github_named_user_context.py b/src/codegen.backup/git/models/github_named_user_context.py new file mode 100644 index 000000000..b3eb22dd2 --- /dev/null +++ b/src/codegen.backup/git/models/github_named_user_context.py @@ -0,0 +1,12 @@ +from pydantic import BaseModel + + +class GithubNamedUserContext(BaseModel): + """Represents a GitHub user parsed from a webhook payload""" + + login: str + email: str | None = None + + @classmethod + def from_payload(cls, payload: dict) -> "GithubNamedUserContext": + return cls(login=payload.get("login"), email=payload.get("email")) diff --git a/src/codegen.backup/git/models/pr_options.py b/src/codegen.backup/git/models/pr_options.py new file mode 100644 index 000000000..345518ff9 --- /dev/null +++ b/src/codegen.backup/git/models/pr_options.py @@ -0,0 +1,20 @@ +from pydantic import BaseModel + +from codegen.shared.decorators.docs import apidoc + + +@apidoc +class PROptions(BaseModel): + """Options for generating a PR. + + Attributes: + title: The title of the pull request. + body: The body content of the pull request. + labels: A list of labels to be added to the pull request. + force_push_head_branch: Whether to force push the head branch. + """ + + title: str | None = None + body: str | None = None + labels: list[str] | None = None # TODO: not used until we add labels to GithubPullRequestModel + force_push_head_branch: bool | None = None diff --git a/src/codegen.backup/git/models/pr_part_context.py b/src/codegen.backup/git/models/pr_part_context.py new file mode 100644 index 000000000..162aed84b --- /dev/null +++ b/src/codegen.backup/git/models/pr_part_context.py @@ -0,0 +1,12 @@ +from pydantic import BaseModel + + +class PRPartContext(BaseModel): + """Represents a GitHub pull request part parsed from a webhook payload""" + + ref: str + sha: str + + @classmethod + def from_payload(cls, payload: dict) -> "PRPartContext": + return cls(ref=payload.get("ref"), sha=payload.get("sha")) diff --git a/src/codegen.backup/git/models/pull_request_context.py b/src/codegen.backup/git/models/pull_request_context.py new file mode 100644 index 000000000..1f97afbd1 --- /dev/null +++ b/src/codegen.backup/git/models/pull_request_context.py @@ -0,0 +1,49 @@ +from pydantic import BaseModel + +from codegen.git.models.github_named_user_context import GithubNamedUserContext +from codegen.git.models.pr_part_context import PRPartContext + + +class PullRequestContext(BaseModel): + """Represents a GitHub pull request""" + + id: int + url: str + html_url: str + number: int + state: str + title: str + user: GithubNamedUserContext + draft: bool + head: PRPartContext + base: PRPartContext + body: str | None = None + merged: bool | None = None + merged_by: dict | None = None + additions: int | None = None + deletions: int | None = None + changed_files: int | None = None + webhook_data: dict | None = None + + @classmethod + def from_payload(cls, webhook_payload: dict) -> "PullRequestContext": + webhook_data = webhook_payload.get("pull_request", {}) + return cls( + id=webhook_data.get("id"), + url=webhook_data.get("url"), + html_url=webhook_data.get("html_url"), + number=webhook_data.get("number"), + state=webhook_data.get("state"), + title=webhook_data.get("title"), + user=GithubNamedUserContext.from_payload(webhook_data.get("user", {})), + body=webhook_data.get("body"), + draft=webhook_data.get("draft"), + head=PRPartContext.from_payload(webhook_data.get("head", {})), + base=PRPartContext.from_payload(webhook_data.get("base", {})), + merged=webhook_data.get("merged"), + merged_by=webhook_data.get("merged_by", {}), + additions=webhook_data.get("additions"), + deletions=webhook_data.get("deletions"), + changed_files=webhook_data.get("changed_files"), + webhook_data=webhook_data, + ) diff --git a/src/codegen.backup/git/py.typed b/src/codegen.backup/git/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/src/codegen.backup/git/repo_operator/local_git_repo.py b/src/codegen.backup/git/repo_operator/local_git_repo.py new file mode 100644 index 000000000..4a24bc62b --- /dev/null +++ b/src/codegen.backup/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.backup/git/repo_operator/repo_operator.py b/src/codegen.backup/git/repo_operator/repo_operator.py new file mode 100644 index 000000000..f3bf2776f --- /dev/null +++ b/src/codegen.backup/git/repo_operator/repo_operator.py @@ -0,0 +1,920 @@ +import codecs +import fnmatch +import glob +import os +from collections.abc import Generator +from datetime import UTC, datetime +from functools import cached_property +from time import perf_counter +from typing import Self + +from codeowners import CodeOwners as CodeOwnersParser +from git import Commit as GitCommit +from git import Diff, GitCommandError, InvalidGitRepositoryError, Remote +from git import Repo as GitCLI +from git.remote import PushInfoList +from github.IssueComment import IssueComment +from github.PullRequest import PullRequest + +from codegen.configs.models.secrets import SecretsConfig +from codegen.git.clients.git_repo_client import GitRepoClient +from codegen.git.configs.constants import CODEGEN_BOT_EMAIL, CODEGEN_BOT_NAME +from codegen.git.repo_operator.local_git_repo import LocalGitRepo +from codegen.git.schemas.enums import CheckoutResult, FetchResult, RepoVisibility, SetupOption +from codegen.git.schemas.repo_config import RepoConfig +from codegen.git.utils.clone import clone_or_pull_repo, clone_repo, pull_repo +from codegen.git.utils.clone_url import add_access_token_to_url, get_authenticated_clone_url_for_repo_config, get_clone_url_for_repo_config, url_to_github +from codegen.git.utils.codeowner_utils import create_codeowners_parser_for_repo +from codegen.git.utils.file_utils import create_files +from codegen.git.utils.remote_progress import CustomRemoteProgress +from codegen.shared.logging.get_logger import get_logger +from codegen.shared.performance.stopwatch_utils import stopwatch +from codegen.shared.performance.time_utils import humanize_duration + +logger = get_logger(__name__) + + +class RepoOperator: + """A wrapper around GitPython to make it easier to interact with a repo.""" + + repo_config: RepoConfig + base_dir: str + bot_commit: bool = True + access_token: str | None = None + + # lazy attributes + _codeowners_parser: CodeOwnersParser | None = None + _default_branch: str | None = None + _remote_git_repo: GitRepoClient | None = None + _local_git_repo: LocalGitRepo | None = None + + def __init__( + self, + repo_config: RepoConfig, + access_token: str | None = None, + bot_commit: bool = False, + setup_option: SetupOption | None = None, + shallow: bool | None = None, + ) -> None: + assert repo_config is not None + self.repo_config = repo_config + self.access_token = access_token or SecretsConfig().github_token + self.base_dir = repo_config.base_dir + self.bot_commit = bot_commit + + if setup_option: + if shallow is not None: + self.setup_repo_dir(setup_option=setup_option, shallow=shallow) + else: + self.setup_repo_dir(setup_option=setup_option) + + else: + os.makedirs(self.repo_path, exist_ok=True) + GitCLI.init(self.repo_path) + self._local_git_repo = LocalGitRepo(repo_path=repo_config.repo_path) + if self.repo_config.full_name is None: + self.repo_config.full_name = self._local_git_repo.full_name + + #################################################################################################################### + # PROPERTIES + #################################################################################################################### + + @property + def repo_name(self) -> str: + return self.repo_config.name + + @property + def repo_path(self) -> str: + return os.path.join(self.base_dir, self.repo_name) + + @property + def remote_git_repo(self) -> GitRepoClient: + if not self.access_token and self.repo_config.visibility != RepoVisibility.PUBLIC: + msg = "Must initialize with access_token to get remote" + raise ValueError(msg) + + if not self._remote_git_repo: + self._remote_git_repo = GitRepoClient(self.repo_config, access_token=self.access_token) + return self._remote_git_repo + + @property + def clone_url(self) -> str: + if self.access_token: + return get_authenticated_clone_url_for_repo_config(repo=self.repo_config, token=self.access_token) + return f"https://github.com/{self.repo_config.full_name}.git" + + @property + def viz_path(self) -> str: + return os.path.join(self.base_dir, "codegen-graphviz") + + @property + def viz_file_path(self) -> str: + return os.path.join(self.viz_path, "graph.json") + + def _set_bot_email(self, git_cli: GitCLI) -> None: + with git_cli.config_writer("repository") as writer: + if not writer.has_section("user"): + writer.add_section("user") + writer.set("user", "email", CODEGEN_BOT_EMAIL) + + def _set_bot_username(self, git_cli: GitCLI) -> None: + with git_cli.config_writer("repository") as writer: + if not writer.has_section("user"): + writer.add_section("user") + writer.set("user", "name", CODEGEN_BOT_NAME) + + def _unset_bot_email(self, git_cli: GitCLI) -> None: + with git_cli.config_writer("repository") as writer: + if writer.has_option("user", "email"): + writer.remove_option("user", "email") + + def _unset_bot_username(self, git_cli: GitCLI) -> None: + with git_cli.config_writer("repository") as writer: + if writer.has_option("user", "name"): + writer.remove_option("user", "name") + + @cached_property + def git_cli(self) -> GitCLI: + git_cli = GitCLI(self.repo_path) + username = None + user_level = None + email = None + email_level = None + levels = ["system", "global", "user", "repository"] + for level in levels: + with git_cli.config_reader(level) as reader: + if reader.has_option("user", "name") and not username: + username = username or reader.get("user", "name") + user_level = user_level or level + if reader.has_option("user", "email") and not email: + email = email or reader.get("user", "email") + email_level = email_level or level + + # We need a username and email to commit, so if they're not set, set them to the bot's + if not username or self.bot_commit: + self._set_bot_username(git_cli) + if not email or self.bot_commit: + self._set_bot_email(git_cli) + + # If user config is set at a level above the repo level: unset it + if not self.bot_commit: + if username and username != CODEGEN_BOT_NAME and user_level != "repository": + self._unset_bot_username(git_cli) + if email and email != CODEGEN_BOT_EMAIL and email_level != "repository": + self._unset_bot_email(git_cli) + + return git_cli + + @property + def head_commit(self) -> GitCommit | None: + try: + return self.git_cli.head.commit + except ValueError as e: + if (f"Reference at {self.git_cli.head.ref.path!r} does not exist") in str(e): + logger.info(f"Ref: {self.git_cli.head.ref.name} has no commits") + return None + raise + + @property + def git_diff(self) -> str: + """Get the diff of the repo. Useful for checking if there are any changes.""" + return self.git_cli.git.diff() + + @property + def default_branch(self) -> str: + # Priority 1: If default branch has been set + if self._default_branch: + if self._default_branch is None: + self._default_branch = self.remote_git_repo.default_branch + return self._default_branch + return self._default_branch + + # Priority 2: If origin/HEAD ref exists + origin_prefix = "origin" + if f"{origin_prefix}/HEAD" in self.git_cli.refs: + return self.git_cli.refs[f"{origin_prefix}/HEAD"].reference.name.removeprefix(f"{origin_prefix}/") + + # Priority 3: Fallback to the active branch + return self.git_cli.active_branch.name + + @property + def codeowners_parser(self) -> CodeOwnersParser | None: + if not self._codeowners_parser: + if not self._remote_git_repo: + return None + self._codeowners_parser = create_codeowners_parser_for_repo(self.remote_git_repo) + return self._codeowners_parser + + #################################################################################################################### + # SET UP + #################################################################################################################### + def setup_repo_dir(self, setup_option: SetupOption = SetupOption.PULL_OR_CLONE, shallow: bool = True) -> None: + os.makedirs(self.base_dir, exist_ok=True) + os.chdir(self.base_dir) + if setup_option is SetupOption.CLONE: + # if repo exists delete, then clone, else clone + clone_repo(shallow=shallow, repo_path=self.repo_path, clone_url=self.clone_url) + elif setup_option is SetupOption.PULL_OR_CLONE: + # if repo exists, pull changes, else clone + self.clone_or_pull_repo(shallow=shallow) + elif setup_option is SetupOption.SKIP: + if not self.repo_exists(): + logger.warning(f"Valid git repo does not exist at {self.repo_path}. Cannot skip setup with SetupOption.SKIP.") + os.chdir(self.repo_path) + + def repo_exists(self) -> bool: + if not os.path.exists(self.repo_path): + return False + try: + _ = GitCLI(self.repo_path) + return True + except InvalidGitRepositoryError as e: + return False + + def clean_repo(self) -> None: + """Cleans the repo by: + 1. Discards any changes (tracked/untracked) + 2. Checks out the default branch (+ makes sure it's up to date with the remote) + 3. Deletes all branches except the default branch + 4. Deletes all remotes except origin + + Used in SetupOption.PULL_OR_CLONE to allow people to re-use existing repos and start from a clean state. + """ + logger.info(f"Cleaning repo at {self.repo_path} ...") + self.discard_changes() + self.checkout_branch(self.default_branch, remote=True) + self.clean_branches() + self.clean_remotes() + + @stopwatch + def discard_changes(self) -> None: + """Cleans repo dir by discarding any changes in staging/working directory and removes untracked files/dirs. Use with .is_dirty().""" + ts1 = perf_counter() + self.git_cli.head.reset(index=True, working_tree=True) # discard staged (aka index) + unstaged (aka working tree) changes in tracked files + ts2 = perf_counter() + self.git_cli.git.clean("-fdxq") # removes untracked changes and ignored files + ts3 = perf_counter() + self.git_cli.git.gc("--auto") # garbage collect + ts4 = perf_counter() + logger.info(f"discard_changes took {humanize_duration(ts2 - ts1)} to reset, {humanize_duration(ts3 - ts2)} to clean, {humanize_duration(ts4 - ts3)} to gc") + + @stopwatch + def clean_remotes(self) -> None: + for remote in self.git_cli.remotes: + if remote.name == "origin": + continue + logger.info(f"Deleting remote {remote.name} ...") + self.git_cli.delete_remote(remote) + + @stopwatch + def clean_branches(self) -> None: + for branch in self.git_cli.branches: + if self.is_branch_checked_out(branch.name): + continue + logger.info(f"Deleting branch {branch.name} ...") + self.git_cli.delete_head(branch.name, force=True) + + def pull_repo(self) -> None: + """Pull the latest commit down to an existing local repo""" + pull_repo(repo_path=self.repo_path, clone_url=self.clone_url) + + def clone_repo(self, shallow: bool = True) -> None: + clone_repo(repo_path=self.repo_path, clone_url=self.clone_url, shallow=shallow) + + def clone_or_pull_repo(self, shallow: bool = True) -> None: + """If repo exists, pulls changes. otherwise, clones the repo.""" + # TODO(CG-7804): if repo is not valid we should delete it and re-clone. maybe we can create a pull_repo util + use the existing clone_repo util + if self.repo_exists(): + self.clean_repo() + clone_or_pull_repo(repo_path=self.repo_path, clone_url=self.clone_url, shallow=shallow) + + #################################################################################################################### + # CHECKOUT, BRANCHES & COMMITS + #################################################################################################################### + @stopwatch + def checkout_remote_branch(self, branch_name: str | None = None, remote_name: str = "origin") -> CheckoutResult: + """Checks out a branch from a Remote + tracks the Remote. + If the branch_name is already checked out, does nothing + """ + return self.checkout_branch(branch_name, remote_name=remote_name, remote=True, create_if_missing=False) + + def safe_get_commit(self, commit: str) -> GitCommit | None: + """Gets commit if it exists, else returns None""" + try: + return self.git_cli.commit(commit) + except Exception as e: + logger.warning(f"Failed to get commit {commit}:\n\t{e}") + return None + + def fetch_remote(self, remote_name: str = "origin", refspec: str | None = None, force: bool = True) -> FetchResult: + """Fetches and updates a ref from a remote repository. + + Args: + remote_name (str): Name of the remote to fetch from. Defaults to "origin". + refspec (str | None): The refspec to fetch. If None, fetches all refs. Defaults to None. + force (bool): If True, forces the fetch operation. Defaults to True. + + Returns: + FetchResult: An enum indicating the result of the fetch operation. + - SUCCESS: Fetch was successful. + - REFSPEC_NOT_FOUND: The specified refspec doesn't exist in the remote. + + Raises: + GitCommandError: If the fetch operation fails for reasons other than a missing refspec. + + Note: + This force fetches by default b/c by default we prefer the remote branch over our local branch. + """ + logger.info(f"Fetching {remote_name} with refspec {refspec}") + progress = CustomRemoteProgress() + + try: + self.git_cli.remotes[remote_name].fetch(refspec=refspec, force=force, progress=progress, no_tags=True) + return FetchResult.SUCCESS + except GitCommandError as e: + if progress.fetch_result == FetchResult.REFSPEC_NOT_FOUND: + return FetchResult.REFSPEC_NOT_FOUND + else: + raise e + + def delete_remote(self, remote_name: str) -> None: + remote = self.git_cli.remote(remote_name) + if remote: + self.git_cli.delete_remote(remote) + + def create_remote(self, remote_name: str, remote_url: str) -> None: + """Creates a remote. Skips if the remote already exists.""" + if remote_name in self.git_cli.remotes: + logger.warning(f"Remote with name {remote_name} already exists. Skipping create_remote.") + return + self.git_cli.create_remote(remote_name, url=remote_url) + + @stopwatch + def checkout_commit(self, commit_hash: str | GitCommit, remote_name: str = "origin") -> CheckoutResult: + """Checks out the relevant commit + TODO: handle the environment being dirty + """ + logger.info(f"Checking out commit: {commit_hash}") + if not self.git_cli.is_valid_object(commit_hash, "commit"): + self.fetch_remote(remote_name=remote_name, refspec=commit_hash) + if not self.git_cli.is_valid_object(commit_hash, "commit"): + return CheckoutResult.NOT_FOUND + + if self.git_cli.is_dirty(): + logger.info(f"Environment is dirty, discarding changes before checking out commit: {commit_hash}") + self.discard_changes() + + self.git_cli.git.checkout(commit_hash) + return CheckoutResult.SUCCESS + + def get_active_branch_or_commit(self) -> str: + """Returns the current active branch, or commit hexsha if head is detached""" + if self.git_cli.head.is_detached: + return self.git_cli.head.commit.hexsha + return self.git_cli.active_branch.name + + def is_branch_checked_out(self, branch_name: str) -> bool: + if self.git_cli.head.is_detached: + return False + return self.git_cli.active_branch.name == branch_name + + def checkout_branch(self, branch_name: str | None, *, remote: bool = False, remote_name: str = "origin", create_if_missing: bool = True) -> CheckoutResult: + """Attempts to check out the branch in the following order: + - Check out the local branch by name + - Check out the remote branch if it's been fetched + - Creates a new branch from the current commit (with create=True) + + NOTE: if branch is already checked out this does nothing. + TIP: Use remote=True if you want to always try to checkout the branch from a remote + + Args: + ---- + branch_name (str): Name of the branch to checkout. + create_if_missing: If the branch doesn't exist, create one + remote: Checks out a branch from a Remote + tracks the Remote + force (bool): If True, force checkout by resetting the current branch to HEAD. + If False, raise an error if the branch is dirty. + + Raises: + ------ + GitCommandError: If there's an error with Git operations. + RuntimeError: If the branch is dirty and force is not set. + """ + if branch_name is None: + branch_name = self.default_branch + + try: + if self.is_branch_checked_out(branch_name): + if remote: + # If the branch is already checked out and we want to fetch it from the remote, reset --hard to the remote branch + logger.info(f"Branch {branch_name} is already checked out locally. Resetting to remote branch: {remote_name}/{branch_name}") + # TODO: would have to fetch the the remote branch first to retrieve latest changes + self.git_cli.git.reset("--hard", f"{remote_name}/{branch_name}") + return CheckoutResult.SUCCESS + else: + logger.info(f"Branch {branch_name} is already checked out! Skipping checkout_branch.") + return CheckoutResult.SUCCESS + + if self.git_cli.is_dirty(): + logger.info(f"Environment is dirty, discarding changes before checking out branch: {branch_name}.") + self.discard_changes() + + # If remote=True, create a local branch tracking the remote branch and checkout onto it + if remote: + res = self.fetch_remote(remote_name, refspec=f"{branch_name}:{branch_name}") + if res is FetchResult.SUCCESS: + self.git_cli.git.checkout(branch_name) + return CheckoutResult.SUCCESS + if res is FetchResult.REFSPEC_NOT_FOUND: + logger.warning(f"Branch {branch_name} not found in remote {remote_name}. Unable to checkout remote branch.") + return CheckoutResult.NOT_FOUND + + # If the branch already exists, checkout onto it + if branch_name in self.git_cli.heads: + self.git_cli.heads[branch_name].checkout() + return CheckoutResult.SUCCESS + + # If the branch does not exist and create_if_missing=True, create and checkout a new branch from the current commit + elif create_if_missing: + logger.info(f"Creating new branch {branch_name} from current commit: {self.git_cli.head.commit.hexsha}") + new_branch = self.git_cli.create_head(branch_name) + new_branch.checkout() + return CheckoutResult.SUCCESS + else: + return CheckoutResult.NOT_FOUND + + except GitCommandError as e: + if "fatal: ambiguous argument" in e.stderr: + logger.warning(f"Branch {branch_name} was not found in remote {remote_name}. Unable to checkout.") + return CheckoutResult.NOT_FOUND + else: + logger.exception(f"Error with Git operations: {e}") + raise + + def get_modified_files(self, ref: str | GitCommit) -> list[str]: + """Returns a list of modified files in the repo""" + self.git_cli.git.add(A=True) + diff = self.git_cli.git.diff(ref, "--name-only") + return diff.splitlines() + + def get_diffs(self, ref: str | GitCommit, reverse: bool = True) -> list[Diff]: + """Gets all staged diffs""" + self.git_cli.git.add(A=True) + return [diff for diff in self.git_cli.index.diff(ref, R=reverse)] + + @stopwatch + def stage_and_commit_all_changes(self, message: str, verify: bool = False, exclude_paths: list[str] | None = None) -> bool: + """TODO: rename to stage_and_commit_changes + Stage all changes and commit them with the given message. + Returns True if a commit was made and False otherwise. + """ + self.git_cli.git.add(A=True) + # Unstage the excluded paths + for path in exclude_paths or []: + try: + self.git_cli.git.reset("HEAD", "--", path) + except GitCommandError as e: + logger.warning(f"Failed to exclude path {path}: {e}") + return self.commit_changes(message, verify) + + def _get_username_email(self) -> tuple[str, str] | None: + for level in ["user", "global", "system"]: + with self.git_cli.config_reader(level) as reader: + if reader.has_section("user"): + user, email = reader.get_value("user", "name"), reader.get_value("user", "email") + if isinstance(user, str) and isinstance(email, str) and user != CODEGEN_BOT_NAME and email != CODEGEN_BOT_EMAIL: + return user, email + return None + + def commit_changes(self, message: str, verify: bool = False) -> bool: + """Returns True if a commit was made and False otherwise.""" + staged_changes = self.git_cli.git.diff("--staged") + if staged_changes: + if self.bot_commit and (info := self._get_username_email()): + user, email = info + message += f"\n\n Co-authored-by: {user} <{email}>" + commit_args = ["-m", message] + if self.bot_commit: + commit_args.append(f"--author='{CODEGEN_BOT_NAME} <{CODEGEN_BOT_EMAIL}>'") + if not verify: + commit_args.append("--no-verify") + self.git_cli.git.commit(*commit_args) + return True + else: + logger.info("No changes to commit. Do nothing.") + return False + + @stopwatch + def push_changes(self, remote: Remote | None = None, refspec: str | None = None, force: bool = False) -> PushInfoList: + """Push the changes to the given refspec of the remote. + + Args: + refspec (str | None): refspec to push. If None, the current active branch is used. + remote (Remote | None): Remote to push too. Defaults to 'origin'. + force (bool): If True, force push the changes. Defaults to False. + """ + # Use default remote if not provided + if not remote: + remote = self.git_cli.remote(name="origin") + + # Use the current active branch if no branch is specified + if not refspec: + # TODO: doesn't work with detached HEAD state + refspec = self.git_cli.active_branch.name + + res = remote.push(refspec=refspec, force=force, progress=CustomRemoteProgress()) + for push_info in res: + if push_info.flags & push_info.ERROR: + # Handle the error case + logger.warning(f"Error pushing {refspec}: {push_info.summary}") + elif push_info.flags & push_info.FAST_FORWARD: + # Successful fast-forward push + logger.info(f"{refspec} pushed successfully (fast-forward).") + elif push_info.flags & push_info.NEW_HEAD: + # Successful push of a new branch + logger.info(f"{refspec} pushed successfully as a new branch.") + elif push_info.flags & push_info.NEW_TAG: + # Successful push of a new tag (if relevant) + logger.info("New tag pushed successfully.") + else: + # Successful push, general case + logger.info(f"{refspec} pushed successfully.") + return res + + def relpath(self, abspath) -> str: + # TODO: check if the path is an abspath (i.e. contains self.repo_path) + return os.path.relpath(abspath, self.repo_path) + + def abspath(self, relpath) -> str: + return os.path.join(self.repo_path, relpath) + + # TODO: should rename to path exists so this can be used for dirs as well + def file_exists(self, path: str) -> bool: + return os.path.exists(self.abspath(path)) + + def folder_exists(self, path: str) -> bool: + return os.path.exists(self.abspath(path)) and os.path.isdir(self.abspath(path)) + + def mkdir(self, path: str) -> None: + os.makedirs(self.abspath(path), exist_ok=True) + + def emptydir(self, path: str) -> None: + """Removes all files within the specified directory.""" + if self.folder_exists(self.abspath(path)): + for filename in os.listdir(self.abspath(path)): + file_path = os.path.join(self.abspath(path), filename) + if os.path.isfile(file_path): + os.remove(file_path) + + def get_file(self, path: str) -> str: + """Returns the contents of a file""" + file_path = self.abspath(path) + try: + with open(file_path, encoding="utf-8-sig") as file: + content = file.read() + return content + except UnicodeDecodeError: + try: + with open(file_path, encoding="latin1") as file: + content = file.read() + return content + except UnicodeDecodeError: + print(f"Warning: Unable to decode file {file_path}. Skipping.") + return None + + def write_file(self, relpath: str, content: str) -> None: + """Writes file content to disk""" + with open(self.abspath(relpath), "w") as f: + f.write(content) + + def delete_file(self, path: str) -> None: + """Deletes a file from the repo""" + os.remove(self.abspath(path)) + if os.listdir(self.abspath(os.path.dirname(path))) == []: + os.rmdir(self.abspath(os.path.dirname(path))) + + def get_filepaths_for_repo(self, ignore_list): + # Get list of files to iterate over based on gitignore setting + if self.repo_config.respect_gitignore: + # ls-file flags: + # -c: show cached files + # -o: show other / untracked files + # --exclude-standard: exclude standard gitignore rules + filepaths = self.git_cli.git.ls_files("-co", "--exclude-standard").split("\n") + else: + filepaths = glob.glob("**", root_dir=self.repo_path, recursive=True, include_hidden=True) + # Filter filepaths by ignore list. + if ignore_list: + filepaths = [f for f in filepaths if not any(fnmatch.fnmatch(f, pattern) or f.startswith(pattern) for pattern in ignore_list)] + + # Fix bug where unicode characters are not handled correctly + for i, filepath in enumerate(filepaths): + # Check if it is one of the broken cases + if filepath.startswith('"'): + # Step 1: Strip the quotes + filepath = filepath.strip('"').strip("'") + + # Step 2: Convert the Python string to raw ASCII bytes (so \\ stays as two 0x5C). + raw_filepath = filepath.encode("ascii") + + # Step 3: Use escape_decode to process backslash escapes like \346 -> 0xE6 + decoded_filepath, _ = codecs.escape_decode(raw_filepath) + + # Step 4: Decode those bytes as UTF-8 to get the actual Unicode text + filepath = decoded_filepath.decode("utf-8") + + # Step 5: Replace the original filepath with the decoded filepath + filepaths[i] = filepath + + return filepaths + + # TODO: unify param naming i.e. subdirectories vs subdirs probably use subdirectories since that's in the DB + def iter_files( + self, + subdirs: list[str] | None = None, + extensions: list[str] | None = None, + ignore_list: list[str] | None = None, + skip_content: bool = False, + ) -> Generator[tuple[str, str]]: + """Iterates over all files in the codebase, yielding the filepath and its content. + + Args: + ---- + subdirs (list[str], optional): List of subdirectories to include. Defaults to None. Can include full filenames. + codeowners (list[str], optional): List of codeowners to iter files for. Defaults to None. Ex: if codeowners=["@group"], only files owned by @group will be included. + extensions (list[str], optional): List of file extensions to include. Defaults to None. + + Yields: + ------ + tuple: A tuple containing the relative filepath and the content of the file. + + """ + filepaths = self.get_filepaths_for_repo(ignore_list) + # Iterate through files and yield contents + for rel_filepath in filepaths: + rel_filepath: str + filepath = os.path.join(self.repo_path, rel_filepath) + + # Filter by subdirectory (includes full filenames) + if subdirs and not any(rel_filepath.startswith(subdir) for subdir in subdirs): + continue + + if extensions is None or any(filepath.endswith(e) for e in extensions): + try: + if os.path.isfile(filepath): + if not skip_content: + content = self.get_file(filepath) + yield rel_filepath, content + else: + yield rel_filepath, "" + else: + logger.warning(f"Skipping {filepath} because it does not exist or is not a valid file.") + except Exception as e: + logger.warning(f"Error reading file {filepath}: {e}") + + def list_files(self, subdirs: list[str] | None = None, extensions: list[str] | None = None) -> list[str]: + """List files matching subdirs + extensions in a repo. + + Args: + ---- + subdirs (list[str], optional): List of subdirectories to include. Defaults to None. + codeowners (list[str], optional): List of codeowners to iter files for. Defaults to None. Ex: if codeowners=["@group"], only files owned by @group will be included. + extensions (list[str], optional): List of file extensions to include. Defaults to None. + + Yields: + ------ + str: filepath + + """ + list_files = [] + + for rel_filepath in self.git_cli.git.ls_files().split("\n"): + rel_filepath: str + if subdirs and not any(d in rel_filepath for d in subdirs): + continue + if extensions is None or any(rel_filepath.endswith(e) for e in extensions): + list_files.append(rel_filepath) + return list_files + + def get_commits_in_last_n_days(self, days: int = 1) -> list[str]: + """Returns a list of commits in the last n days""" + repo = self.git_cli + ret = [] + default_branch = self.default_branch + for commit in repo.iter_commits(default_branch, all=True, reverse=False): + current_dt = datetime.now(tz=UTC) + current_dt = current_dt.replace(tzinfo=None) + commit_dt = commit.committed_datetime + commit_dt = commit_dt.replace(tzinfo=None) + if int((current_dt - commit_dt).total_seconds()) > 60 * 60 * 24 * days: + break + ret.append(commit.hexsha) + return ret + + def get_modified_files_in_last_n_days(self, days: int = 1) -> tuple[list[str], list[str]]: + """Returns a list of files modified and deleted in the last n days""" + modified_files = [] + deleted_files = [] + allowed_extensions = [".py"] + + repo = self.git_cli + commits = self.get_commits_in_last_n_days(days) + + for commit_sha in commits: + commit = repo.commit(commit_sha) + files_changed = commit.stats.files + for file, stats in files_changed.items(): + if stats["deletions"] == stats["lines"]: + deleted_files.append(file) + if file in modified_files: + modified_files.remove(file) + else: + if file not in modified_files and file[-3:] in allowed_extensions: + modified_files.append(file) + return modified_files, deleted_files + + @cached_property + def base_url(self) -> str | None: + repo_config = self.repo_config + clone_url = get_clone_url_for_repo_config(repo_config) + branch = self.get_active_branch_or_commit() + return url_to_github(clone_url, branch) + + def stash_push(self) -> None: + self.git_cli.git.stash("push") + + def stash_pop(self) -> None: + self.git_cli.git.stash("pop") + + #################################################################################################################### + # PR UTILITIES + #################################################################################################################### + + def get_pr_data(self, pr_number: int) -> dict: + """Returns the data associated with a PR""" + return self.remote_git_repo.get_pr_data(pr_number) + + def create_pr_comment(self, pr_number: int, body: str) -> IssueComment: + """Create a general comment on a pull request. + + Args: + pr_number (int): The PR number to comment on + body (str): The comment text + """ + pr = self.remote_git_repo.get_pull_safe(pr_number) + if pr: + comment = self.remote_git_repo.create_issue_comment(pr, body) + return comment + + def create_pr_review_comment( + self, + pr_number: int, + body: str, + commit_sha: str, + path: str, + line: int | None = None, + side: str = "RIGHT", + start_line: int | None = None, + ) -> None: + """Create an inline review comment on a specific line in a pull request. + + Args: + pr_number (int): The PR number to comment on + body (str): The comment text + commit_sha (str): The commit SHA to attach the comment to + path (str): The file path to comment on + line (int | None, optional): The line number to comment on. Defaults to None. + side (str | None, optional): Which version of the file to comment on ('LEFT' or 'RIGHT'). Defaults to None. + start_line (int | None, optional): For multi-line comments, the starting line. Defaults to None. + """ + pr = self.remote_git_repo.get_pull_safe(pr_number) + if pr: + commit = self.remote_git_repo.get_commit_safe(commit_sha) + if commit: + self.remote_git_repo.create_review_comment( + pull=pr, + body=body, + commit=commit, + path=path, + line=line, + side=side, + ) + + def get_pull_request(self, pr_number: int) -> PullRequest | None: + """Get a GitHub Pull Request object for the given PR number. + + Args: + pr_number (int): The PR number to fetch + + Returns: + PullRequest | None: The PyGitHub PullRequest object if found, None otherwise + + Note: + This requires a GitHub API key to be set when creating the RepoOperator + """ + try: + # Create GitHub client and get the PR + repo = self.remote_git_repo + if repo is None: + logger.warning("GitHub API key is required to fetch pull requests") + return None + return repo.get_pull_safe(pr_number) + except Exception as e: + logger.warning(f"Failed to get PR {pr_number}: {e!s}") + return None + + #################################################################################################################### + # CLASS METHODS + #################################################################################################################### + @classmethod + def create_from_files(cls, repo_path: str, files: dict[str, str], bot_commit: bool = True) -> Self: + """Used when you want to create a directory from a set of files and then create a RepoOperator that points to that directory. + Use cases: + - Unit testing + - Playground + - Codebase eval + + Args: + repo_path (str): The path to the directory to create. + files (dict[str, str]): A dictionary of file names and contents to create in the directory. + """ + # Step 1: Create dir (if not exists) + files + os.makedirs(repo_path, exist_ok=True) + create_files(base_dir=repo_path, files=files) + + # Step 2: Init git repo + op = cls(repo_config=RepoConfig.from_repo_path(repo_path), bot_commit=bot_commit) + if op.stage_and_commit_all_changes("[Codegen] initial commit"): + op.checkout_branch(None, create_if_missing=True) + return op + + @classmethod + def create_from_commit(cls, repo_path: str, commit: str, url: str, access_token: str | None = None, full_name: str | None = None) -> Self: + """Do a shallow checkout of a particular commit to get a repository from a given remote URL. + + Args: + repo_path (str): Path where the repo should be cloned + commit (str): The commit hash to checkout + url (str): Git URL of the repository + access_token (str | None): Optional GitHub API key for operations that need GitHub access + """ + op = cls(repo_config=RepoConfig.from_repo_path(repo_path, full_name=full_name), bot_commit=False, access_token=access_token) + + op.discard_changes() + if op.get_active_branch_or_commit() != commit: + op.create_remote("origin", url) + op.git_cli.remotes["origin"].fetch(commit, depth=1) + op.checkout_commit(commit) + return op + + @classmethod + def create_from_repo(cls, repo_path: str, url: str, access_token: str | None = None, full_history: bool = False) -> Self | None: + """Create a fresh clone of a repository or use existing one if up to date. + + Args: + repo_path (str): Path where the repo should be cloned + url (str): Git URL of the repository + access_token (str | None): Optional GitHub API key for operations that need GitHub access + full_history (bool): If True, clones the complete repository history. If False, performs a shallow clone. Defaults to False. + """ + access_token = access_token or SecretsConfig().github_token + if access_token: + url = add_access_token_to_url(url=url, token=access_token) + + # Check if repo already exists + if os.path.exists(repo_path): + try: + # Try to initialize git repo from existing path + git_cli = GitCLI(repo_path) + # Check if it has our remote URL + if any(remote.url == url for remote in git_cli.remotes): + # Fetch to check for updates + git_cli.remotes.origin.fetch() + # Get current and remote HEADs + local_head = git_cli.head.commit + remote_head = git_cli.remotes.origin.refs[git_cli.active_branch.name].commit + # If up to date, use existing repo + if local_head.hexsha == remote_head.hexsha: + return cls(repo_config=RepoConfig.from_repo_path(repo_path), bot_commit=False, access_token=access_token) + except Exception: + logger.exception("Failed to initialize Git repository. Falling back to fresh clone.") + + # If we get here, repo exists but is not up to date or valid + # Remove the existing directory to do a fresh clone + import shutil + + shutil.rmtree(repo_path) + + try: + # Clone the repository with or without full history + if full_history: + GitCLI.clone_from(url=url, to_path=repo_path) + else: + GitCLI.clone_from(url=url, to_path=repo_path, depth=1) + + # Initialize with the cloned repo + git_cli = GitCLI(repo_path) + except (GitCommandError, ValueError) as e: + logger.exception("Failed to initialize Git repository") + return None + return cls(repo_config=RepoConfig.from_repo_path(repo_path), bot_commit=False, access_token=access_token) diff --git a/src/codegen.backup/git/schemas/enums.py b/src/codegen.backup/git/schemas/enums.py new file mode 100644 index 000000000..8219e97b4 --- /dev/null +++ b/src/codegen.backup/git/schemas/enums.py @@ -0,0 +1,35 @@ +from enum import StrEnum, auto + + +class SetupOption(StrEnum): + # always do a fresh clone (if the repo already exists it will delete the dir first) + CLONE = auto() + # if the repo already exists, pull latest, else clone + PULL_OR_CLONE = auto() + # do nothing (useful if you want to use an existing repo in it's current state) + SKIP = auto() + # only initialize the repo + INIT = auto() + + +class FetchResult(StrEnum): + SUCCESS = "SUCCESS" + REFSPEC_NOT_FOUND = "REFSPEC_NOT_FOUND" + + +class CheckoutResult(StrEnum): + SUCCESS = "SUCCESS" + NOT_FOUND = "BRANCH_NOT_FOUND" + + +class DiffChangeType(StrEnum): + ADDED = "A" + DELETED = "D" + RENAMED = "R" + MODIFIED = "M" + + +class RepoVisibility(StrEnum): + PRIVATE = auto() + PUBLIC = auto() + INTERNAL = auto() diff --git a/src/codegen.backup/git/schemas/repo_config.py b/src/codegen.backup/git/schemas/repo_config.py new file mode 100644 index 000000000..f94e85592 --- /dev/null +++ b/src/codegen.backup/git/schemas/repo_config.py @@ -0,0 +1,56 @@ +import os.path +from pathlib import Path + +from pydantic import BaseModel + +from codegen.configs.models.repository import RepositoryConfig +from codegen.git.schemas.enums import RepoVisibility +from codegen.shared.enums.programming_language import ProgrammingLanguage +from codegen.shared.logging.get_logger import get_logger + +logger = get_logger(__name__) + + +class RepoConfig(BaseModel): + """All the information about the repo needed to build a codebase""" + + name: str + full_name: str | None = None + visibility: RepoVisibility | None = None + + # Codebase fields + base_dir: str = "/tmp" # parent directory of the git repo + language: ProgrammingLanguage = ProgrammingLanguage.PYTHON + respect_gitignore: bool = True + base_path: str | None = None # root directory of the codebase within the repo + subdirectories: list[str] | None = None + + # Additional sandbox settings + setup_commands: list[str] | None = None + + @classmethod + def from_envs(cls) -> "RepoConfig": + default_repo_config = RepositoryConfig() + return RepoConfig( + name=default_repo_config.name, + full_name=default_repo_config.full_name, + base_dir=os.path.dirname(default_repo_config.path), + language=ProgrammingLanguage(default_repo_config.language.upper()), + ) + + @classmethod + def from_repo_path(cls, repo_path: str, full_name: str | None = None) -> "RepoConfig": + name = os.path.basename(repo_path) + base_dir = os.path.dirname(repo_path) + return cls(name=name, base_dir=base_dir, full_name=full_name) + + @property + def repo_path(self) -> Path: + return Path(f"{self.base_dir}/{self.name}") + + @property + def organization_name(self) -> str | None: + if self.full_name is not None: + return self.full_name.split("/")[0] + + return None diff --git a/src/codegen.backup/git/utils/clone.py b/src/codegen.backup/git/utils/clone.py new file mode 100644 index 000000000..57a8fc3e8 --- /dev/null +++ b/src/codegen.backup/git/utils/clone.py @@ -0,0 +1,61 @@ +import os +import subprocess + +from git import Repo as GitRepo + +from codegen.git.utils.remote_progress import CustomRemoteProgress +from codegen.shared.logging.get_logger import get_logger +from codegen.shared.performance.stopwatch_utils import subprocess_with_stopwatch + +logger = get_logger(__name__) + + +# TODO: move into RepoOperator +def clone_repo( + repo_path: str, + clone_url: str, + shallow: bool = True, +): + """TODO: re-use this code in clone_or_pull_repo. create separate pull_repo util""" + if os.path.exists(repo_path) and os.listdir(repo_path): + # NOTE: if someone calls the current working directory is the repo directory then we need to move up one level + if os.getcwd() == os.path.realpath(repo_path): + repo_parent_dir = os.path.dirname(repo_path) + os.chdir(repo_parent_dir) + delete_command = f"rm -rf {repo_path}" + logger.info(f"Deleting existing clone with command: {delete_command}") + subprocess.run(delete_command, shell=True, capture_output=True) + GitRepo.clone_from(url=clone_url, to_path=repo_path, depth=1 if shallow else None, progress=CustomRemoteProgress()) + return repo_path + + +# TODO: update to use GitPython instead + move into RepoOperator +def clone_or_pull_repo( + repo_path: str, + clone_url: str, + shallow: bool = True, +): + if os.path.exists(repo_path) and os.listdir(repo_path): + logger.info(f"{repo_path} directory already exists. Pulling instead of cloning ...") + pull_repo(clone_url=clone_url, repo_path=repo_path) + else: + logger.info(f"{repo_path} directory does not exist running git clone ...") + clone_repo(repo_path=repo_path, clone_url=clone_url, shallow=shallow) + return repo_path + + +# TODO: update to use GitPython instead + move into RepoOperator +def pull_repo( + repo_path: str, + clone_url: str, +) -> None: + if not os.path.exists(repo_path): + logger.info(f"{repo_path} directory does not exist. Unable to git pull.") + return + + logger.info(f"Refreshing token for repo at {repo_path} ...") + subprocess.run(f"git -C {repo_path} remote set-url origin {clone_url}", shell=True, capture_output=True) + + pull_command = f"git -C {repo_path} pull {clone_url}" + logger.info(f"Pulling with command: {pull_command} ...") + subprocess_with_stopwatch(command=pull_command, command_desc=f"pull {repo_path}", shell=True, capture_output=True) diff --git a/src/codegen.backup/git/utils/clone_url.py b/src/codegen.backup/git/utils/clone_url.py new file mode 100644 index 000000000..21cd80cfb --- /dev/null +++ b/src/codegen.backup/git/utils/clone_url.py @@ -0,0 +1,25 @@ +from urllib.parse import urlparse + +from codegen.git.schemas.repo_config import RepoConfig + + +# TODO: move out doesn't belong here +def url_to_github(url: str, branch: str) -> str: + clone_url = url.removesuffix(".git").replace("git@github.com:", "github.com/") + return f"{clone_url}/blob/{branch}" + + +def get_clone_url_for_repo_config(repo_config: RepoConfig) -> str: + return f"https://github.com/{repo_config.full_name}.git" + + +def get_authenticated_clone_url_for_repo_config(repo: RepoConfig, token: str) -> str: + git_url = get_clone_url_for_repo_config(repo) + return add_access_token_to_url(git_url, token) + + +def add_access_token_to_url(url: str, token: str | None) -> str: + parsed_url = urlparse(url) + scheme = parsed_url.scheme or "https" + token_prefix = f"x-access-token:{token}@" if token else "" + return f"{scheme}://{token_prefix}{parsed_url.netloc}{parsed_url.path}" diff --git a/src/codegen.backup/git/utils/codeowner_utils.py b/src/codegen.backup/git/utils/codeowner_utils.py new file mode 100644 index 000000000..21e999179 --- /dev/null +++ b/src/codegen.backup/git/utils/codeowner_utils.py @@ -0,0 +1,50 @@ +from codeowners import CodeOwners +from github.PullRequest import PullRequest + +from codegen.git.clients.git_repo_client import GitRepoClient +from codegen.git.configs.constants import CODEOWNERS_FILEPATHS +from codegen.shared.logging.get_logger import get_logger + +logger = get_logger(__name__) + + +def get_filepath_owners(codeowners: CodeOwners, filepath: str) -> set[str]: + filename_owners = codeowners.of(filepath) + return {owner[1] for owner in filename_owners} + + +def is_path_owned_by_codeowner(codeowners: CodeOwners, path: str, codeowner: str) -> bool: + filename_owners = codeowners.of(path) + for owner in filename_owners: + if owner[1] == codeowner: + return True + return False + + +def create_codeowners_parser_for_repo(py_github_repo: GitRepoClient) -> CodeOwners | None: + for codeowners_filepath in CODEOWNERS_FILEPATHS: + try: + codeowner_file_contents = py_github_repo.get_contents(codeowners_filepath) + if codeowner_file_contents: + codeowners = CodeOwners(codeowner_file_contents) + return codeowners + except Exception as e: + continue + logger.info(f"Failed to create CODEOWNERS parser for repo: {py_github_repo.repo_config.name}. Returning None.") + return None + + +def get_codeowners_for_pull(repo: GitRepoClient, pull: PullRequest) -> list[str]: + codeowners_parser = create_codeowners_parser_for_repo(repo) + if not codeowners_parser: + logger.warning(f"Failed to create codeowners parser for repo: {repo.repo_config.name}. Returning empty list.") + return [] + codeowners_for_pull_set = set() + pull_files = pull.get_files() + for file in pull_files: + codeowners_for_file = codeowners_parser.of(file.filename) + for codeowner_for_file in codeowners_for_file: + codeowners_for_pull_set.add(codeowner_for_file[1]) + codeowners_for_pull_list = list(codeowners_for_pull_set) + logger.info(f"Pull: {pull.html_url} ({pull.title}) has codeowners: {codeowners_for_pull_list}") + return codeowners_for_pull_list diff --git a/src/codegen.backup/git/utils/file_utils.py b/src/codegen.backup/git/utils/file_utils.py new file mode 100644 index 000000000..f8ed0cbb6 --- /dev/null +++ b/src/codegen.backup/git/utils/file_utils.py @@ -0,0 +1,73 @@ +import os +from pathlib import Path + + +def create_file(file_path: str, content: str | bytes) -> str: + # Define the file path, name, and content + filepath = os.path.dirname(file_path) + filename = os.path.basename(file_path) + content = content + + # Call the create_file function + os.makedirs(filepath, exist_ok=True) + + # Create the full file path by joining the directory and filename + file_path = os.path.join(filepath, filename) + + # Write the content to the file + if isinstance(content, str): + with open(file_path, "w") as file: + file.write(content) + elif isinstance(content, bytes): + with open(file_path, "wb") as file: + file.write(content) + else: + msg = f"Invalid content type: {type(content)}" + raise ValueError(msg) + + # Check if the file was created + file_path = os.path.join(filepath, filename) + if not os.path.exists(file_path): + msg = f"Failed to create file {format(file_path)}" + raise FileNotFoundError(msg) + return file_path + + +def create_files(base_dir: str, files: dict[str, str]) -> None: + for filename, content in files.items(): + create_file(os.path.join(base_dir, filename), content) + + +def split_git_path(filepath: str) -> tuple[str, str | None]: + """Split a filepath into (git_root, base_path) tuple by finding .git directory. + + Args: + filepath (str): The full path to split + + Returns: + tuple: (git_root_path, relative_path) + + Raises: + ValueError: If the path is not in a git repository + """ + # Convert to absolute path and resolve any symlinks + path = Path(filepath).resolve() + + # Start from the given path and traverse up until we find .git + current = path + while current != current.parent: + if (current / ".git").exists(): + # Found the git root + git_root = str(current) + rel_path = str(path.relative_to(current)) + + # Handle the case where filepath is the git root itself + if rel_path == ".": + rel_path = None + + return (git_root, rel_path) + current = current.parent + + # If we get here, we didn't find a .git directory + msg = f"Path '{filepath}' is not in a git repository!" + raise ValueError(msg) diff --git a/src/codegen.backup/git/utils/format.py b/src/codegen.backup/git/utils/format.py new file mode 100644 index 000000000..20f6764d1 --- /dev/null +++ b/src/codegen.backup/git/utils/format.py @@ -0,0 +1,24 @@ +from github.Comparison import Comparison + + +def format_comparison(comparison: Comparison, show_commits: bool = True) -> str: + diff_str_list = [] + + for file in comparison.files: + # Header for each file + diff_str_list.append(f"File: {file.filename}, Status: {file.status}") + diff_str_list.append(f"+++ {file.filename if file.status != 'removed' else '/dev/null'}") + diff_str_list.append(f"--- {file.filename if file.status != 'added' else '/dev/null'}") + + # Parsing the patch for each file + if file.patch: + for line in file.patch.split("\n"): + if line.startswith("+") or line.startswith("-"): + diff_str_list.append(line) + + if show_commits: + for commit in comparison.commits: + # Commit information + diff_str_list.append(f"Commit: {commit.sha}, Author: {commit.commit.author.name}, Message: {commit.commit.message}") + + return "\n".join(diff_str_list) diff --git a/src/codegen.backup/git/utils/language.py b/src/codegen.backup/git/utils/language.py new file mode 100644 index 000000000..551ac4212 --- /dev/null +++ b/src/codegen.backup/git/utils/language.py @@ -0,0 +1,183 @@ +from collections import Counter +from pathlib import Path +from typing import Literal + +from codegen.git.utils.file_utils import split_git_path +from codegen.shared.enums.programming_language import ProgrammingLanguage +from codegen.shared.logging.get_logger import get_logger + +logger = get_logger(__name__) + +# Minimum ratio of files that must match the dominant language +MIN_LANGUAGE_RATIO = 0.1 + + +def determine_project_language(folder_path: str, strategy: Literal["most_common", "git_most_common", "package_json"] = "git_most_common") -> ProgrammingLanguage: + """Determines the primary programming language of a project. + + Args: + folder_path (str): Path to the folder to analyze + strategy (Literal["most_common", "git_most_common", "package_json"]): Strategy to use for determining language. + "most_common" analyzes file extensions, "git_most_common" analyzes files in the git repo, "package_json" checks for package.json presence. + + Returns: + ProgrammingLanguage: The determined programming language + """ + # TODO: Create a new strategy that follows gitignore + if strategy == "most_common": + return _determine_language_by_file_count(folder_path) + elif strategy == "git_most_common": + return _determine_language_by_git_file_count(folder_path) + elif strategy == "package_json": + return _determine_language_by_package_json(folder_path) + else: + msg = f"Invalid strategy: {strategy}" + raise ValueError(msg) + + +def _determine_language_by_file_count(folder_path: str) -> ProgrammingLanguage: + """Analyzes a folder to determine the primary programming language based on file extensions. + Returns the language with the most matching files. + + Args: + folder_path (str): Path to the folder to analyze + + Returns: + ProgrammingLanguage: The dominant programming language, or OTHER if no matching files found + or if less than MIN_LANGUAGE_RATIO of files match the dominant language + """ + from codegen.sdk.python import PyFile + from codegen.sdk.typescript.file import TSFile + + EXTENSIONS = { + ProgrammingLanguage.PYTHON: PyFile.get_extensions(), + ProgrammingLanguage.TYPESCRIPT: TSFile.get_extensions(), + } + + folder = Path(folder_path) + if not folder.exists() or not folder.is_dir(): + msg = f"Invalid folder path: {folder_path}" + raise ValueError(msg) + + # Initialize counters for each language + language_counts = Counter() + total_files = 0 + + # Walk through the directory + for file_path in folder.rglob("*"): + # Skip directories and hidden files + if file_path.is_dir() or file_path.name.startswith("."): + continue + + # Skip common directories to ignore + if any(ignore in str(file_path) for ignore in [".git", "node_modules", "__pycache__", "venv", ".env"]): + continue + + total_files += 1 + + # Count files for each language based on extensions + for language, exts in EXTENSIONS.items(): + if file_path.suffix in exts: + language_counts[language] += 1 + + # If no files found, return None + if not language_counts: + return ProgrammingLanguage.OTHER + + # Get the most common language and its count + most_common_language, count = language_counts.most_common(1)[0] + + logger.debug(f"Most common language: {most_common_language}, count: {count}, total files: {total_files}") + + # Check if the most common language makes up at least MIN_LANGUAGE_RATIO of all files + if total_files > 0 and (count / total_files) < MIN_LANGUAGE_RATIO: + return ProgrammingLanguage.OTHER + + return most_common_language + + +def _determine_language_by_git_file_count(folder_path: str) -> ProgrammingLanguage: + """Analyzes a git repo to determine the primary programming language based on file extensions. + Returns the language with the most matching files. + + Args: + folder_path (str): Path to the git repo to analyze + + Returns: + ProgrammingLanguage: The dominant programming language, or OTHER if no matching files found + or if less than MIN_LANGUAGE_RATIO of files match the dominant language + """ + from codegen.git.repo_operator.repo_operator import RepoOperator + from codegen.git.schemas.repo_config import RepoConfig + from codegen.sdk.codebase.codebase_context import GLOBAL_FILE_IGNORE_LIST + from codegen.sdk.python import PyFile + from codegen.sdk.typescript.file import TSFile + + EXTENSIONS = { + ProgrammingLanguage.PYTHON: PyFile.get_extensions(), + ProgrammingLanguage.TYPESCRIPT: TSFile.get_extensions(), + } + + folder = Path(folder_path) + if not folder.exists() or not folder.is_dir(): + msg = f"Invalid folder path: {folder_path}" + raise ValueError(msg) + + # Initialize counters for each language + language_counts = Counter() + total_files = 0 + + # Initiate RepoOperator + git_root, base_path = split_git_path(folder_path) + repo_config = RepoConfig.from_repo_path(repo_path=git_root) + repo_operator = RepoOperator(repo_config=repo_config) + + # Walk through the directory + for rel_path, _ in repo_operator.iter_files(subdirs=[base_path] if base_path else None, ignore_list=GLOBAL_FILE_IGNORE_LIST): + # Convert to Path object + file_path = Path(git_root) / Path(rel_path) + + # Skip directories and hidden files + if file_path.is_dir() or file_path.name.startswith("."): + continue + + total_files += 1 + + # Count files for each language based on extensions + for language, exts in EXTENSIONS.items(): + if file_path.suffix in exts: + language_counts[language] += 1 + + # If no files found, return None + if not language_counts: + return ProgrammingLanguage.OTHER + + # Get the most common language and its count + most_common_language, count = language_counts.most_common(1)[0] + + logger.debug(f"Most common language: {most_common_language}, count: {count}, total files: {total_files}") + + # Check if the most common language makes up at least MIN_LANGUAGE_RATIO of all files + if total_files > 0 and (count / total_files) < MIN_LANGUAGE_RATIO: + return ProgrammingLanguage.OTHER + + return most_common_language + + +def _determine_language_by_package_json(folder_path: str) -> ProgrammingLanguage: + """Determines project language by checking for presence of package.json. + Faster but less accurate than file count strategy. + + Args: + folder_path (str): Path to the folder to analyze + + Returns: + ProgrammingLanguage: TYPESCRIPT if package.json exists, otherwise PYTHON + """ + package_json_path = Path(folder_path) / "package.json" + if package_json_path.exists(): + logger.debug(f"Found package.json at {package_json_path}") + return ProgrammingLanguage.TYPESCRIPT + else: + logger.debug(f"No package.json found at {package_json_path}") + return ProgrammingLanguage.PYTHON diff --git a/src/codegen.backup/git/utils/pr_review.py b/src/codegen.backup/git/utils/pr_review.py new file mode 100644 index 000000000..ffb3f52f0 --- /dev/null +++ b/src/codegen.backup/git/utils/pr_review.py @@ -0,0 +1,171 @@ +from typing import TYPE_CHECKING + +from github import Repository +from github.PullRequest import PullRequest +from unidiff import PatchSet + +from codegen.git.models.pull_request_context import PullRequestContext +from codegen.git.repo_operator.repo_operator import RepoOperator + +if TYPE_CHECKING: + from codegen.sdk.core.codebase import Codebase, Editable, File + + +def get_merge_base(git_repo_client: Repository, pull: PullRequest | PullRequestContext) -> str: + """Gets the merge base of a pull request using a remote GitHub API client. + + Args: + git_repo_client (GitRepoClient): The GitHub repository client. + pull (PullRequest): The pull request object. + + Returns: + str: The SHA of the merge base commit. + """ + comparison = git_repo_client.compare(pull.base.sha, pull.head.sha) + return comparison.merge_base_commit.sha + + +def get_file_to_changed_ranges(pull_patch_set: PatchSet) -> dict[str, list]: + file_to_changed_ranges = {} + for patched_file in pull_patch_set: + # TODO: skip is deleted + if patched_file.is_removed_file: + continue + changed_ranges = [] # list of changed lines for the file + for hunk in patched_file: + changed_ranges.append(range(hunk.target_start, hunk.target_start + hunk.target_length)) + file_to_changed_ranges[patched_file.path] = changed_ranges + return file_to_changed_ranges + + +def to_1_indexed(zero_indexed_range: range) -> range: + """Converts a n-indexed range to n+1-indexed. + Primarily to convert 0-indexed ranges to 1 indexed + """ + return range(zero_indexed_range.start + 1, zero_indexed_range.stop + 1) + + +def overlaps(range1: range, range2: range) -> bool: + """Returns True if the two ranges overlap, False otherwise.""" + return max(range1.start, range2.start) < min(range1.stop, range2.stop) + + +def get_file_to_commit_sha(op: RepoOperator, pull: PullRequest) -> dict[str, str]: + """Gets a mapping of file paths to their latest commit SHA in the PR. + + Args: + op (RepoOperator): The repository operator + pull (PullRequest): The pull request object + + Returns: + dict[str, str]: A dictionary mapping file paths to their latest commit SHA + """ + if not op.remote_git_repo: + msg = "GitHub API client is required to get PR commit information" + raise ValueError(msg) + + file_to_commit = {} + + # Get all commits in the PR + commits = list(pull.get_commits()) + + # Get all modified files + files = pull.get_files() + + # For each file, find its latest commit + for file in files: + # Look through commits in reverse order to find the latest one that modified this file + for commit in reversed(commits): + # Get the files modified in this commit + files_in_commit = commit.files + if any(f.filename == file.filename for f in files_in_commit): + file_to_commit[file.filename] = commit.sha + break + + # If we didn't find a commit (shouldn't happen), use the head SHA + if file.filename not in file_to_commit: + file_to_commit[file.filename] = pull.head.sha + + return file_to_commit + + +class CodegenPR: + """Wrapper around PRs - enables codemods to interact with them""" + + _gh_pr: PullRequest + _codebase: "Codebase" + _op: RepoOperator + + # =====[ Computed ]===== + _modified_file_ranges: dict[str, list[tuple[int, int]]] = None + + def __init__(self, op: RepoOperator, codebase: "Codebase", pr: PullRequest): + self._op = op + self._gh_pr = pr + self._codebase = codebase + + @property + def modified_file_ranges(self) -> dict[str, list[tuple[int, int]]]: + """Files and the ranges within that are modified""" + if not self._modified_file_ranges: + pull_patch_set = self.get_pull_patch_set() + self._modified_file_ranges = get_file_to_changed_ranges(pull_patch_set) + return self._modified_file_ranges + + @property + def modified_files(self) -> list["File"]: + filenames = self.modified_file_ranges.keys() + return [self._codebase.get_file(f, optional=True) for f in filenames] + + def is_modified(self, editable: "Editable") -> bool: + """Returns True if the Editable's range contains any modified lines""" + filepath = editable.filepath + changed_ranges = self._modified_file_ranges.get(filepath, []) + symbol_range = to_1_indexed(editable.line_range) + if any(overlaps(symbol_range, changed_range) for changed_range in changed_ranges): + return True + return False + + @property + def modified_symbols(self) -> list[str]: + # Import SourceFile locally to avoid circular dependencies + from codegen.sdk.core.file import SourceFile + + all_modified = [] + for file in self.modified_files: + if file is None: + print("Warning: File is None") + continue + if not isinstance(file, SourceFile): + continue + for symbol in file.symbols: + if self.is_modified(symbol): + all_modified.append(symbol.name) + + return all_modified + + def get_pr_diff(self) -> str: + """Get the full diff of the PR""" + if not self._op.remote_git_repo: + msg = "GitHub API client is required to get PR diffs" + raise ValueError(msg) + + # Get the diff directly from the PR + status, _, res = self._op.remote_git_repo.repo._requester.requestJson("GET", self._gh_pr.url, headers={"Accept": "application/vnd.github.v3.diff"}) + if status != 200: + msg = f"Failed to get PR diff: {res}" + raise Exception(msg) + return res + + def get_pull_patch_set(self) -> PatchSet: + diff = self.get_pr_diff() + pull_patch_set = PatchSet(diff) + return pull_patch_set + + def get_commit_sha(self) -> str: + """Get the commit SHA of the PR""" + return self._gh_pr.head.sha + + def get_file_commit_shas(self) -> dict[str, str]: + """Get a mapping of file paths to their latest commit SHA in the PR""" + return get_file_to_commit_sha(op=self._op, pull=self._gh_pr) diff --git a/src/codegen.backup/git/utils/remote_progress.py b/src/codegen.backup/git/utils/remote_progress.py new file mode 100644 index 000000000..55878c588 --- /dev/null +++ b/src/codegen.backup/git/utils/remote_progress.py @@ -0,0 +1,32 @@ +import time + +from git import RemoteProgress + +from codegen.git.schemas.enums import FetchResult +from codegen.shared.logging.get_logger import get_logger + +logger = get_logger(__name__) + + +class CustomRemoteProgress(RemoteProgress): + fetch_result: FetchResult | None = None + last_line_time: float | None = None + + def _parse_progress_line(self, line) -> None: + self.line_dropped(line) + if "fatal: couldn't find remote ref" in line: + self.fetch_result = FetchResult.REFSPEC_NOT_FOUND + + def line_dropped(self, line) -> None: + if self.last_line_time is None or time.time() - self.last_line_time > 1: + logger.info(line) + self.last_line_time = time.time() + + def update( + self, + op_code: int, + cur_count: str | float, + max_count: str | float | None = None, + message: str = "", + ) -> None: + logger.info(f"message: {message} op_code: {op_code} cur_count: {cur_count} max_count: {max_count}") diff --git a/src/codegen.backup/py.typed b/src/codegen.backup/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/src/codegen.backup/shared/README.md b/src/codegen.backup/shared/README.md new file mode 100644 index 000000000..8c21453f1 --- /dev/null +++ b/src/codegen.backup/shared/README.md @@ -0,0 +1,8 @@ +# Codegen Shared + +A codegen module to contain a miscellaneous set of shared utilities. + +### Dependencies + +This module should NOT contain any high level dependencies on other codegen modules. +It should only depend on standard libraries and other shared utilities. diff --git a/src/codegen.backup/shared/__init__.py b/src/codegen.backup/shared/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/codegen.backup/shared/compilation/README.md b/src/codegen.backup/shared/compilation/README.md new file mode 100644 index 000000000..236fdb209 --- /dev/null +++ b/src/codegen.backup/shared/compilation/README.md @@ -0,0 +1,8 @@ +Utils around compiling a user's codeblock into a function. + +This includes: + +- Raising on any dangerous operations in the codeblock +- Catching and logging any compilation errors +- Monkey patching built-ins like print +- etc diff --git a/src/codegen.backup/shared/compilation/codeblock_validation.py b/src/codegen.backup/shared/compilation/codeblock_validation.py new file mode 100644 index 000000000..1d9672b03 --- /dev/null +++ b/src/codegen.backup/shared/compilation/codeblock_validation.py @@ -0,0 +1,14 @@ +import re + +from codegen.shared.exceptions.compilation import DangerousUserCodeException + + +def check_for_dangerous_operations(user_code: str) -> None: + """If codeblock has dangerous operations (ex: exec, os.environ, etc) then raise an error and prevent the user from executing it.""" + dangerous_operation_patterns = [ + r"\b(os\.environ|locals|globals)\b", # Environment variables and scope access + ] + pattern = "|".join(dangerous_operation_patterns) + if re.search(pattern, user_code, re.IGNORECASE): + msg = "The codeblock contains potentially dangerous operations that are not allowed." + raise DangerousUserCodeException(msg) diff --git a/src/codegen.backup/shared/compilation/exception_utils.py b/src/codegen.backup/shared/compilation/exception_utils.py new file mode 100644 index 000000000..b19cc6084 --- /dev/null +++ b/src/codegen.backup/shared/compilation/exception_utils.py @@ -0,0 +1,51 @@ +from types import FrameType, TracebackType + +from codegen.shared.logging.get_logger import get_logger + +logger = get_logger(__name__) + + +def get_offset_traceback(tb_lines: list[str], line_offset: int = 0, filenameFilter: str = "") -> str: + """Generate a traceback string with offset line numbers. + + :param tb_lines: lines output for the traceback + :param line_offset: Number of lines to offset the traceback + :return: A string containing the offset traceback + """ + # Process each line of the traceback + offset_tb_lines = [] + for line in tb_lines: + if line.lstrip().startswith("File"): + if line.lstrip().startswith(f'File "{filenameFilter}"') and "execute" not in line: + # This line contains file and line number information + parts = line.split(", line ") + if len(parts) > 1: + # Offset the line number + line_num = int(parts[1].split(",")[0]) + new_line_num = line_num - line_offset + line = f"{parts[0]}, line {new_line_num}{','.join(parts[1].split(',')[1:])}" + offset_tb_lines.append(line) + else: + offset_tb_lines.append(line) + + # Join the processed lines back into a single string + return "".join(offset_tb_lines) + + +def get_local_frame(exc_type: type[BaseException], exc_value: BaseException, exc_traceback: TracebackType) -> FrameType | None: + LOCAL_FILENAME = "" + LOCAL_MODULE_DIR = "codegen-backend/app/" + tb = exc_traceback + while tb and ((tb.tb_next and tb.tb_frame.f_code.co_filename != LOCAL_FILENAME) or LOCAL_MODULE_DIR in tb.tb_frame.f_code.co_filename): + tb = tb.tb_next + + frame = tb.tb_frame if tb else None + return frame + + +def get_local_frame_context(frame: FrameType): + local_vars = {k: v for k, v in frame.f_locals.items() if not k.startswith("__")} + local_vars.pop("print", None) + local_vars.pop("codebase", None) + local_vars.pop("pr_options", None) + return local_vars diff --git a/src/codegen.backup/shared/compilation/function_compilation.py b/src/codegen.backup/shared/compilation/function_compilation.py new file mode 100644 index 000000000..e29b267f6 --- /dev/null +++ b/src/codegen.backup/shared/compilation/function_compilation.py @@ -0,0 +1,67 @@ +import linecache +import sys +import traceback +from collections.abc import Callable + +from codegen.shared.exceptions.compilation import InvalidUserCodeException +from codegen.shared.logging.get_logger import get_logger + +logger = get_logger(__name__) + + +def get_compilation_error_context(filename: str, line_number: int, window_size: int = 2): + """Get lines of context around SyntaxError + Exceptions that occur when compiling functions.""" + start = max(1, line_number - window_size) + end = line_number + window_size + 1 + lines = [] + for i in range(start, end): + line = linecache.getline(filename, i).rstrip() + if line: + lines.append((i, line)) + return lines + + +def safe_compile_function_string(custom_scope: dict, func_name: str, func_str: str) -> Callable: + # =====[ Add function string to linecache ]===== + # (This is necessary for the traceback to work correctly) + linecache.cache[""] = (len(func_str), None, func_str.splitlines(True), "") + + # =====[ Compile & exec the code ]===== + # This will throw errors if there is invalid syntax + try: + # First, try to compile the code to catch syntax errors + logger.info(f"Compiling function: {func_name} ...") + compiled_code = compile(func_str, "", "exec") + # If compilation succeeds, try to execute the code + logger.info(f"Compilation succeeded. exec-ing function: {func_name} ...") + exec(compiled_code, custom_scope, custom_scope) + + # =====[ Catch SyntaxErrors ]===== + except SyntaxError as e: + error_class = e.__class__.__name__ + detail = e.args[0] + line_number = e.lineno + context_lines = get_compilation_error_context("", line_number) + context_str = "\n".join(f"{'>' if i == line_number else ' '} {i}: {line}" for i, line in context_lines) + error_line = linecache.getline("", line_number).strip() + caret_line = " " * (e.offset - 1) + "^" * (len(error_line) - e.offset + 1) + error_message = f"{error_class} at line {line_number}: {detail}\n {error_line}\n {caret_line}\n{context_str}" + raise InvalidUserCodeException(error_message) from e + + # =====[ All other Exceptions ]===== + except Exception as e: + error_class = e.__class__.__name__ + detail = str(e) + _, _, tb = sys.exc_info() + line_number = traceback.extract_tb(tb)[-1].lineno + context_lines = get_compilation_error_context("", line_number) + context_str = "\n".join(f"{'>' if i == line_number else ' '} {i}: {line}" for i, line in context_lines) + error_line = linecache.getline("", line_number).strip() + error_message = f"{error_class} at line {line_number}: {detail}\n {error_line}\n{context_str}" + raise InvalidUserCodeException(error_message) from e + + finally: + # Clear the cache to free up memory + linecache.clearcache() + + return custom_scope.get(func_name) diff --git a/src/codegen.backup/shared/compilation/function_construction.py b/src/codegen.backup/shared/compilation/function_construction.py new file mode 100644 index 000000000..f50772015 --- /dev/null +++ b/src/codegen.backup/shared/compilation/function_construction.py @@ -0,0 +1,55 @@ +import re + +from codegen.shared.compilation.function_imports import get_generated_imports +from codegen.shared.logging.get_logger import get_logger + +logger = get_logger(__name__) + + +def create_function_str_from_codeblock(codeblock: str, func_name: str) -> str: + """Creates a function string from a codeblock.""" + # =====[ Make an `execute` function string w/ imports ]===== + func_str = wrap_codeblock_in_function(codeblock, func_name) + + # =====[ Add imports to the top ]===== + func_str = get_imports_string().format(func_str=func_str) + return func_str + + +def wrap_codeblock_in_function(codeblock: str, func_name: str) -> str: + """Wrap a codeblock in a function with the specified name. + + Args: + codeblock (str): The code to be wrapped in a function. + func_name (str): The name to give the wrapping function. + + Note: + Skip wrapping if a function with the specified name already exists in the codeblock. + """ + if re.search(rf"\bdef\s+{func_name}\s*\(", codeblock): + logger.info(f"Codeblock already has a function named {func_name}. Skipping wrap.") + return codeblock + + # If not function func_name does not already exist, create a new function with the codeblock inside + user_code = indent_user_code(codeblock) + codeblock = f""" +def {func_name}(codebase: Codebase, pr_options: PROptions | None = None, pr = None, **kwargs): + print = codebase.log +{user_code} + """ + return codeblock + + +def indent_user_code(codeblock: str) -> str: + return "\n".join(f" {line}" for line in codeblock.strip().split("\n")) + + +def get_imports_string(): + """Gets imports marked with apidoc decorators. This list is autogenerated by generate_runner_imports""" + imports_str = get_generated_imports() + + func_str_template = """ + +{func_str} +""" + return imports_str + func_str_template diff --git a/src/codegen.backup/shared/compilation/function_imports.py b/src/codegen.backup/shared/compilation/function_imports.py new file mode 100644 index 000000000..f8539926c --- /dev/null +++ b/src/codegen.backup/shared/compilation/function_imports.py @@ -0,0 +1,200 @@ +# This file is auto-generated, do not modify manually. Edit this in src/codegen/gscli/generate/runner_imports.py. +def get_generated_imports(): + return """ +# External imports +import os +import re +from pathlib import Path +import networkx as nx +import plotly + +# GraphSitter imports (private) + +from codegen.git.models.codemod_context import CodemodContext +from codegen.git.models.github_named_user_context import GithubNamedUserContext +from codegen.git.models.pr_options import PROptions +from codegen.git.models.pr_part_context import PRPartContext +from codegen.git.models.pull_request_context import PullRequestContext + +from codegen.shared.exceptions.control_flow import StopCodemodException + +# GraphSitter imports (public) +from codegen.sdk.codebase.flagging.enums import FlagKwargs +from codegen.sdk.codebase.flagging.enums import MessageType +from codegen.sdk.codebase.span import Span +from codegen.sdk.core.assignment import Assignment +from codegen.sdk.core.class_definition import Class +from codegen.sdk.core.codebase import Codebase +from codegen.sdk.core.codebase import CodebaseType +from codegen.sdk.core.codebase import PyCodebaseType +from codegen.sdk.core.codebase import TSCodebaseType +from codegen.sdk.core.codeowner import CodeOwner +from codegen.sdk.core.dataclasses.usage import Usage +from codegen.sdk.core.dataclasses.usage import UsageKind +from codegen.sdk.core.dataclasses.usage import UsageType +from codegen.sdk.core.detached_symbols.argument import Argument +from codegen.sdk.core.detached_symbols.code_block import CodeBlock +from codegen.sdk.core.detached_symbols.decorator import Decorator +from codegen.sdk.core.detached_symbols.function_call import FunctionCall +from codegen.sdk.core.detached_symbols.parameter import Parameter +from codegen.sdk.core.directory import Directory +from codegen.sdk.core.export import Export +from codegen.sdk.core.expressions.await_expression import AwaitExpression +from codegen.sdk.core.expressions.binary_expression import BinaryExpression +from codegen.sdk.core.expressions.boolean import Boolean +from codegen.sdk.core.expressions.chained_attribute import ChainedAttribute +from codegen.sdk.core.expressions.comparison_expression import ComparisonExpression +from codegen.sdk.core.expressions.expression import Expression +from codegen.sdk.core.expressions.generic_type import GenericType +from codegen.sdk.core.expressions.multi_expression import MultiExpression +from codegen.sdk.core.expressions.name import Name +from codegen.sdk.core.expressions.named_type import NamedType +from codegen.sdk.core.expressions.none_type import NoneType +from codegen.sdk.core.expressions.number import Number +from codegen.sdk.core.expressions.parenthesized_expression import ParenthesizedExpression +from codegen.sdk.core.expressions.placeholder_type import PlaceholderType +from codegen.sdk.core.expressions.string import String +from codegen.sdk.core.expressions.subscript_expression import SubscriptExpression +from codegen.sdk.core.expressions.ternary_expression import TernaryExpression +from codegen.sdk.core.expressions.tuple_type import TupleType +from codegen.sdk.core.expressions.type import Type +from codegen.sdk.core.expressions.unary_expression import UnaryExpression +from codegen.sdk.core.expressions.union_type import UnionType +from codegen.sdk.core.expressions.unpack import Unpack +from codegen.sdk.core.expressions.value import Value +from codegen.sdk.core.external_module import ExternalModule +from codegen.sdk.core.file import File +from codegen.sdk.core.file import SourceFile +from codegen.sdk.core.function import Function +from codegen.sdk.core.import_resolution import Import +from codegen.sdk.core.interface import Interface +from codegen.sdk.core.interfaces.callable import Callable +from codegen.sdk.core.interfaces.editable import Editable +from codegen.sdk.core.interfaces.exportable import Exportable +from codegen.sdk.core.interfaces.has_block import HasBlock +from codegen.sdk.core.interfaces.has_name import HasName +from codegen.sdk.core.interfaces.has_value import HasValue +from codegen.sdk.core.interfaces.importable import Importable +from codegen.sdk.core.interfaces.typeable import Typeable +from codegen.sdk.core.interfaces.unwrappable import Unwrappable +from codegen.sdk.core.interfaces.usable import Usable +from codegen.sdk.core.placeholder.placeholder import Placeholder +from codegen.sdk.core.placeholder.placeholder_stub import StubPlaceholder +from codegen.sdk.core.placeholder.placeholder_type import TypePlaceholder +from codegen.sdk.core.statements.assignment_statement import AssignmentStatement +from codegen.sdk.core.statements.attribute import Attribute +from codegen.sdk.core.statements.block_statement import BlockStatement +from codegen.sdk.core.statements.catch_statement import CatchStatement +from codegen.sdk.core.statements.comment import Comment +from codegen.sdk.core.statements.export_statement import ExportStatement +from codegen.sdk.core.statements.expression_statement import ExpressionStatement +from codegen.sdk.core.statements.for_loop_statement import ForLoopStatement +from codegen.sdk.core.statements.if_block_statement import IfBlockStatement +from codegen.sdk.core.statements.import_statement import ImportStatement +from codegen.sdk.core.statements.raise_statement import RaiseStatement +from codegen.sdk.core.statements.return_statement import ReturnStatement +from codegen.sdk.core.statements.statement import Statement +from codegen.sdk.core.statements.statement import StatementType +from codegen.sdk.core.statements.switch_case import SwitchCase +from codegen.sdk.core.statements.switch_statement import SwitchStatement +from codegen.sdk.core.statements.symbol_statement import SymbolStatement +from codegen.sdk.core.statements.try_catch_statement import TryCatchStatement +from codegen.sdk.core.statements.while_statement import WhileStatement +from codegen.sdk.core.symbol import Symbol +from codegen.sdk.core.symbol_group import SymbolGroup +from codegen.sdk.core.symbol_groups.comment_group import CommentGroup +from codegen.sdk.core.symbol_groups.dict import Dict +from codegen.sdk.core.symbol_groups.dict import Pair +from codegen.sdk.core.symbol_groups.expression_group import ExpressionGroup +from codegen.sdk.core.symbol_groups.list import List +from codegen.sdk.core.symbol_groups.multi_line_collection import MultiLineCollection +from codegen.sdk.core.symbol_groups.tuple import Tuple +from codegen.sdk.core.type_alias import TypeAlias +from codegen.sdk.enums import ImportType +from codegen.sdk.python.assignment import PyAssignment +from codegen.sdk.python.class_definition import PyClass +from codegen.sdk.python.detached_symbols.code_block import PyCodeBlock +from codegen.sdk.python.detached_symbols.decorator import PyDecorator +from codegen.sdk.python.detached_symbols.parameter import PyParameter +from codegen.sdk.python.expressions.chained_attribute import PyChainedAttribute +from codegen.sdk.python.expressions.conditional_expression import PyConditionalExpression +from codegen.sdk.python.expressions.generic_type import PyGenericType +from codegen.sdk.python.expressions.named_type import PyNamedType +from codegen.sdk.python.expressions.string import PyString +from codegen.sdk.python.expressions.union_type import PyUnionType +from codegen.sdk.python.file import PyFile +from codegen.sdk.python.function import PyFunction +from codegen.sdk.python.import_resolution import PyImport +from codegen.sdk.python.interfaces.has_block import PyHasBlock +from codegen.sdk.python.placeholder.placeholder_return_type import PyReturnTypePlaceholder +from codegen.sdk.python.statements.assignment_statement import PyAssignmentStatement +from codegen.sdk.python.statements.attribute import PyAttribute +from codegen.sdk.python.statements.block_statement import PyBlockStatement +from codegen.sdk.python.statements.break_statement import PyBreakStatement +from codegen.sdk.python.statements.catch_statement import PyCatchStatement +from codegen.sdk.python.statements.comment import PyComment +from codegen.sdk.python.statements.comment import PyCommentType +from codegen.sdk.python.statements.for_loop_statement import PyForLoopStatement +from codegen.sdk.python.statements.if_block_statement import PyIfBlockStatement +from codegen.sdk.python.statements.import_statement import PyImportStatement +from codegen.sdk.python.statements.match_case import PyMatchCase +from codegen.sdk.python.statements.match_statement import PyMatchStatement +from codegen.sdk.python.statements.pass_statement import PyPassStatement +from codegen.sdk.python.statements.try_catch_statement import PyTryCatchStatement +from codegen.sdk.python.statements.while_statement import PyWhileStatement +from codegen.sdk.python.statements.with_statement import WithStatement +from codegen.sdk.python.symbol import PySymbol +from codegen.sdk.python.symbol_groups.comment_group import PyCommentGroup +from codegen.sdk.typescript.assignment import TSAssignment +from codegen.sdk.typescript.class_definition import TSClass +from codegen.sdk.typescript.detached_symbols.code_block import TSCodeBlock +from codegen.sdk.typescript.detached_symbols.decorator import TSDecorator +from codegen.sdk.typescript.detached_symbols.jsx.element import JSXElement +from codegen.sdk.typescript.detached_symbols.jsx.expression import JSXExpression +from codegen.sdk.typescript.detached_symbols.jsx.prop import JSXProp +from codegen.sdk.typescript.detached_symbols.parameter import TSParameter +from codegen.sdk.typescript.enum_definition import TSEnum +from codegen.sdk.typescript.export import TSExport +from codegen.sdk.typescript.expressions.array_type import TSArrayType +from codegen.sdk.typescript.expressions.chained_attribute import TSChainedAttribute +from codegen.sdk.typescript.expressions.conditional_type import TSConditionalType +from codegen.sdk.typescript.expressions.expression_type import TSExpressionType +from codegen.sdk.typescript.expressions.function_type import TSFunctionType +from codegen.sdk.typescript.expressions.generic_type import TSGenericType +from codegen.sdk.typescript.expressions.lookup_type import TSLookupType +from codegen.sdk.typescript.expressions.named_type import TSNamedType +from codegen.sdk.typescript.expressions.object_type import TSObjectType +from codegen.sdk.typescript.expressions.query_type import TSQueryType +from codegen.sdk.typescript.expressions.readonly_type import TSReadonlyType +from codegen.sdk.typescript.expressions.string import TSString +from codegen.sdk.typescript.expressions.ternary_expression import TSTernaryExpression +from codegen.sdk.typescript.expressions.undefined_type import TSUndefinedType +from codegen.sdk.typescript.expressions.union_type import TSUnionType +from codegen.sdk.typescript.file import TSFile +from codegen.sdk.typescript.function import TSFunction +from codegen.sdk.typescript.import_resolution import TSImport +from codegen.sdk.typescript.interface import TSInterface +from codegen.sdk.typescript.interfaces.has_block import TSHasBlock +from codegen.sdk.typescript.namespace import TSNamespace +from codegen.sdk.typescript.placeholder.placeholder_return_type import TSReturnTypePlaceholder +from codegen.sdk.typescript.statements.assignment_statement import TSAssignmentStatement +from codegen.sdk.typescript.statements.attribute import TSAttribute +from codegen.sdk.typescript.statements.block_statement import TSBlockStatement +from codegen.sdk.typescript.statements.catch_statement import TSCatchStatement +from codegen.sdk.typescript.statements.comment import TSComment +from codegen.sdk.typescript.statements.comment import TSCommentType +from codegen.sdk.typescript.statements.for_loop_statement import TSForLoopStatement +from codegen.sdk.typescript.statements.if_block_statement import TSIfBlockStatement +from codegen.sdk.typescript.statements.import_statement import TSImportStatement +from codegen.sdk.typescript.statements.labeled_statement import TSLabeledStatement +from codegen.sdk.typescript.statements.switch_case import TSSwitchCase +from codegen.sdk.typescript.statements.switch_statement import TSSwitchStatement +from codegen.sdk.typescript.statements.try_catch_statement import TSTryCatchStatement +from codegen.sdk.typescript.statements.while_statement import TSWhileStatement +from codegen.sdk.typescript.symbol import TSSymbol +from codegen.sdk.typescript.symbol_groups.comment_group import TSCommentGroup +from codegen.sdk.typescript.symbol_groups.dict import TSDict +from codegen.sdk.typescript.symbol_groups.dict import TSPair +from codegen.sdk.typescript.ts_config import TSConfig +from codegen.sdk.typescript.type_alias import TSTypeAlias +""" diff --git a/src/codegen.backup/shared/compilation/string_to_code.py b/src/codegen.backup/shared/compilation/string_to_code.py new file mode 100644 index 000000000..e526b6d7e --- /dev/null +++ b/src/codegen.backup/shared/compilation/string_to_code.py @@ -0,0 +1,109 @@ +import linecache +import sys +import traceback +from collections.abc import Callable +from typing import Any + +from codegen.shared.compilation.codeblock_validation import check_for_dangerous_operations +from codegen.shared.compilation.exception_utils import get_local_frame, get_offset_traceback +from codegen.shared.compilation.function_compilation import safe_compile_function_string +from codegen.shared.compilation.function_construction import create_function_str_from_codeblock, get_imports_string +from codegen.shared.exceptions.control_flow import StopCodemodException +from codegen.shared.logging.get_logger import get_logger + +logger = get_logger(__name__) + + +def create_execute_function_from_codeblock(codeblock: str, custom_scope: dict | None = None, func_name: str = "execute") -> Callable: + """Convert a user code string into a Callable that takes in a Codebase. + + Steps: + 1. Check for any dangerous operations in the codeblock. Will raise DangerousUserCodeException if any dangerous operations are found. + 2. Create a function string from the codeblock. Ex: "def execute(codebase: Codebase): ..." + 3. Compile the function string into a Callable that takes in a Codebase. Will raise InvalidUserCodeException if there are any code errors (ex: IndentationErrors) + 4. Wrap the function in another function (that also takes in a Codebase) that handles calling the function and safely handling any exceptions occur during execution. + + Args: + codeblock (str): The user code to construct the Callable with (usually CodemodVersionModel.source) + custom_scope (dict | None, optional): Custom scope to be used during compilation. Defaults to None. + func_name (str, optional): Name of the function to be created. Defaults to "execute". + + Returns: + Callable: def (codebase: Codebase) -> any | dict + + Raises: + UnsafeUserCodeException: If the user's code contains dangerous operations. + InvalidUserCodeException: If there are syntax errors in the provided code. + """ + # =====[ Set up custom scope ]===== + custom_scope = custom_scope or {} + logger.info(f"create_execute_function custom_scope: {custom_scope.keys()}") + + # =====[ Check for dangerous operations in the codeblock ]===== + check_for_dangerous_operations(codeblock) + # =====[ Create function string from codeblock ]===== + func_str = create_function_str_from_codeblock(codeblock, func_name) + # =====[ Compile the function string into a function ]===== + func = safe_compile_function_string(custom_scope=custom_scope, func_name=func_name, func_str=func_str) + + # =====[ Compute line offset of func_str ]===== + # This is to generate the a traceback with the correct line window + len_imports = len(get_imports_string().split("\n")) + len_func_str = 1 + line_offset = len_imports + len_func_str + + # =====[ Create closure function to enclose outer scope variables]===== + def closure_func() -> Callable[[Any], None]: + """Wrap user code in a closure to capture the outer scope variables and format errors.""" + _func_str = func_str + _line_offset = line_offset + + # Wrap the func for better tracing + def wrapped_func(*args, **kwargs): + """Wraps the user code to capture and format exceptions + grab locals""" + try: + linecache.cache[""] = (len(_func_str), None, _func_str.splitlines(True), "") + func(*args, **kwargs) + + # =====[ Grab locals during `StopCodemodException` ]===== + except StopCodemodException as e: + logger.info(f"Stopping codemod due to {e.__class__.__name__}: {e}") + raise e + + except Exception as e: + # =====[ Get offset, filtered traceback message ]===== + tb_lines = traceback.format_exception(type(e), e, e.__traceback__) + error_message = get_offset_traceback(tb_lines, _line_offset, filenameFilter="") + + # =====[ Find frame in user's code ]===== + exc_type, exc_value, exc_traceback = sys.exc_info() + frame = get_local_frame(exc_type, exc_value, exc_traceback) + # TODO: handle frame is None + line_num = frame.f_lineno + + # =====[ Get context lines ]===== + context_start = max(0, line_num - 3) + context_end = min(len(func_str.split("\n")), line_num + 2) + context_lines = func_str.split("\n")[context_start:context_end] + + # =====[ Format error message with context ]===== + error_lines = [] + for i, line in enumerate(context_lines, start=context_start + 1): + marker = ">" if i == line_num else " " + error_lines.append(f"{marker} {i - _line_offset}: {line.rstrip()}") + error_context = "\n".join(error_lines) + + # =====[ Format error message ]===== + error_message = ( + error_message + + f""" + +Code context: +{error_context} +""" + ) + raise RuntimeError(error_message) from e + + return wrapped_func + + return closure_func() diff --git a/src/codegen.backup/shared/decorators/docs.py b/src/codegen.backup/shared/decorators/docs.py new file mode 100644 index 000000000..a7a1aaa49 --- /dev/null +++ b/src/codegen.backup/shared/decorators/docs.py @@ -0,0 +1,91 @@ +import bisect +import inspect +from collections.abc import Callable +from dataclasses import dataclass +from typing import TypeVar + + +@dataclass +class DocumentedObject: + name: str + module: str + object: any + + def __lt__(self, other): + return self.module < other.module + + def signature(self) -> str: + return f"{self.name}" + + +apidoc_objects: list[DocumentedObject] = [] + + +def apidoc(obj): + """Decorator for objects that will be used as API documentation for AI-agent prompts.""" + obj._apidoc = True + obj._api_doc_lang = "core" + if doc_obj := get_documented_object(obj): + bisect.insort(apidoc_objects, doc_obj) + return obj + + +py_apidoc_objects: list[DocumentedObject] = [] + + +def py_apidoc(obj): + """Decorator for objects that will be used as Python API documentation for AI-agent prompts.""" + obj._py_apidoc = True + obj._api_doc_lang = "python" + if doc_obj := get_documented_object(obj): + bisect.insort(py_apidoc_objects, doc_obj) + return obj + + +ts_apidoc_objects: list[DocumentedObject] = [] + + +def ts_apidoc(obj): + """Decorator for objects that will be used as Typescript API documentation for AI-agent prompts.""" + obj._ts_apidoc = True + obj._api_doc_lang = "typescript" + if doc_obj := get_documented_object(obj): + bisect.insort(ts_apidoc_objects, doc_obj) + return obj + + +no_apidoc_objects: list[DocumentedObject] = [] +no_apidoc_signatures: set[str] = set() + +T = TypeVar("T", bound=Callable) + + +def noapidoc(obj: T) -> T: + """Decorator for things that are hidden from the API documentation for AI-agent prompts.""" + obj._apidoc = False + obj._api_doc_lang = None + if doc_obj := get_documented_object(obj): + bisect.insort(no_apidoc_objects, doc_obj) + no_apidoc_signatures.add(doc_obj.signature()) + return obj + + +py_no_apidoc_objects: list[DocumentedObject] = [] +py_no_apidoc_signatures: set[str] = set() + + +def py_noapidoc(obj: T) -> T: + """Decorator for things that are hidden from the Python API documentation for AI-agent prompts.""" + obj._py_apidoc = False + obj._api_doc_lang = "python" + if doc_obj := get_documented_object(obj): + bisect.insort(py_no_apidoc_objects, doc_obj) + py_no_apidoc_signatures.add(doc_obj.signature()) + return obj + + +def get_documented_object(obj) -> DocumentedObject | None: + module = inspect.getmodule(obj) + module_name = module.__name__ if module else "" + if module_name: + return DocumentedObject(name=obj.__name__, module=module_name, object=obj) diff --git a/src/codegen.backup/shared/enums/programming_language.py b/src/codegen.backup/shared/enums/programming_language.py new file mode 100644 index 000000000..28eb52d2b --- /dev/null +++ b/src/codegen.backup/shared/enums/programming_language.py @@ -0,0 +1,8 @@ +from enum import StrEnum + + +class ProgrammingLanguage(StrEnum): + TYPESCRIPT = "TYPESCRIPT" + PYTHON = "PYTHON" + OTHER = "OTHER" + UNSUPPORTED = "UNSUPPORTED" diff --git a/src/codegen.backup/shared/exceptions/api.py b/src/codegen.backup/shared/exceptions/api.py new file mode 100644 index 000000000..5a229bbec --- /dev/null +++ b/src/codegen.backup/shared/exceptions/api.py @@ -0,0 +1,2 @@ +class APINotApplicableForLanguageError(Exception): + pass diff --git a/src/codegen.backup/shared/exceptions/compilation.py b/src/codegen.backup/shared/exceptions/compilation.py new file mode 100644 index 000000000..e9a274e61 --- /dev/null +++ b/src/codegen.backup/shared/exceptions/compilation.py @@ -0,0 +1,10 @@ +class UserCodeException(Exception): + """Custom exception for any issues in user code.""" + + +class DangerousUserCodeException(UserCodeException): + """Custom exception user code that has dangerous / not permitted operations.""" + + +class InvalidUserCodeException(UserCodeException): + """Custom exception for user code that can be compiled/executed. Ex: syntax errors, indentation errors, name errors etc.""" diff --git a/src/codegen.backup/shared/exceptions/control_flow.py b/src/codegen.backup/shared/exceptions/control_flow.py new file mode 100644 index 000000000..ba26ffcaf --- /dev/null +++ b/src/codegen.backup/shared/exceptions/control_flow.py @@ -0,0 +1,28 @@ +class StopCodemodException(Exception): + """Raises when the codemod execution should stop early. + This gets caught upstream and causes an early exit so that we can surface a subset of the results to the user for faster iteration. + """ + + threshold: int | None = None + + def __init__(self, message: str | None = None, threshold: int | None = None): + super().__init__(message) + self.threshold = threshold + + +class MaxTransactionsExceeded(StopCodemodException): + """Raised when the number of transactions exceeds the max_transactions limit. + This gets caught upstream and causes an early exit so that we can surface a subset of the results to the user for faster iteration. + """ + + +class MaxPreviewTimeExceeded(StopCodemodException): + """Raised when more than the allotted time has passed for previewing transactions. Enables us to keep it at like ~5s in the frontend during debugging""" + + +class MaxAIRequestsError(StopCodemodException): + """Raised when the number of AI requests exceeds the max_ai_requests limit. + + This gets caught upstream and causes an early exit so that we can surface a subset of the + results to the user for faster iteration. + """ diff --git a/src/codegen.backup/shared/logging/get_logger.py b/src/codegen.backup/shared/logging/get_logger.py new file mode 100644 index 000000000..677363bdf --- /dev/null +++ b/src/codegen.backup/shared/logging/get_logger.py @@ -0,0 +1,157 @@ +import logging +import sys + +import colorlog + +formatter = colorlog.ColoredFormatter( + "%(white)s%(asctime)s - %(name)s - %(log_color)s%(levelname)s%(reset)s%(white)s - %(message_log_color)s%(message)s", + log_colors={ + "DEBUG": "white", + "INFO": "green", + "WARNING": "yellow", + "ERROR": "red", + "CRITICAL": "red,bg_white", + }, + secondary_log_colors={ + "message": { + "DEBUG": "cyan", + "INFO": "white", + "WARNING": "yellow", + "ERROR": "red", + "CRITICAL": "red,bg_white", + } + }, +) + + +class StdOutFilter(logging.Filter): + def filter(self, record): + return record.levelno < logging.ERROR + + +class StdErrFilter(logging.Filter): + def filter(self, record): + return record.levelno >= logging.ERROR + + +# Create handlers +stdout_handler = logging.StreamHandler(sys.stdout) # Logs to stdout +stdout_handler.setFormatter(formatter) +stdout_handler.addFilter(StdOutFilter()) + +stderr_handler = logging.StreamHandler(sys.stderr) # Logs to stderr +stderr_handler.setFormatter(formatter) +stderr_handler.addFilter(StdErrFilter()) + +# Global OpenTelemetry handler (lazy-loaded) +_otel_handler = None +_otel_handler_checked = False + +# Global telemetry config cache +_telemetry_config = None +_telemetry_config_checked = False + + +def _get_telemetry_config(): + """Get telemetry configuration for debug mode checking.""" + global _telemetry_config, _telemetry_config_checked + + if _telemetry_config_checked: + return _telemetry_config + + _telemetry_config_checked = True + + try: + # Use non-prompting config loader to avoid consent prompts during logging setup + from codegen.configs.models.telemetry import TelemetryConfig + from codegen.configs.constants import GLOBAL_ENV_FILE + + _telemetry_config = TelemetryConfig(env_filepath=GLOBAL_ENV_FILE) + except ImportError: + # Telemetry dependencies not available + _telemetry_config = None + except Exception: + # Other setup errors - fallback to console logging + _telemetry_config = None + + return _telemetry_config + + +def _get_otel_handler(): + """Get OpenTelemetry handler if available and enabled.""" + global _otel_handler, _otel_handler_checked + + if _otel_handler_checked: + return _otel_handler + + _otel_handler_checked = True + + try: + from codegen.cli.telemetry.otel_setup import get_otel_logging_handler + + _otel_handler = get_otel_logging_handler() + except ImportError: + # OTel dependencies not available + _otel_handler = None + except Exception: + # Other setup errors + _otel_handler = None + + return _otel_handler + + +def get_logger(name: str, level: int = logging.INFO) -> logging.Logger: + logger = _setup_logger(name, level) + # Note: Global exception handling is managed by cli/telemetry/exception_logger.py + return logger + + +def refresh_telemetry_config(): + """Refresh the cached telemetry configuration. + + This should be called when telemetry settings change to ensure + logging behavior updates accordingly. + """ + global _telemetry_config_checked, _telemetry_config + _telemetry_config_checked = False + _telemetry_config = None + + +def _setup_logger(name: str, level: int = logging.INFO) -> logging.Logger: + # Force configure the root logger with a NullHandler to prevent duplicate logs + logging.basicConfig(handlers=[logging.NullHandler()], force=True) + logger = logging.getLogger(name) + if logger.hasHandlers(): + for h in logger.handlers: + logger.removeHandler(h) + + # Check telemetry configuration to determine console logging behavior + telemetry_config = _get_telemetry_config() + + # Only add console handlers if: + # 1. Telemetry is not configured (default behavior) + # 2. Telemetry debug mode is enabled + # 3. Telemetry is disabled (fallback to console logging) + should_log_to_console = ( + telemetry_config is None # Telemetry not configured + or telemetry_config.debug # Debug mode enabled + or not telemetry_config.enabled # Telemetry disabled + ) + + if should_log_to_console: + logger.addHandler(stdout_handler) + logger.addHandler(stderr_handler) + + # Always add OpenTelemetry handler if telemetry is enabled (regardless of debug mode) + otel_handler = _get_otel_handler() + if otel_handler is not None: + logger.addHandler(otel_handler) + + # Ensure the logger propagates to the root logger + logger.propagate = True + # Set the level on the logger itself + logger.setLevel(level) + return logger + + +# Note: Exception logging is handled by cli/telemetry/exception_logger.py diff --git a/src/codegen.backup/shared/network/port.py b/src/codegen.backup/shared/network/port.py new file mode 100644 index 000000000..d704ec63c --- /dev/null +++ b/src/codegen.backup/shared/network/port.py @@ -0,0 +1,17 @@ +import socket +from contextlib import closing + + +def get_free_port() -> int: + """Find and return a free port on localhost""" + with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: + s.bind(("", 0)) + s.listen(1) + port = s.getsockname()[1] + return int(port) + + +def is_port_free(port: int, host: str = "localhost") -> bool: + """Check if a port is free on localhost""" + with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: + return s.connect_ex((host, port)) != 0 diff --git a/src/codegen.backup/shared/path.py b/src/codegen.backup/shared/path.py new file mode 100644 index 000000000..a541c42cd --- /dev/null +++ b/src/codegen.backup/shared/path.py @@ -0,0 +1,15 @@ +import os +import subprocess +from pathlib import Path + + +def get_git_root_path(path: Path | None = None) -> Path | None: + """Get the closest root of the git repository containing the given path""" + try: + path = path or Path.cwd() + path = path.resolve() + os.chdir(path) + output = subprocess.run(["git", "rev-parse", "--show-toplevel"], capture_output=True, check=True, text=True) + return Path(output.stdout.strip()) + except (subprocess.CalledProcessError, FileNotFoundError): + return None diff --git a/src/codegen.backup/shared/performance/memory_utils.py b/src/codegen.backup/shared/performance/memory_utils.py new file mode 100644 index 000000000..bd04a7e3d --- /dev/null +++ b/src/codegen.backup/shared/performance/memory_utils.py @@ -0,0 +1,20 @@ +import os +from dataclasses import dataclass + +import psutil + + +@dataclass +class MemoryStats: + memory_rss_gb: float + memory_vms_gb: float + + +def get_memory_stats() -> MemoryStats: + process = psutil.Process(os.getpid()) + memory_info = process.memory_info() + + return MemoryStats( + memory_rss_gb=memory_info.rss / 1024 / 1024 / 1024, + memory_vms_gb=memory_info.vms / 1024 / 1024 / 1024, + ) diff --git a/src/codegen.backup/shared/performance/stopwatch_utils.py b/src/codegen.backup/shared/performance/stopwatch_utils.py new file mode 100644 index 000000000..d0ea64528 --- /dev/null +++ b/src/codegen.backup/shared/performance/stopwatch_utils.py @@ -0,0 +1,52 @@ +import subprocess +import time +from functools import wraps +from typing import cast + +import sentry_sdk + +from codegen.shared.logging.get_logger import get_logger +from codegen.shared.performance.time_utils import humanize_duration + +logger = get_logger(__name__) + + +def stopwatch(func): + @wraps(func) + def wrapper(*args, **kwargs): + start_time = time.perf_counter() + result = func(*args, **kwargs) + end_time = time.perf_counter() + execution_time = end_time - start_time + logger.info(f"Function '{func.__name__}' took {humanize_duration(execution_time)} to execute.") + return result + + return wrapper + + +def stopwatch_with_sentry(name: str): + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + with sentry_sdk.start_transaction(name=name): + start_time = time.perf_counter() + res = func(*args, **kwargs) + end_time = time.perf_counter() + execution_time = end_time - start_time + logger.info(f"Function '{func.__name__}' took {humanize_duration(execution_time)} to execute.") + return res + + return wrapper + + return decorator + + +def subprocess_with_stopwatch(command, command_desc: str | None = None, *args, **kwargs) -> subprocess.CompletedProcess[str]: + start_time = time.time() + # Ensure text=True to get string output instead of bytes + kwargs.setdefault("text", True) + result = subprocess.run(command, *args, **kwargs) + end_time = time.time() + logger.info(f"Command '{command_desc or command}' took {end_time - start_time} seconds to execute.") + # Cast to the correct type since we set text=True + return cast("subprocess.CompletedProcess[str]", result) diff --git a/src/codegen.backup/shared/performance/time_utils.py b/src/codegen.backup/shared/performance/time_utils.py new file mode 100644 index 000000000..d8c3c7851 --- /dev/null +++ b/src/codegen.backup/shared/performance/time_utils.py @@ -0,0 +1,11 @@ +import datetime as dt + +import humanize + + +def humanize_duration(seconds: float) -> str: + """Converts a duration in seconds to a human-readable string. + Example: humanize_duration(60) -> "1 minute" + """ + delta = dt.timedelta(seconds=seconds) + return humanize.precisedelta(delta, minimum_unit="milliseconds") diff --git a/src/codegen.backup/shared/string/csv_utils.py b/src/codegen.backup/shared/string/csv_utils.py new file mode 100644 index 000000000..3cd4fd366 --- /dev/null +++ b/src/codegen.backup/shared/string/csv_utils.py @@ -0,0 +1,21 @@ +def list_to_comma_separated(items: list[str]) -> str: + """Given a list of items, returns a comma separated string of the items""" + return ",".join(items) + + +def comma_separated_to_list(comma_separated: str) -> list[str]: + """Given a comma separated string, returns a list of the comma separated items. + Strips whitespace from each item, drops any items that are whitespace only + """ + items = comma_separated.split(",") if comma_separated else [] + non_empty_items = [item.strip() for item in items if item.strip()] + return non_empty_items + + +def comma_separated_to_set(comma_separated: str) -> set[str]: + """Given a comma separated string, returns a set of the comma separated items. + Strips whitespace from each item, drops any items that are whitespace only + """ + items = comma_separated.split(",") if comma_separated else [] + non_empty_items = {item.strip() for item in items if item.strip()} + return non_empty_items diff --git a/src/codegen/exports.py b/src/codegen/exports.py index fe9bba50c..5363e7afa 100644 --- a/src/codegen/exports.py +++ b/src/codegen/exports.py @@ -6,8 +6,8 @@ """ from codegen.agents.agent import Agent -from codegen.sdk.core.codebase import Codebase # type: ignore[import-untyped] -from codegen.sdk.core.function import Function # type: ignore[import-untyped] +from codegen.sdk.core.codebase import Codebase +from codegen.sdk.core.function import Function from codegen.shared.enums.programming_language import ProgrammingLanguage __all__ = [ diff --git a/src/codegen/sdk/_proxy.py b/src/codegen/sdk/_proxy.py new file mode 100644 index 000000000..290b73886 --- /dev/null +++ b/src/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/src/codegen/sdk/ai/client.py b/src/codegen/sdk/ai/client.py new file mode 100644 index 000000000..8902a2fa1 --- /dev/null +++ b/src/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/src/codegen/sdk/ai/utils.py b/src/codegen/sdk/ai/utils.py new file mode 100644 index 000000000..b903a9a1a --- /dev/null +++ b/src/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/src/codegen/sdk/cli/README.md b/src/codegen/sdk/cli/README.md new file mode 100644 index 000000000..101f1b034 --- /dev/null +++ b/src/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/sdk/cli/__init__.py b/src/codegen/sdk/cli/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/codegen/sdk/cli/_env.py b/src/codegen/sdk/cli/_env.py new file mode 100644 index 000000000..5a12ba1d0 --- /dev/null +++ b/src/codegen/sdk/cli/_env.py @@ -0,0 +1 @@ +ENV = "" diff --git a/src/codegen/sdk/cli/auth/constants.py b/src/codegen/sdk/cli/auth/constants.py new file mode 100644 index 000000000..84849c81c --- /dev/null +++ b/src/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/src/codegen/sdk/cli/auth/session.py b/src/codegen/sdk/cli/auth/session.py new file mode 100644 index 000000000..650990d0c --- /dev/null +++ b/src/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/src/codegen/sdk/cli/cli.py b/src/codegen/sdk/cli/cli.py new file mode 100644 index 000000000..21b14c840 --- /dev/null +++ b/src/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/src/codegen/sdk/cli/codemod/convert.py b/src/codegen/sdk/cli/codemod/convert.py new file mode 100644 index 000000000..f88d570f5 --- /dev/null +++ b/src/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/src/codegen/sdk/cli/commands/config/main.py b/src/codegen/sdk/cli/commands/config/main.py new file mode 100644 index 000000000..f692be59b --- /dev/null +++ b/src/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/src/codegen/sdk/cli/commands/create/main.py b/src/codegen/sdk/cli/commands/create/main.py new file mode 100644 index 000000000..ec9c4b73d --- /dev/null +++ b/src/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/src/codegen/sdk/cli/commands/init/main.py b/src/codegen/sdk/cli/commands/init/main.py new file mode 100644 index 000000000..bb71caf73 --- /dev/null +++ b/src/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/src/codegen/sdk/cli/commands/init/render.py b/src/codegen/sdk/cli/commands/init/render.py new file mode 100644 index 000000000..7c7ee42ed --- /dev/null +++ b/src/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/src/codegen/sdk/cli/commands/list/main.py b/src/codegen/sdk/cli/commands/list/main.py new file mode 100644 index 000000000..e03c998b5 --- /dev/null +++ b/src/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