initial
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
---
|
||||
name: CLI rules
|
||||
description: Rules for the typer-based finn-eiendom CLI
|
||||
applyTo: "finn_eiendom/cli.py,finn_eiendom/__main__.py"
|
||||
---
|
||||
|
||||
# CLI rules
|
||||
|
||||
The CLI is a **thin wrapper** over `service.py`. It is a sibling of `mcp_server.py` — they never call each other and they share the same underlying service functions. Every CLI command maps 1:1 to a service function with the same parameters and defaults.
|
||||
|
||||
## Framework
|
||||
|
||||
Built with [`typer`](https://typer.tiangolo.com/). One `typer.Typer` app:
|
||||
|
||||
```python
|
||||
# finn_eiendom/cli.py
|
||||
import asyncio, typer
|
||||
from . import service, formatting
|
||||
|
||||
app = typer.Typer(no_args_is_help=True, add_completion=False)
|
||||
```
|
||||
|
||||
Entry points in `pyproject.toml`:
|
||||
|
||||
```toml
|
||||
[project.scripts]
|
||||
finn-eiendom-mcp = "finn_eiendom.mcp_server:main"
|
||||
finn-eiendom = "finn_eiendom.cli:app"
|
||||
```
|
||||
|
||||
Plus `finn_eiendom/__main__.py`:
|
||||
|
||||
```python
|
||||
from .cli import app
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
```
|
||||
|
||||
So `python -m finn_eiendom ...` works without installation.
|
||||
|
||||
## Command body shape
|
||||
|
||||
```python
|
||||
@app.command()
|
||||
def analyze_search(
|
||||
url: str,
|
||||
max_pages: int = 3,
|
||||
detail_limit: int = 20,
|
||||
no_details: bool = typer.Option(False, "--no-details"),
|
||||
no_eiendom: bool = typer.Option(False, "--no-eiendom"),
|
||||
with_similar: bool = typer.Option(False, "--with-similar"),
|
||||
format: str = typer.Option("json", "--format"),
|
||||
) -> None:
|
||||
"""Analyze a FINN search URL and return a ranked shortlist."""
|
||||
result = asyncio.run(service.analyze_search(
|
||||
search_url=url,
|
||||
max_pages=max_pages,
|
||||
detail_limit=detail_limit,
|
||||
include_details=not no_details,
|
||||
include_eiendom_no=not no_eiendom,
|
||||
include_similar_units_for_shortlist=with_similar,
|
||||
))
|
||||
typer.echo(formatting.render_shortlist(result, format))
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
* The command body has at most three sections: option parsing (handled by typer), one `service.<function>` call, one `typer.echo(formatting.render_<thing>(result, format))`.
|
||||
* If the body has more than ~20 lines, the logic belongs in `service.py`.
|
||||
* No `print()` — use `typer.echo()` for stdout, `typer.echo(..., err=True)` for stderr.
|
||||
* No business logic, no rendering, no SQLite, no HTTP, no parsing.
|
||||
|
||||
## Formats
|
||||
|
||||
Every command that produces structured output accepts `--format`:
|
||||
|
||||
* `--format json` (default) — full structured output, pipeable into `jq`.
|
||||
* `--format markdown` — human-readable.
|
||||
* `--format table` — terminal table (only where it makes sense: `analyze-search`, `compare`, `shortlist`, `diff`).
|
||||
|
||||
All three render paths are produced by `formatting.py`. Never format inline in `cli.py`. Unsupported values raise `ValueError` with a list of supported formats — typer surfaces this as a non-zero exit.
|
||||
|
||||
## Commands
|
||||
|
||||
```text
|
||||
finn-eiendom analyze-search <url> [--max-pages 3] [--detail-limit 20] [--no-details] [--no-eiendom] [--with-similar] [--format ...]
|
||||
finn-eiendom get-ad <finnkode> [--force-refresh] [--no-eiendom] [--with-similar] [--format ...]
|
||||
finn-eiendom compare <finnkode...> [--no-eiendom] [--no-comps] [--format ...]
|
||||
finn-eiendom save-feedback <finnkode> <verdict> [--notes "..."]
|
||||
finn-eiendom shortlist [--run-id ID] [--limit 10] [--format ...]
|
||||
finn-eiendom diff <url> [--format ...]
|
||||
finn-eiendom resolve-unit <finn_url>
|
||||
finn-eiendom get-unit <unit_code> [--force-refresh]
|
||||
finn-eiendom enrich-ad <finnkode> [--with-similar]
|
||||
finn-eiendom build-vector <unit_code>
|
||||
finn-eiendom decode-vector <unit_vector>
|
||||
finn-eiendom similar-units <unit_vector> [--status RECENTLY_SOLD|FOR_SALE|CURRENT]
|
||||
finn-eiendom similar-to-liked <finnkode> [--mode recommendations|comps] [--status ...]
|
||||
finn-eiendom analyze-against-comps <finnkode>
|
||||
finn-eiendom cache stats | clear | clear-html | clear-json
|
||||
finn-eiendom serve [--transport stdio|http] [--host 127.0.0.1] [--port 8010]
|
||||
finn-eiendom config show | path
|
||||
finn-eiendom doctor
|
||||
finn-eiendom version
|
||||
```
|
||||
|
||||
Sub-command groups (`cache`, `config`) use `typer.Typer` sub-apps:
|
||||
|
||||
```python
|
||||
cache_app = typer.Typer(help="Cache management")
|
||||
app.add_typer(cache_app, name="cache")
|
||||
|
||||
@cache_app.command("stats")
|
||||
def cache_stats() -> None:
|
||||
typer.echo(formatting.render_cache_stats(service.get_cache_stats(), "json"))
|
||||
```
|
||||
|
||||
## Async glue
|
||||
|
||||
Service functions are async; CLI commands are sync. Always use `asyncio.run(service.<function>(...))` at the call boundary. Don't sprinkle `async def` across CLI commands — typer expects sync handlers.
|
||||
|
||||
## Exit codes
|
||||
|
||||
* `0` — success.
|
||||
* `1` — runtime error (raised exception in service).
|
||||
* `2` — usage error (typer's default for bad options).
|
||||
|
||||
Let exceptions propagate from `service.py` and rely on typer's default handling. Only catch where you want a more specific exit code or message.
|
||||
|
||||
## What stays out of cli.py
|
||||
|
||||
* `import httpx`, `import sqlite3`, `import msgpack` — never.
|
||||
* `from .ad import ...`, `from .search import ...`, `from .eiendom_no import ...`, `from .scoring import ...`, `from .cache import ...`, `from .http import ...` — never.
|
||||
* Inline formatting logic — goes in `formatting.py`.
|
||||
* MCP imports (no `from .mcp_server import ...`).
|
||||
|
||||
Allowed imports in `cli.py`:
|
||||
|
||||
```python
|
||||
import asyncio, json, sys
|
||||
import typer
|
||||
from . import service, formatting, config
|
||||
from .models import FinnAd, EiendomUnit, SimilarUnit # only for type hints
|
||||
```
|
||||
|
||||
`tests/test_architecture.py` enforces this.
|
||||
|
||||
## When uncertain about typer
|
||||
|
||||
Use `context7` instead of guessing:
|
||||
|
||||
```
|
||||
context7:resolve-library-id → "tiangolo/typer"
|
||||
context7:query-docs(id, "Typer sub-apps and option groups")
|
||||
```
|
||||
|
||||
See `docs.instructions.md`.
|
||||
Reference in New Issue
Block a user