diff --git a/docs/cli/about.mdx b/docs/cli/about.mdx index c9b890a92..8d4fa938b 100644 --- a/docs/cli/about.mdx +++ b/docs/cli/about.mdx @@ -1,7 +1,7 @@ --- title: "Codegen CLI" sidebarTitle: "Overview" -icon: "terminal" +icon: "square-info" iconType: "solid" --- diff --git a/docs/cli/notebook.mdx b/docs/cli/notebook.mdx new file mode 100644 index 000000000..343856970 --- /dev/null +++ b/docs/cli/notebook.mdx @@ -0,0 +1,74 @@ +--- +title: "notebook" +sidebarTitle: "notebook" +description: "Open a Jupyter notebook with the current codebase loaded" +icon: "book" +iconType: "solid" +--- + +The `notebook` command launches a Jupyter Lab instance with a pre-configured notebook for exploring your codebase. + +## Usage + +```bash +codegen notebook [--background] +``` + +### Options + +- `--background`: Run Jupyter Lab in the background (default: false) + +By default, Jupyter Lab runs in the foreground, making it easy to stop with Ctrl+C. Use `--background` if you want to run it in the background. + +## What it Does + +This will: + +1. Create a Python virtual environment in `.codegen/.venv` if it doesn't exist +2. Install required packages (codegen and jupyterlab) +3. Create a starter notebook in `.codegen/jupyter/tmp.ipynb` +4. Launch Jupyter Lab with the notebook open + +The notebook comes pre-configured with: + +```python +from codegen import Codebase + +# Initialize codebase +codebase = Codebase('../../') +``` + + + The notebook is created in `.codegen/jupyter/`, so the relative path `../../` + points to your repository root. + + +## Directory Structure + +After running `codegen notebook`, your repository will have this structure: + +``` +.codegen/ +├── .venv/ # Virtual environment for this project +├── jupyter/ # Jupyter notebooks +│ └── tmp.ipynb # Pre-configured notebook +└── config.toml # Project configuration +``` + +## Examples + +```bash +# Run Jupyter in the foreground (default) +codegen notebook + +# Run Jupyter in the background +codegen notebook --background + +# Initialize a new repository and launch notebook +codegen init && codegen notebook +``` + + + The notebook command will automatically initialize codegen if needed, so you + can run it directly in a new repository. + diff --git a/docs/introduction/getting-started.mdx b/docs/introduction/getting-started.mdx index 0d3624c7a..ab8d8c9da 100644 --- a/docs/introduction/getting-started.mdx +++ b/docs/introduction/getting-started.mdx @@ -5,48 +5,55 @@ icon: "bolt" iconType: "solid" --- -Follow our step-by-step tutorial to get up and running with Codegen in a jupyter notebook. +Follow our step-by-step tutorial to get up and running with Codegen. ## Installation -We recommend using [`uv`](https://github.com/astral-sh/uv) with Python 3.13 for the best experience. +We recommend using [`pipx`](https://pypa.github.io/pipx/) to install Codegen globally. `pipx` is a tool to help you install and run Python applications in isolated environments. This isolation ensures that Codegen's dependencies won't conflict with your other Python projects. -First, install `uv` if you haven't already: +First, install `pipx` if you haven't already: ```bash -brew install uv +brew install pipx +pipx ensurepath # Ensure pipx binaries are on your PATH ``` -`cd` into the directory you want to parse: +Then install Codegen globally: ```bash -cd path/to/git/repository +pipx install codegen ``` -Create and activate a Python 3.13 virtual environment: + + This makes the `codegen` command available globally in your terminal, while + keeping its dependencies isolated. + -```bash -uv venv --python 3.13.0 && source .venv/bin/activate -``` +## Quick Start with Jupyter -Install [Codegen from PyPI](https://pypi.org/project/codegen/) using `uv`: +The fastest way to explore a codebase is using Jupyter. Codegen provides a built-in command to set this up: ```bash -uv pip install codegen -``` +# Navigate to your repository +cd path/to/git/repository -Install and run Jupyter (optional): +# Initialize codegen and launch Jupyter +codegen init +codegen notebook +``` -Jupyter notebooks are great for exploration and debugging. +This will: -```bash -uv pip install jupyterlab && jupyter lab -``` +1. Create a `.codegen/` directory with: + - `.venv/` - A dedicated virtual environment for this project + - `jupyter/` - Jupyter notebooks for exploring your code + - `config.toml` - Project configuration +2. Launch Jupyter Lab with a pre-configured notebook - - You can also use the CLI if you prefer to work in a terminal. [Learn more - here](/cli/about). - + + The notebook comes pre-configured to load your codebase, so you can start + exploring right away! + ## Initializing a Codebase diff --git a/docs/mint.json b/docs/mint.json index f41a8d355..668c81ed1 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -114,7 +114,14 @@ }, { "group": "CLI", - "pages": ["cli/about", "cli/init", "cli/create", "cli/run", "cli/expert"] + "pages": [ + "cli/about", + "cli/init", + "cli/notebook", + "cli/create", + "cli/run", + "cli/expert" + ] }, { "group": "Changelog", diff --git a/src/codegen/cli/cli.py b/src/codegen/cli/cli.py index 0d115c820..ce7e28b13 100644 --- a/src/codegen/cli/cli.py +++ b/src/codegen/cli/cli.py @@ -8,6 +8,7 @@ from codegen.cli.commands.list.main import list_command from codegen.cli.commands.login.main import login_command from codegen.cli.commands.logout.main import logout_command +from codegen.cli.commands.notebook.main import notebook_command from codegen.cli.commands.profile.main import profile_command from codegen.cli.commands.run.main import run_command from codegen.cli.commands.run_on_pr.main import run_on_pr_command @@ -35,6 +36,7 @@ def main(): main.add_command(deploy_command) main.add_command(style_debug_command) main.add_command(run_on_pr_command) +main.add_command(notebook_command) if __name__ == "__main__": diff --git a/src/codegen/cli/commands/init/main.py b/src/codegen/cli/commands/init/main.py index baee5dc94..50f2309b0 100644 --- a/src/codegen/cli/commands/init/main.py +++ b/src/codegen/cli/commands/init/main.py @@ -1,10 +1,12 @@ import subprocess import sys +from pathlib import Path import rich import rich_click as click +import toml -from codegen.cli.auth.decorators import requires_auth +from codegen.cli.auth.constants import CODEGEN_DIR from codegen.cli.auth.session import CodegenSession from codegen.cli.commands.init.render import get_success_message from codegen.cli.git.url import get_git_organization_and_repo @@ -15,8 +17,8 @@ @click.command(name="init") @click.option("--repo-name", type=str, help="The name of the repository") @click.option("--organization-name", type=str, help="The name of the organization") -@requires_auth -def init_command(session: CodegenSession, repo_name: str | None = None, organization_name: str | None = None): +@click.option("--fetch-docs", is_flag=True, help="Fetch docs and examples (requires auth)") +def init_command(repo_name: str | None = None, organization_name: str | None = None, fetch_docs: bool = False): """Initialize or update the Codegen folder.""" # Print a message if not in a git repo try: @@ -30,27 +32,36 @@ def init_command(session: CodegenSession, repo_name: str | None = None, organiza rich.print(format_command("codegen init")) sys.exit(1) - codegen_dir = session.codegen_dir + # Only create session if we need to fetch docs + session = CodegenSession() if fetch_docs else None + codegen_dir = Path.cwd() / CODEGEN_DIR if not session else session.codegen_dir is_update = codegen_dir.exists() - if organization_name is not None: - session.config.organization_name = organization_name - if repo_name is not None: - session.config.repo_name = repo_name - if not session.config.organization_name or not session.config.repo_name: - cwd_org, cwd_repo = get_git_organization_and_repo(session.git_repo) - session.config.organization_name = session.config.organization_name or cwd_org - session.config.repo_name = session.config.repo_name or cwd_repo - session.write_config() + # Only update config if we have a session + if session: + if organization_name is not None: + session.config.organization_name = organization_name + if repo_name is not None: + session.config.repo_name = repo_name + if not session.config.organization_name or not session.config.repo_name: + cwd_org, cwd_repo = get_git_organization_and_repo(session.git_repo) + session.config.organization_name = session.config.organization_name or cwd_org + session.config.repo_name = session.config.repo_name or cwd_repo + session.write_config() action = "Updating" if is_update else "Initializing" rich.print("") # Add a newline before the spinner - codegen_dir, docs_dir, examples_dir = initialize_codegen(action=action) + codegen_dir, docs_dir, examples_dir = initialize_codegen(action, session=session, fetch_docs=fetch_docs) # Print success message rich.print(f"✅ {action} complete") - rich.print(f" [dim]Organization:[/dim] {session.config.organization_name}") - rich.print(f" [dim]Repository:[/dim] {session.config.repo_name}") + + # Show repo info from config.toml + config_path = codegen_dir / "config.toml" + if config_path.exists(): + config = toml.load(config_path) + rich.print(f" [dim]Organization:[/dim] {config.get('organization_name', 'unknown')}") + rich.print(f" [dim]Repository:[/dim] {config.get('repo_name', 'unknown')}") rich.print("") rich.print(get_success_message(codegen_dir, docs_dir, examples_dir)) diff --git a/src/codegen/cli/commands/init/render.py b/src/codegen/cli/commands/init/render.py index c92738b91..e6a152bd8 100644 --- a/src/codegen/cli/commands/init/render.py +++ b/src/codegen/cli/commands/init/render.py @@ -4,6 +4,6 @@ def get_success_message(codegen_dir: Path, docs_dir: Path, examples_dir: Path) -> str: """Get the success message to display after initialization.""" return """📁 Folders Created: - [dim] Location:[/dim] .codegen-sh - [dim] Docs:[/dim] .codegen-sh/docs - [dim] Examples:[/dim] .codegen-sh/examples""" + [dim] Location:[/dim] .codegen + [dim] Docs:[/dim] .codegen/docs + [dim] Examples:[/dim] .codegen/examples""" diff --git a/src/codegen/cli/commands/notebook/main.py b/src/codegen/cli/commands/notebook/main.py new file mode 100644 index 000000000..29bf92375 --- /dev/null +++ b/src/codegen/cli/commands/notebook/main.py @@ -0,0 +1,75 @@ +import os +import subprocess +from pathlib import Path + +import rich_click as click + +from codegen.cli.auth.constants import CODEGEN_DIR +from codegen.cli.auth.decorators import requires_auth +from codegen.cli.auth.session import CodegenSession +from codegen.cli.rich.spinners import create_spinner +from codegen.cli.workspace.decorators import requires_init +from codegen.cli.workspace.venv_manager import VenvManager + + +def create_jupyter_dir() -> Path: + """Create and return the jupyter directory.""" + jupyter_dir = Path.cwd() / CODEGEN_DIR / "jupyter" + jupyter_dir.mkdir(parents=True, exist_ok=True) + return jupyter_dir + + +def create_notebook(jupyter_dir: Path) -> Path: + """Create a new Jupyter notebook if it doesn't exist.""" + notebook_path = jupyter_dir / "tmp.ipynb" + if not notebook_path.exists(): + notebook_content = { + "cells": [ + { + "cell_type": "code", + "execution_count": None, + "metadata": {}, + "outputs": [], + "source": ["from codegen import Codebase\n", "\n", "# Initialize codebase\n", "codebase = Codebase('../../')\n"], + } + ], + "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}}, + "nbformat": 4, + "nbformat_minor": 4, + } + import json + + notebook_path.write_text(json.dumps(notebook_content, indent=2)) + return notebook_path + + +@click.command(name="notebook") +@click.option("--background", is_flag=True, help="Run Jupyter Lab in the background") +@requires_auth +@requires_init +def notebook_command(session: CodegenSession, background: bool = False): + """Open a Jupyter notebook with the current codebase loaded.""" + with create_spinner("Setting up Jupyter environment...") as status: + venv = VenvManager() + + if not venv.is_initialized(): + status.update("Creating virtual environment...") + venv.create_venv() + + status.update("Installing required packages...") + venv.install_packages("codegen", "jupyterlab") + + jupyter_dir = create_jupyter_dir() + notebook_path = create_notebook(jupyter_dir) + + status.update("Starting Jupyter Lab...") + + # Prepare the environment with the virtual environment activated + env = {**os.environ, "VIRTUAL_ENV": str(venv.venv_dir), "PATH": f"{venv.venv_dir}/bin:{os.environ['PATH']}"} + + # Start Jupyter Lab + if background: + subprocess.Popen(["jupyter", "lab", str(notebook_path)], env=env, start_new_session=True) + else: + # Run in foreground + subprocess.run(["jupyter", "lab", str(notebook_path)], env=env, check=True) diff --git a/src/codegen/cli/utils/codemod_manager.py b/src/codegen/cli/utils/codemod_manager.py index 2055058f3..14353c6df 100644 --- a/src/codegen/cli/utils/codemod_manager.py +++ b/src/codegen/cli/utils/codemod_manager.py @@ -100,7 +100,7 @@ def get_decorated(cls, start_path: Path | None = None) -> builtins.list[Decorate ".ruff_cache", ".coverage", "htmlcov", - ".codegen-sh", + ".codegen", } all_functions = [] diff --git a/src/codegen/cli/workspace/initialize_workspace.py b/src/codegen/cli/workspace/initialize_workspace.py index 278837b09..198d28319 100644 --- a/src/codegen/cli/workspace/initialize_workspace.py +++ b/src/codegen/cli/workspace/initialize_workspace.py @@ -1,26 +1,73 @@ import shutil +from contextlib import nullcontext from pathlib import Path import rich +import toml +from rich.status import Status from codegen.cli.api.client import RestAPI from codegen.cli.auth.constants import CODEGEN_DIR, DOCS_DIR, EXAMPLES_DIR, PROMPTS_DIR from codegen.cli.auth.session import CodegenSession from codegen.cli.git.repo import get_git_repo +from codegen.cli.git.url import get_git_organization_and_repo from codegen.cli.rich.spinners import create_spinner from codegen.cli.workspace.docs_workspace import populate_api_docs from codegen.cli.workspace.examples_workspace import populate_examples - -def initialize_codegen(action: str = "Initializing") -> tuple[Path, Path, Path]: +DEFAULT_CODE = """ +from codegen 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)}") +""" + + +def create_notebook(jupyter_dir: Path) -> Path: + """Create a new Jupyter notebook if it doesn't exist.""" + notebook_path = jupyter_dir / "tmp.ipynb" + if not notebook_path.exists(): + notebook_content = { + "cells": [ + { + "cell_type": "code", + "execution_count": None, + "metadata": {}, + "outputs": [], + "source": [DEFAULT_CODE], + } + ], + "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}}, + "nbformat": 4, + "nbformat_minor": 4, + } + import json + + notebook_path.write_text(json.dumps(notebook_content, indent=2)) + return notebook_path + + +def initialize_codegen( + status: Status | str = "Initializing", + session: CodegenSession | None = None, + fetch_docs: bool = False, +) -> tuple[Path, Path, Path]: """Initialize or update the codegen directory structure and content. Args: - action: The action being performed ("Initializing" or "Updating") + status: Either a Status object to update, or a string action being performed ("Initializing" or "Updating") + session: Optional CodegenSession for fetching docs and examples + fetch_docs: Whether to fetch docs and examples (requires auth) Returns: Tuple of (codegen_folder, docs_folder, examples_folder) - """ repo = get_git_repo() REPO_PATH = Path(repo.workdir) @@ -28,32 +75,59 @@ def initialize_codegen(action: str = "Initializing") -> tuple[Path, Path, Path]: PROMPTS_FOLDER = REPO_PATH / PROMPTS_DIR DOCS_FOLDER = REPO_PATH / DOCS_DIR EXAMPLES_FOLDER = REPO_PATH / EXAMPLES_DIR + CONFIG_PATH = CODEGEN_FOLDER / "config.toml" + JUPYTER_DIR = CODEGEN_FOLDER / "jupyter" + + # If status is a string, create a new spinner + context = create_spinner(f" {status} folders...") if isinstance(status, str) else nullcontext() + + with context as spinner: + status_obj = spinner if isinstance(status, str) else status - with create_spinner(f" {action} folders...") as status: # Create folders if they don't exist CODEGEN_FOLDER.mkdir(parents=True, exist_ok=True) PROMPTS_FOLDER.mkdir(parents=True, exist_ok=True) - DOCS_FOLDER.mkdir(parents=True, exist_ok=True) - EXAMPLES_FOLDER.mkdir(parents=True, exist_ok=True) + JUPYTER_DIR.mkdir(parents=True, exist_ok=True) + if not repo: rich.print("No git repository found. Please run this command in a git repository.") else: - status.update(f" {action} .gitignore...") + status_obj.update(f" {'Updating' if isinstance(status, Status) else status} .gitignore...") modify_gitignore(CODEGEN_FOLDER) - # Always fetch and update docs & examples - status.update("Fetching latest docs & examples...") - shutil.rmtree(DOCS_FOLDER, ignore_errors=True) - shutil.rmtree(EXAMPLES_FOLDER, ignore_errors=True) - - session = CodegenSession() - response = RestAPI(session.token).get_docs() - populate_api_docs(DOCS_FOLDER, response.docs, status) - populate_examples(session, EXAMPLES_FOLDER, response.examples, status) - - # Set programming language - session.config.programming_language = str(response.language) - session.write_config() + # Create or update config.toml with basic repo info + if not session: # Only create if session doesn't exist (it handles config itself) + org_name, repo_name = get_git_organization_and_repo(repo) + config = {} + if CONFIG_PATH.exists(): + config = toml.load(CONFIG_PATH) + config.update( + { + "organization_name": config.get("organization_name", org_name), + "repo_name": config.get("repo_name", repo_name), + } + ) + CONFIG_PATH.write_text(toml.dumps(config)) + + # Create notebook template + create_notebook(JUPYTER_DIR) + + # Only fetch docs and examples if requested and session is provided + if fetch_docs and session: + status_obj.update("Fetching latest docs & examples...") + shutil.rmtree(DOCS_FOLDER, ignore_errors=True) + shutil.rmtree(EXAMPLES_FOLDER, ignore_errors=True) + + DOCS_FOLDER.mkdir(parents=True, exist_ok=True) + EXAMPLES_FOLDER.mkdir(parents=True, exist_ok=True) + + response = RestAPI(session.token).get_docs() + populate_api_docs(DOCS_FOLDER, response.docs, status_obj) + populate_examples(session, EXAMPLES_FOLDER, response.examples, status_obj) + + # Set programming language + session.config.programming_language = str(response.language) + session.write_config() return CODEGEN_FOLDER, DOCS_FOLDER, EXAMPLES_FOLDER diff --git a/src/codegen/cli/workspace/venv_manager.py b/src/codegen/cli/workspace/venv_manager.py new file mode 100644 index 000000000..cb137d060 --- /dev/null +++ b/src/codegen/cli/workspace/venv_manager.py @@ -0,0 +1,44 @@ +import os +import subprocess +from pathlib import Path + +from codegen.cli.auth.constants import CODEGEN_DIR +from codegen.cli.git.repo import get_git_repo + + +class VenvManager: + def __init__(self): + repo = get_git_repo() + self.repo_path = Path(repo.workdir) + self.codegen_dir = self.repo_path / CODEGEN_DIR + self.venv_dir = self.codegen_dir / ".venv" + self.python_path = self.venv_dir / "bin" / "python" + self.pip_path = self.venv_dir / "bin" / "pip" + + def create_venv(self, python_version: str = "3.12") -> None: + """Create a virtual environment using uv.""" + self.codegen_dir.mkdir(parents=True, exist_ok=True) + subprocess.run( + ["uv", "venv", "--python", python_version, str(self.venv_dir)], + check=True, + ) + + def install_packages(self, *packages: str) -> None: + """Install packages into the virtual environment using uv pip.""" + subprocess.run( + ["uv", "pip", "install", *packages], + check=True, + env={**os.environ, "VIRTUAL_ENV": str(self.venv_dir)}, + ) + + def get_activate_command(self) -> str: + """Get the command to activate the virtual environment.""" + return f"source {self.venv_dir}/bin/activate" + + def is_active(self) -> bool: + """Check if a virtual environment is active.""" + return "VIRTUAL_ENV" in os.environ + + def is_initialized(self) -> bool: + """Check if the virtual environment exists and is properly set up.""" + return self.venv_dir.exists() and self.python_path.exists()