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
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,25 @@ draft init

### draft list

List the 15 most recent runs across all projects.
List the 15 most recent runs across all projects. Each run is printed as a multi-line record separated by a blank line.

```shell
draft list
```

Example output:

```
Run: 260521-212506 (1/2, running)
Project: draft
Branch: draft/scope-list-render-layout
PR: -
Workspace: /path/to/workspace
Logs: /path/to/run/logs
```

Pass `--json` for machine-readable output.

### draft status

Show the status of a single run.
Expand Down
63 changes: 44 additions & 19 deletions src/draft/command_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,46 @@ def _workspace_status(wt_dir: str) -> str:
return "-"


def _load_state_payload_for_display(run_dir: Path) -> dict | None:
try:
return json.loads((run_dir / "state.json").read_text())
except Exception:
return None


def _workspace_display(wt_dir: str | None) -> str:
if not wt_dir:
return "-"
try:
return str(wt_dir) if Path(wt_dir).is_dir() else "(deleted)"
except OSError:
return "(deleted)"


def _format_run_line(row: dict) -> str:
if row["state"] == "missing":
parts = ["missing"]
elif row["state"] == "corrupt":
parts = ["corrupt"]
else:
parts = [f"{row['stages_completed']}/{row['stages_total']}"]
if row["running"]:
parts.append("running")
return f"Run: {row['run_id']} ({', '.join(parts)})"


def _print_human_record(run_dir: Path) -> None:
row = _row_data(run_dir)
payload = _load_state_payload_for_display(run_dir)
wt_dir = (payload or {}).get("data", {}).get("wt_dir") or None
print(_format_run_line(row))
print(f"Project: {row['project']}")
print(f"Branch: {row['branch'] or '-'}")
print(f"PR: {row['pr_url'] or '-'}")
print(f"Workspace: {_workspace_display(wt_dir)}")
print(f"Logs: {run_dir}")


def _row_data(run_dir: Path) -> dict:
running = _is_run_active(run_dir)
run_id = run_dir.name
Expand Down Expand Up @@ -134,24 +174,9 @@ def run(args) -> int:
print(json.dumps(rows, indent=2))
return 0

header = f"{'RUN-ID':<18} {'PROJECT':<20} {'STAGES':<10} {'RUNNING':<8} {'WORKSPACE':<10} {'BRANCH':<30} PR"
print(header)
print("-" * len(header))

for d in dirs:
row = _row_data(d)
running_str = "yes" if row["running"] else "-"
workspace_str = row["workspace"] if row["workspace"] is not None else "-"
branch_str = row["branch"] or "-"
pr_str = row["pr_url"] or "-"
if row["state"] == "missing":
stages_str = "-"
elif row["state"] == "corrupt":
stages_str = "corrupt"
else:
stages_str = f"{row['stages_completed']}/{row['stages_total']}"
print(
f"{row['run_id']:<18} {row['project']:<20} {stages_str:<10} {running_str:<8} {workspace_str:<10} {branch_str:<30} {pr_str}"
)
for index, d in enumerate(dirs):
if index > 0:
print()
_print_human_record(d)

return 0
125 changes: 104 additions & 21 deletions tests/draft/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,7 @@ def test_command_list_missing_state_shows_dash(tmp_path, capsys):
result = clm.run(object())
out = capsys.readouterr().out
assert result == 0
lines = [line for line in out.splitlines() if "260508-100000" in line]
assert lines and "-" in lines[0]
assert "Run: 260508-100000 (missing)" in out


def test_command_list_corrupt_state_shows_corrupt(tmp_path, capsys):
Expand Down Expand Up @@ -208,8 +207,7 @@ def test_command_list_workspace_column_yes(tmp_path, capsys):
with patch("draft.command_list.runs_base", return_value=base):
clm.run(object())
out = capsys.readouterr().out
lines = [line for line in out.splitlines() if "260508-100000" in line]
assert lines and "yes" in lines[0]
assert f"Workspace: {wt}" in out


def test_command_list_workspace_column_no(tmp_path, capsys):
Expand All @@ -225,8 +223,7 @@ def test_command_list_workspace_column_no(tmp_path, capsys):
with patch("draft.command_list.runs_base", return_value=base):
clm.run(object())
out = capsys.readouterr().out
lines = [line for line in out.splitlines() if "260508-100000" in line]
assert lines and "no" in lines[0]
assert "Workspace: (deleted)" in out


def test_command_list_workspace_column_absent_wt_dir(tmp_path, capsys):
Expand All @@ -239,11 +236,7 @@ def test_command_list_workspace_column_absent_wt_dir(tmp_path, capsys):
with patch("draft.command_list.runs_base", return_value=base):
clm.run(object())
out = capsys.readouterr().out
lines = [line for line in out.splitlines() if "260508-100000" in line]
assert lines
# The WORKSPACE column should show '-'
# Split by multiple spaces to check the workspace field
assert "-" in lines[0]
assert "Workspace: -" in out


def test_command_list_missing_state_workspace_dash(tmp_path, capsys):
Expand All @@ -257,9 +250,7 @@ def test_command_list_missing_state_workspace_dash(tmp_path, capsys):
result = clm.run(object())
out = capsys.readouterr().out
assert result == 0
lines = [line for line in out.splitlines() if "260508-100001" in line]
assert lines and "WORKSPACE" not in lines[0]
assert "WORKSPACE" in out.splitlines()[0]
assert "Workspace: -" in out


def test_command_list_corrupt_state_workspace_dash(tmp_path, capsys):
Expand All @@ -274,9 +265,101 @@ def test_command_list_corrupt_state_workspace_dash(tmp_path, capsys):
result = clm.run(object())
out = capsys.readouterr().out
assert result == 0
assert "WORKSPACE" in out.splitlines()[0]
lines = [line for line in out.splitlines() if "260508-100002" in line]
assert lines
assert "Workspace: -" in out


def test_command_list_normal_run_full_record(tmp_path, capsys):
import draft.command_list as clm

base = tmp_path / "runs"
wt = tmp_path / "my-wt"
wt.mkdir()
state = {
"completed": ["worktree-create", "code-spec"],
"data": {
"worktree_mode": "worktree",
"pr_mode": "open",
"skip_pr": False,
"branch": "feat/foo",
"wt_dir": str(wt),
"pr_url": "https://github.com/org/repo/pull/1",
"pipeline": "create",
},
}
run_dir = base / "myproject" / "260508-100000"
run_dir.mkdir(parents=True)
(run_dir / "state.json").write_text(json.dumps(state))

with patch("draft.command_list.runs_base", return_value=base):
result = clm.run(object())

assert result == 0
out = capsys.readouterr().out
assert "Run: 260508-100000 (2/5)" in out
assert "Project: myproject" in out
assert "Branch: feat/foo" in out
assert "PR: https://github.com/org/repo/pull/1" in out
assert f"Workspace: {wt}" in out
assert f"Logs: {run_dir}" in out


def test_command_list_active_run_shows_running(tmp_path, capsys):
import draft.command_list as clm

base = tmp_path / "runs"
state = {
"completed": [],
"data": {"worktree_mode": "worktree", "pr_mode": "open", "pipeline": "create"},
}
run_dir = base / "myproject" / "260508-100000"
run_dir.mkdir(parents=True)
(run_dir / "state.json").write_text(json.dumps(state))
(run_dir / "draft.pid").write_text(str(os.getpid()))

with patch("draft.command_list.runs_base", return_value=base):
clm.run(object())

out = capsys.readouterr().out
lines = out.splitlines()
assert lines[0].startswith("Run: 260508-100000 (")
assert "running" in lines[0]


def test_command_list_non_active_run_no_running(tmp_path, capsys):
import draft.command_list as clm

base = tmp_path / "runs"
state = {
"completed": ["worktree-create"],
"data": {"worktree_mode": "worktree", "pr_mode": "open", "pipeline": "create"},
}
_make_list_run(base, "260508-100000", state)

with patch("draft.command_list.runs_base", return_value=base):
clm.run(object())

out = capsys.readouterr().out
lines = out.splitlines()
assert lines[0].startswith("Run: 260508-100000 (")
assert "running" not in lines[0]


def test_command_list_multiple_runs_blank_line_separator(tmp_path, capsys):
import draft.command_list as clm

base = tmp_path / "runs"
state = {"completed": [], "data": {"pipeline": "create"}}
_make_list_run(base, "260508-100000", state)
_make_list_run(base, "260508-100001", state)

with patch("draft.command_list.runs_base", return_value=base):
clm.run(object())

out = capsys.readouterr().out
lines = out.splitlines()
run_lines = [i for i, line in enumerate(lines) if line.startswith("Run:")]
assert len(run_lines) == 2
assert lines[run_lines[1] - 1] == ""


# --- command_delete ---
Expand Down Expand Up @@ -2364,20 +2447,20 @@ def test_command_list_json_running_false(tmp_path, capsys):
assert rows[0]["running"] is False


def test_command_list_no_json_unchanged(tmp_path, capsys):
def test_command_list_human_record_layout(tmp_path, capsys):
import draft.command_list as clm

base = tmp_path / "runs"
state = {"completed": [], "data": {"branch": "feat"}}
state = {"completed": [], "data": {"branch": "feat", "pipeline": "create"}}
_make_list_run(base, "260508-100000", state)

with patch("draft.command_list.runs_base", return_value=base):
result = clm.run(object())

assert result == 0
out = capsys.readouterr().out
assert "RUN-ID" in out
assert "260508-100000" in out
assert "Run: 260508-100000" in out
assert "Branch: feat" in out


# --- command_status ---
Expand Down
Loading