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()