Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ uv run whygraph-mcp # launch MCP server on stdio (Ctrl-C to exit)

A root `Makefile` wraps these plus dev-only tooling — `make` lists targets; `make db` / `make db-down` run a DBGate viewer for both databases (via `docker-compose.example.yml`), `make inspect` launches the MCP Inspector.

## Before pushing

CI (`ci-code-checks`) gates every PR on two parallel jobs: **lint** (`uv run ruff check src/ tests/` *and* `uv run ruff format --check src/ tests/` — both, not just the first) and **tests** (`uv run pytest`). Run all three locally before pushing or opening a PR:

```bash
uv run ruff check src/ tests/
uv run ruff format --check src/ tests/ # `ruff check` passing does NOT imply this passes
uv run pytest
```

If `ruff format --check` fails, run `uv run ruff format src/ tests/` to fix it in place, then re-run the check before pushing.

## Architecture

Top-level packages under `src/whygraph/`:
Expand Down
13 changes: 10 additions & 3 deletions docs/getting-started/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,16 @@ From the repo you want to analyze:
whygraph init
```

This creates `.whygraph/whygraph.db`, writes a commented `whygraph.example.toml`, and adds the right
`.gitignore` entries. It's idempotent - run it again any time. It does *not* index CodeGraph yet;
that's the next step.
On a terminal this runs a short guided setup - pick your agent, the analyze/rationale LLMs (with
optional API keys), and the source-control provider (with an optional GitHub token), then review a
summary that masks every secret and confirm. It creates `.whygraph/whygraph.db`, writes a commented
`whygraph.example.toml` (never any secrets) and a ready-to-run `whygraph.toml` (with the secrets you
entered), and adds the right `.gitignore` entries. Every prompt is defaulted, so a bare Enter accepts
it. It's idempotent - run it again any time; an existing `whygraph.toml` is only touched if you ask.
It does *not* index CodeGraph yet; that's the next step.

Prefer no prompts? `whygraph init --yes` (and any non-interactive shell - pipes, CI, the git hooks)
accepts every default without asking, writing a default `whygraph.toml` only if none exists.

## 2. Scan

Expand Down
10 changes: 9 additions & 1 deletion docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,22 @@ Bootstrap the WhyGraph database under `.whygraph/whygraph.db`, write a committab
`whygraph.example.toml` documenting every tunable, and add the right `.gitignore` entries. It's
idempotent - re-running on an initialized project just confirms both databases are present.

On a terminal, `init` runs a guided, arrow-key setup: pick the agent, the analyze/rationale LLMs
(with optional API keys), and the source-control provider (with an optional GitHub token). It shows a
summary that masks every secret, asks *"Write these files?"*, then writes both `whygraph.example.toml`
(secret-free) and a ready-to-run `whygraph.toml` (with the secrets you entered). Every prompt is
defaulted. `--yes` (and any non-TTY invocation) skips the prompts, uses defaults, and never clobbers
an existing `whygraph.toml`.

`init` does **not** index CodeGraph. That happens on [`scan`](#whygraph-scan).

With `--agent X`, it also wires the WhyGraph MCP server into that agent's config. All supported
agents are project-scoped, so the config file is written inside the repo.

| Option | Description |
|---|---|
| `--agent [claude\|codex\|copilot\|cursor\|vscode]` | Wire the MCP server into the named agent's config. |
| `--agent [claude\|codex\|copilot\|cursor\|vscode]` | Wire the MCP server into the named agent's config. On a terminal, skips the interactive agent prompt. |
| `--yes` / `-y` | Accept all defaults without prompting (also implied off a TTY). Writes a default `whygraph.toml` only if none exists. |
| `--print` | Print the MCP snippet to stdout instead of writing any config file. |
| `--list-agents` | List supported agents (with config-file paths) and exit. |
| `--install-assets / --no-install-assets` | Copy the chosen agent's bundled assets into the project. Default: enabled. No-op for agents that ship no asset tree. |
Expand Down
7 changes: 5 additions & 2 deletions docs/reference/configuration.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# Configuration

WhyGraph reads an optional `whygraph.toml` at your repo root. Every field has a built-in default, so
an unedited file behaves exactly as if none were present. `whygraph init` scaffolds a fully-commented
`whygraph.example.toml` for you - copy it to `whygraph.toml` and edit what you need.
an unedited file behaves exactly as if none were present. On a terminal, `whygraph init` walks you
through the common choices (agent, analyze/rationale LLMs + keys, source-control provider + token)
and writes both a fully-commented `whygraph.example.toml` (secret-free, committable) and a ready-to-run
`whygraph.toml` (with any secrets you entered). You can always edit `whygraph.toml` by hand afterwards,
or start from the example. `whygraph init --yes` skips the prompts and uses defaults.

!!! warning "`whygraph.toml` is gitignored - never commit a token"
`init` adds `whygraph.toml` to `.gitignore` precisely because it can hold API keys. Keep it that
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ dependencies = [
"openai>=1.40",
"ollama>=0.3",
"tomli-w>=1.0",
"questionary>=2.0",
]

[project.scripts]
Expand Down
118 changes: 106 additions & 12 deletions src/whygraph/cli/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import sys
from pathlib import Path

import click
Expand Down Expand Up @@ -60,21 +61,45 @@
" left alone."
),
)
@click.option(
"--yes",
"-y",
"yes",
is_flag=True,
help=(
"Accept all defaults without prompting. Also implied whenever"
" stdin is not a TTY (pipes, CI, the git hooks). Writes a"
" default whygraph.toml if none exists and never clobbers an"
" existing one."
),
)
def init_cmd(
agent_name: str | None,
print_only: bool,
list_agents: bool,
install_assets: bool,
skip_preflight: bool,
force: bool,
yes: bool,
) -> None:
"""Initialize the WhyGraph database under ``.whygraph/whygraph.db``.

Also writes a committable ``whygraph.example.toml`` at the project root
(it documents every tunable and ships the built-in defaults — copy it
to ``whygraph.toml`` and edit to customize) and ensures the project's
``.gitignore`` keeps the user-owned config and generated caches out of
git (``whygraph.toml``, ``.whygraph/``, ``.codegraph/``).
On a terminal this runs a guided, arrow-key setup — pick the agent,
the analyze/rationale LLMs (+ API keys), and the source-control
provider (+ token), review a summary that masks every secret, then
confirm. It writes a committable ``whygraph.example.toml`` (never any
secrets) and, once confirmed, a ready-to-run ``whygraph.toml`` (with
the secrets you entered). Every prompt is defaulted, so a bare Enter
accepts it.

``--yes`` (and any non-TTY invocation: pipes, CI, the git hooks)
skips all prompts and uses defaults — writing a default
``whygraph.toml`` only if none exists and never clobbering an existing
one. A bare non-TTY ``init`` refreshes only the example, as before.

Either way it ensures the project's ``.gitignore`` keeps the
user-owned config and generated caches out of git (``whygraph.toml``,
``.whygraph/``, ``.codegraph/``).

Does **not** index CodeGraph — that happens on ``whygraph scan``,
which populates ``.codegraph/codegraph.db`` (and refreshes it on every
Expand Down Expand Up @@ -103,20 +128,28 @@ def init_cmd(
if not skip_preflight:
_run_preflight()

# Prompt only on a real terminal and only when not told to accept
# defaults. Pipes / CI / the git hooks have no TTY and fall straight
# through to defaults — identical to the pre-interactive behaviour.
interactive = sys.stdin.isatty() and not yes
answers = _gather_answers(project_root, agent_name, interactive=interactive)

db_path = _ensure_db_initialized()
click.echo(f"Initialized WhyGraph database at {db_path}")

_scaffold_example_config(project_root)
_scaffold_example_config(project_root, answers)
_maybe_write_user_config(project_root, answers, write_user=interactive or yes)
_ensure_gitignore(project_root)

if agent_name is None:
resolved_agent = answers.agent or agent_name
if resolved_agent is None:
click.echo(
"Tip: run `whygraph init --list-agents` to see supported agents,"
" then `whygraph init --agent <name>` to wire it up."
)
return

target = agents.resolve_agent(agent_name)
target = agents.resolve_agent(resolved_agent)
snippet = agents.render_snippet(target)

if print_only or not agents.is_write_supported(target):
Expand Down Expand Up @@ -144,21 +177,82 @@ def _run_preflight() -> None:
fail(str(exc))


def _scaffold_example_config(project_root: Path) -> None:
def _gather_answers(project_root: Path, agent_name: str | None, *, interactive: bool):
"""Collect the init choices — interactively, or from defaults.

In interactive mode this runs the guided flow (agent + LLMs + scan +
a summary panel + confirm) and returns its answers. A Ctrl-C / EOF at
any prompt or a declined confirm raises :class:`InitAborted`, which we
turn into a clean non-zero exit **before** any file is written or the
DB is bootstrapped. Non-interactive callers get the defaults with the
``--agent`` value threaded in.

Lazy-imports the interactive module so lightweight surfaces stay fast.
"""
from whygraph.core.config import DEFAULT_ANSWERS, InitAnswers

if not interactive:
return InitAnswers(agent=agent_name, reconfigure_toml=False)

from whygraph.cli.interactive import InitAborted, prompt_for_init

try:
return prompt_for_init(
project_root,
preset_agent=agent_name,
on_summary=_render_summary_panel,
)
except InitAborted:
fail("Aborted — no changes written.")
# Unreachable (fail raises); satisfies type-checkers.
return DEFAULT_ANSWERS


def _render_summary_panel(summary: str) -> None:
"""Render the pre-write review summary as a Rich panel on stderr."""
from rich.panel import Panel

from ..console import console

console.print(
Panel(summary, title="Review your choices", border_style="cyan", expand=False)
)


def _scaffold_example_config(project_root: Path, answers) -> None:
"""Write the committable ``whygraph.example.toml`` at the project root.

Always refreshed so it tracks the shipped defaults. Lazy-imports the
config helper so lightweight surfaces like ``--list-agents`` and
Always refreshed so it tracks the shipped defaults and any non-secret
choices from ``answers``. Secrets are never written here. Lazy-imports
the config helper so lightweight surfaces like ``--list-agents`` and
``--help`` don't pay the cost.
"""
from whygraph.core.config import write_example_config

path = write_example_config(project_root)
path = write_example_config(project_root, answers)
click.echo(
f"Wrote example config to {path} — copy to whygraph.toml and edit to customize"
)


def _maybe_write_user_config(project_root: Path, answers, *, write_user: bool) -> None:
"""Write ``whygraph.toml`` when appropriate, else preserve an existing one.

Writes only when ``write_user`` (interactive or ``--yes``) **and** the
file is absent or the user chose to reconfigure it. A bare non-TTY
``init`` never writes it (``write_user`` is ``False``), preserving the
historical scaffold-only behaviour.
"""
from whygraph.core.config import CONFIG_FILENAME, write_user_config

user_path = project_root / CONFIG_FILENAME
if write_user and (not user_path.exists() or answers.reconfigure_toml):
path = write_user_config(project_root, answers)
click.echo(f"Wrote {path} — gitignored; holds any secrets you entered")
elif user_path.exists():
click.echo(f"Kept existing {user_path}")


def _ensure_gitignore(project_root: Path) -> None:
"""Keep the user config and generated caches out of git.

Expand Down
Loading
Loading