From 75b4cfd096f66335ba462bd9b9f3fcb2bbaa8237 Mon Sep 17 00:00:00 2001 From: kukirin-ui <325067280+kukirin-ui@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:19:00 +0200 Subject: [PATCH] feat(cli): add --json to runs list --- CHANGELOG.md | 2 ++ src/slmkit/cli.py | 6 ++++++ tests/unit/test_runs.py | 21 +++++++++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 260a1a0..be1048c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ interfaces; each entry says what moved. Milestones (M0, M1, …) are described i ## [Unreleased] ### Added +- `slm runs list --json` prints the runs as a JSON array (run ID plus the fields of `status.json`) for + scripts and notebooks. Works with `--project`. - Model cards open with Hugging Face Hub metadata (licence, library, task, tags), so an export can be uploaded as is. A new optional Project API hook, `model_card_info()`, gives the licence, subject tags and data statement; `abc_music` and `shakespeare_char` state theirs (ADR 0010). Export code version 4. diff --git a/src/slmkit/cli.py b/src/slmkit/cli.py index 4fdaf62..74ed927 100644 --- a/src/slmkit/cli.py +++ b/src/slmkit/cli.py @@ -352,8 +352,11 @@ def _short(n: float) -> str: @_friendly_errors def runs_list( project: str | None = typer.Option(None, "--project", help="Only show runs for this project."), + as_json: bool = typer.Option(False, "--json", help="Print the runs as a JSON array."), ) -> None: """Every run: progress, GPU-hours used and left, best validation loss, last checkpoint.""" + import json + from slmkit.train.run import list_runs rows = list_runs() @@ -364,6 +367,9 @@ def runs_list( if isinstance(st.get("experiment"), str) and st["experiment"].partition("/")[0] == project ] + if as_json: + typer.echo(json.dumps([{**st, "run_id": path.name} for path, st in rows], indent=2)) + return if not rows: message = ( "no runs yet under $SLM_HOME/runs" diff --git a/tests/unit/test_runs.py b/tests/unit/test_runs.py index 37f8ebc..3faea8f 100644 --- a/tests/unit/test_runs.py +++ b/tests/unit/test_runs.py @@ -43,3 +43,24 @@ def test_runs_list_can_filter_by_project(slm_home: Path) -> None: assert filtered.exit_code == 0, filtered.output assert "abc run" in all_runs.output and "chess run" in all_runs.output assert "abc run" in filtered.output and "chess run" not in filtered.output + + +def test_runs_list_json(slm_home: Path) -> None: + _write_status(slm_home, "run-abc123456789", "abc_music/micro_v1", "abc run") + _write_status(slm_home, "run-chess123456", "chess/nano", "chess run") + + all_runs = CliRunner().invoke(app, ["runs", "list", "--json"]) + filtered = CliRunner().invoke(app, ["runs", "list", "--json", "--project", "chess"]) + + assert all_runs.exit_code == 0, all_runs.output + runs = json.loads(all_runs.output) + assert {r["run_id"] for r in runs} == {"run-abc123456789", "run-chess123456"} + assert runs[0]["tokens_seen"] == 1000 + assert [r["name"] for r in json.loads(filtered.output)] == ["chess run"] + + +def test_runs_list_json_empty(slm_home: Path) -> None: + result = CliRunner().invoke(app, ["runs", "list", "--json"]) + + assert result.exit_code == 0, result.output + assert json.loads(result.output) == []