feat(cli): tht session list --json + session show --json (Task 7)

- Add _list_sessions(sessions_root) pure helper: scans sessions dir,
  returns list[dict] with id/status/question/summary/created_at/
  updated_at/author, sorted by created_at desc.
- Add `tht session list` command: --json emits pristine JSON array,
  human mode prints one line per session.
- Add --json flag to `tht session show`: emits manifest
  (model_dump by_alias) + phase (current_phase) + has_schema_linking.
- Tests: 8 tests in test_session_list_json.py (TDD red→green).
- Full suite: 230 passed (--ignore=tests/l2).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-27 20:23:46 +02:00
co-authored by Claude Sonnet 4.6
parent 85e2fdc00a
commit a10a872a99
2 changed files with 223 additions and 3 deletions
+61 -3
View File
@@ -1,3 +1,4 @@
import json
from pathlib import Path
import typer
@@ -22,6 +23,47 @@ def load_session_or_exit(cfg, session_id: str):
raise typer.Exit(code=1)
def _list_sessions(sessions_root: Path) -> list[dict]:
"""Return session summary dicts sorted by created_at desc (pure helper, no I/O side-effects)."""
from tht.session.models import SessionManifest
MANIFEST_NAME = "session_manifest.yaml"
out = []
if not sessions_root.exists():
return out
for d in sessions_root.iterdir():
manifest_path = d / MANIFEST_NAME
if not manifest_path.exists():
continue
m = SessionManifest.from_yaml(manifest_path)
out.append({
"id": m.id,
"status": m.status,
"question": m.question,
"summary": m.summary,
"created_at": m.created_at.isoformat(),
"updated_at": m.updated_at.isoformat() if m.updated_at else None,
"author": m.author,
})
out.sort(key=lambda r: r["created_at"], reverse=True)
return out
@session_app.command("list")
def list_cmd(
json_out: bool = typer.Option(False, "--json", help="Emetti JSON su stdout (pristine)."),
config: Path = CONFIG_OPT,
) -> None:
"""Elenca le sessioni esistenti."""
cfg = _load_config_or_exit(config)
rows = _list_sessions(cfg.paths.sessions)
if json_out:
typer.echo(json.dumps(rows, ensure_ascii=False, indent=2))
return
for r in rows:
typer.echo(f"{r['id']} [{r['status']}] {r['summary'] or r['question'][:60]}")
@session_app.command("new")
def new_cmd(
question: str = typer.Argument(..., help="La domanda in linguaggio naturale."),
@@ -56,13 +98,29 @@ def set_question_cmd(
@session_app.command("show")
def show_cmd(session_id: str = typer.Argument(...), config: Path = CONFIG_OPT) -> None:
def show_cmd(
session_id: str = typer.Argument(...),
json_out: bool = typer.Option(False, "--json", help="Emetti JSON su stdout (pristine)."),
config: Path = CONFIG_OPT,
) -> None:
"""Stato della sessione: manifest + decisioni registrate (per la ripresa)."""
from tht.decisions import list_decisions
from tht.phase import current_phase
cfg = _load_config_or_exit(config)
manifest = load_session_or_exit(cfg, session_id)
decisions = list_decisions(session_dir(cfg, session_id))
sdir = session_dir(cfg, session_id)
if json_out:
has_schema_linking = (sdir / "schema_linking.json").exists()
phase = current_phase(sdir)
data = manifest.model_dump(mode="json", by_alias=True)
data["phase"] = phase
data["has_schema_linking"] = has_schema_linking
typer.echo(json.dumps(data, ensure_ascii=False, indent=2))
return
decisions = list_decisions(sdir)
typer.echo(f"id : {manifest.id}")
typer.echo(f"stato : {manifest.status}")
typer.echo(f"domanda : {manifest.question}")
@@ -70,7 +128,7 @@ def show_cmd(session_id: str = typer.Argument(...), config: Path = CONFIG_OPT) -
typer.echo(f"decisioni: {len(decisions)}")
for d in decisions[-10:]:
typer.echo(f" [{d.seq}] {d.type}: {d.subject}" + (f" — {d.detail}" if d.detail else ""))
linking = session_dir(cfg, session_id) / "schema_linking.json"
linking = sdir / "schema_linking.json"
typer.echo(f"schema_linking.json: {'presente' if linking.exists() else 'assente'}")