recon-github 0.1.6__tar.gz → 0.1.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {recon_github-0.1.6 → recon_github-0.1.7}/PKG-INFO +7 -8
- {recon_github-0.1.6 → recon_github-0.1.7}/README.md +6 -7
- {recon_github-0.1.6 → recon_github-0.1.7}/app/commands/repo.py +15 -22
- {recon_github-0.1.6 → recon_github-0.1.7}/app/ui/repo.py +132 -2
- {recon_github-0.1.6 → recon_github-0.1.7}/pyproject.toml +1 -1
- recon_github-0.1.7/tests/test_cli_commands.py +176 -0
- {recon_github-0.1.6 → recon_github-0.1.7}/uv.lock +1 -1
- recon_github-0.1.6/tests/test_cli_commands.py +0 -80
- {recon_github-0.1.6 → recon_github-0.1.7}/.agents/rich-ui-architect.md +0 -0
- {recon_github-0.1.6 → recon_github-0.1.7}/.github/workflows/cd.yml +0 -0
- {recon_github-0.1.6 → recon_github-0.1.7}/.github/workflows/ci.yml +0 -0
- {recon_github-0.1.6 → recon_github-0.1.7}/.gitignore +0 -0
- {recon_github-0.1.6 → recon_github-0.1.7}/.python-version +0 -0
- {recon_github-0.1.6 → recon_github-0.1.7}/LICENSE +0 -0
- {recon_github-0.1.6 → recon_github-0.1.7}/ROADMAP.md +0 -0
- {recon_github-0.1.6 → recon_github-0.1.7}/app/cli.py +0 -0
- {recon_github-0.1.6 → recon_github-0.1.7}/app/commands/auth.py +0 -0
- {recon_github-0.1.6 → recon_github-0.1.7}/app/commands/me.py +0 -0
- {recon_github-0.1.6 → recon_github-0.1.7}/app/services/auth.py +0 -0
- {recon_github-0.1.6 → recon_github-0.1.7}/app/services/github.py +0 -0
- {recon_github-0.1.6 → recon_github-0.1.7}/app/services/github_errors.py +0 -0
- {recon_github-0.1.6 → recon_github-0.1.7}/app/services/storage.py +0 -0
- {recon_github-0.1.6 → recon_github-0.1.7}/app/ui/avatar.py +0 -0
- {recon_github-0.1.6 → recon_github-0.1.7}/app/ui/display.py +0 -0
- {recon_github-0.1.6 → recon_github-0.1.7}/main.py +0 -0
- {recon_github-0.1.6 → recon_github-0.1.7}/tests/test_auth_service.py +0 -0
- {recon_github-0.1.6 → recon_github-0.1.7}/tests/test_cli.py +0 -0
- {recon_github-0.1.6 → recon_github-0.1.7}/tests/test_github_errors.py +0 -0
- {recon_github-0.1.6 → recon_github-0.1.7}/tests/test_github_service.py +0 -0
- {recon_github-0.1.6 → recon_github-0.1.7}/tests/test_repo_helpers.py +0 -0
- {recon_github-0.1.6 → recon_github-0.1.7}/tests/test_storage.py +0 -0
- {recon_github-0.1.6 → recon_github-0.1.7}/tests/test_ui_repo.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: recon-github
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: Recon is a powerful CLI companion for developers and tools for agents, turning GitHub activity and repository data into useful insights from your terminal
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Requires-Python: >=3.12
|
|
@@ -11,7 +11,6 @@ Requires-Dist: rich>=15.0.0
|
|
|
11
11
|
Requires-Dist: typer>=0.27.1
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
|
|
14
|
-
|
|
15
14
|
# Recon CLI
|
|
16
15
|
|
|
17
16
|
Recon is a powerful CLI companion for developers and tools for agents, turning GitHub activity and repository data into useful insights from your terminal.
|
|
@@ -25,9 +24,9 @@ Recon is a powerful CLI companion for developers and tools for agents, turning G
|
|
|
25
24
|
[](https://github.com/owenpalfreymandev/reconcli/blob/main/LICENSE)
|
|
26
25
|
## Overview
|
|
27
26
|
|
|
28
|
-
Recon allows
|
|
27
|
+
Recon allows developers to view basic GitHub analytics in the terminal quickly, rather than opening up their browser. This means it allows agents to interact with the service without having browser capabilities.
|
|
29
28
|
|
|
30
|
-
It is powered by [Typer](github.com/fastapi/typer), a library for building fast CLI tools, and made pretty by [Rich](https://github.com/textualize/rich) and
|
|
29
|
+
It is powered by [Typer](github.com/fastapi/typer), a library for building fast CLI tools, and made pretty by [Rich](https://github.com/textualize/rich) and its prebuilt components.
|
|
31
30
|
## Features
|
|
32
31
|
|
|
33
32
|
* **GitHub Authentication** — Securely authenticate with GitHub using OAuth device flow.
|
|
@@ -41,7 +40,7 @@ It is powered by [Typer](github.com/fastapi/typer), a library for building fast
|
|
|
41
40
|
|
|
42
41
|
## Installation
|
|
43
42
|
|
|
44
|
-
Since we want you to be able to use Recon from anywhere - not just one directory - we
|
|
43
|
+
Since we want you to be able to use Recon from anywhere - not just one directory - we recommend you install it with [uv package manager](https://docs.astral.sh/uv/getting-started/installation/).
|
|
45
44
|
|
|
46
45
|
```bash
|
|
47
46
|
uv tool install recon-github
|
|
@@ -68,7 +67,7 @@ Example output:
|
|
|
68
67
|
╭────── Owen Palfreyman ───────╮ ╭──────────────────────────────╮
|
|
69
68
|
│ Repositories 2 │ │ │
|
|
70
69
|
│ Followers 7 │ │ │
|
|
71
|
-
│ Following 2
|
|
70
|
+
│ Following 2 │ │ │
|
|
72
71
|
│ Location United Kingdom │ │ │
|
|
73
72
|
│ Company — │ │ │
|
|
74
73
|
│ Joined 2023-11-02 │ │ │
|
|
@@ -213,7 +212,7 @@ uv run pytest
|
|
|
213
212
|
uv run pyright
|
|
214
213
|
```
|
|
215
214
|
|
|
216
|
-
Be sure to lint before
|
|
215
|
+
Be sure to lint before committing, or it will fail CI/CD:
|
|
217
216
|
```bash
|
|
218
217
|
uv run ruff check --fix .
|
|
219
218
|
```
|
|
@@ -237,4 +236,4 @@ Recon is developed by Owen Palfreyman.
|
|
|
237
236
|
GitHub: @owenpalfreymandev
|
|
238
237
|
Repository: owenpalfreymandev/reconcli
|
|
239
238
|
|
|
240
|
-
*or could
|
|
239
|
+
*or could just run `recon details owenpalfreymandev reconcli --contributors` wink wink*
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
# Recon CLI
|
|
3
2
|
|
|
4
3
|
Recon is a powerful CLI companion for developers and tools for agents, turning GitHub activity and repository data into useful insights from your terminal.
|
|
@@ -12,9 +11,9 @@ Recon is a powerful CLI companion for developers and tools for agents, turning G
|
|
|
12
11
|
[](https://github.com/owenpalfreymandev/reconcli/blob/main/LICENSE)
|
|
13
12
|
## Overview
|
|
14
13
|
|
|
15
|
-
Recon allows
|
|
14
|
+
Recon allows developers to view basic GitHub analytics in the terminal quickly, rather than opening up their browser. This means it allows agents to interact with the service without having browser capabilities.
|
|
16
15
|
|
|
17
|
-
It is powered by [Typer](github.com/fastapi/typer), a library for building fast CLI tools, and made pretty by [Rich](https://github.com/textualize/rich) and
|
|
16
|
+
It is powered by [Typer](github.com/fastapi/typer), a library for building fast CLI tools, and made pretty by [Rich](https://github.com/textualize/rich) and its prebuilt components.
|
|
18
17
|
## Features
|
|
19
18
|
|
|
20
19
|
* **GitHub Authentication** — Securely authenticate with GitHub using OAuth device flow.
|
|
@@ -28,7 +27,7 @@ It is powered by [Typer](github.com/fastapi/typer), a library for building fast
|
|
|
28
27
|
|
|
29
28
|
## Installation
|
|
30
29
|
|
|
31
|
-
Since we want you to be able to use Recon from anywhere - not just one directory - we
|
|
30
|
+
Since we want you to be able to use Recon from anywhere - not just one directory - we recommend you install it with [uv package manager](https://docs.astral.sh/uv/getting-started/installation/).
|
|
32
31
|
|
|
33
32
|
```bash
|
|
34
33
|
uv tool install recon-github
|
|
@@ -55,7 +54,7 @@ Example output:
|
|
|
55
54
|
╭────── Owen Palfreyman ───────╮ ╭──────────────────────────────╮
|
|
56
55
|
│ Repositories 2 │ │ │
|
|
57
56
|
│ Followers 7 │ │ │
|
|
58
|
-
│ Following 2
|
|
57
|
+
│ Following 2 │ │ │
|
|
59
58
|
│ Location United Kingdom │ │ │
|
|
60
59
|
│ Company — │ │ │
|
|
61
60
|
│ Joined 2023-11-02 │ │ │
|
|
@@ -200,7 +199,7 @@ uv run pytest
|
|
|
200
199
|
uv run pyright
|
|
201
200
|
```
|
|
202
201
|
|
|
203
|
-
Be sure to lint before
|
|
202
|
+
Be sure to lint before committing, or it will fail CI/CD:
|
|
204
203
|
```bash
|
|
205
204
|
uv run ruff check --fix .
|
|
206
205
|
```
|
|
@@ -224,4 +223,4 @@ Recon is developed by Owen Palfreyman.
|
|
|
224
223
|
GitHub: @owenpalfreymandev
|
|
225
224
|
Repository: owenpalfreymandev/reconcli
|
|
226
225
|
|
|
227
|
-
*or could
|
|
226
|
+
*or could just run `recon details owenpalfreymandev reconcli --contributors` wink wink*
|
|
@@ -67,31 +67,24 @@ def format_languages(languages: dict[str, int], max_languages: int = 5) -> list[
|
|
|
67
67
|
|
|
68
68
|
|
|
69
69
|
@app.command()
|
|
70
|
-
def list(
|
|
70
|
+
def list(
|
|
71
|
+
boxy: bool = typer.Option(
|
|
72
|
+
False,
|
|
73
|
+
"--boxy",
|
|
74
|
+
help="Render each repository as a panel (not recommended for long lists).",
|
|
75
|
+
),
|
|
76
|
+
):
|
|
71
77
|
"""See a list of all your repos."""
|
|
72
|
-
from app.services.github import get_user_repos
|
|
78
|
+
from app.services.github import get_authenticated_user, get_user_repos
|
|
79
|
+
from app.ui.repo import display_repository_list
|
|
73
80
|
|
|
81
|
+
user = get_authenticated_user()
|
|
74
82
|
repos = get_user_repos()
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
typer.echo(
|
|
81
|
-
f"visibility: {repo.get('visibility') or ('private' if repo.get('private') else 'public')}"
|
|
82
|
-
)
|
|
83
|
-
typer.echo(f"language: {repo.get('language') or '—'}")
|
|
84
|
-
typer.echo(f"default branch: {repo.get('default_branch') or '—'}")
|
|
85
|
-
typer.echo(
|
|
86
|
-
f"stars: {repo.get('stargazers_count', 0)} forks: {repo.get('forks_count', 0)} open issues: {repo.get('open_issues_count', 0)}"
|
|
87
|
-
)
|
|
88
|
-
typer.echo(f" url: {repo['html_url']}")
|
|
89
|
-
|
|
90
|
-
topics = repo.get("topics") or []
|
|
91
|
-
if topics:
|
|
92
|
-
typer.echo(f" topics: {format_topics(topics)}")
|
|
93
|
-
|
|
94
|
-
typer.echo("")
|
|
83
|
+
display_repository_list(
|
|
84
|
+
repos,
|
|
85
|
+
user.get("login", "Unknown user"),
|
|
86
|
+
boxy=boxy,
|
|
87
|
+
)
|
|
95
88
|
|
|
96
89
|
|
|
97
90
|
@app.command()
|
|
@@ -7,13 +7,143 @@ from rich.text import Text
|
|
|
7
7
|
console = Console()
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
def
|
|
10
|
+
def build_repository_table(repositories: list[dict]) -> Table:
|
|
11
|
+
"""Build the compact repository listing used by ``recon list``."""
|
|
12
|
+
table_width = _repository_table_width()
|
|
13
|
+
repository_width = max(13, min(32, table_width - 66))
|
|
14
|
+
table = Table(
|
|
15
|
+
title=f"Repositories ({len(repositories)})",
|
|
16
|
+
title_style="bold cyan",
|
|
17
|
+
header_style="bold cyan",
|
|
18
|
+
box=None,
|
|
19
|
+
show_edge=False,
|
|
20
|
+
pad_edge=False,
|
|
21
|
+
padding=(0, 1),
|
|
22
|
+
collapse_padding=False,
|
|
23
|
+
width=table_width,
|
|
24
|
+
)
|
|
25
|
+
table.add_column(
|
|
26
|
+
"Repository",
|
|
27
|
+
no_wrap=True,
|
|
28
|
+
overflow="ellipsis",
|
|
29
|
+
width=repository_width,
|
|
30
|
+
max_width=repository_width,
|
|
31
|
+
)
|
|
32
|
+
table.add_column(
|
|
33
|
+
"Visibility", no_wrap=True, overflow="ellipsis", width=10, max_width=10
|
|
34
|
+
)
|
|
35
|
+
table.add_column(
|
|
36
|
+
"Language", no_wrap=True, overflow="ellipsis", width=9, max_width=9
|
|
37
|
+
)
|
|
38
|
+
table.add_column(
|
|
39
|
+
"Updated", no_wrap=True, overflow="ellipsis", width=10, max_width=10
|
|
40
|
+
)
|
|
41
|
+
table.add_column("Branch", no_wrap=True, overflow="ellipsis", width=6, max_width=6)
|
|
42
|
+
table.add_column("Stars", justify="right", no_wrap=True, width=5)
|
|
43
|
+
table.add_column("Forks", justify="right", no_wrap=True, width=5)
|
|
44
|
+
table.add_column("Issues", justify="right", no_wrap=True, width=5)
|
|
45
|
+
|
|
46
|
+
for repository in repositories:
|
|
47
|
+
visibility = repository.get("visibility") or (
|
|
48
|
+
"private" if repository.get("private") else "public"
|
|
49
|
+
)
|
|
50
|
+
table.add_row(
|
|
51
|
+
str(repository.get("full_name") or repository.get("name") or "—"),
|
|
52
|
+
str(visibility or "—"),
|
|
53
|
+
str(repository.get("language") or "—"),
|
|
54
|
+
_format_updated(repository.get("updated_at")),
|
|
55
|
+
str(repository.get("default_branch") or "—"),
|
|
56
|
+
_format_count(repository.get("stargazers_count")),
|
|
57
|
+
_format_count(repository.get("forks_count")),
|
|
58
|
+
_format_count(repository.get("open_issues_count")),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if not repositories:
|
|
62
|
+
table.add_row("No repositories found.", *["—"] * 7)
|
|
63
|
+
|
|
64
|
+
return table
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def display_repository_list(
|
|
68
|
+
repositories: list[dict], username: str, boxy: bool = False
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Render a compact, scan-friendly table of repositories."""
|
|
71
|
+
width = _repository_box_width() if boxy else _repository_table_width()
|
|
72
|
+
console.print(build_view_header("List", username, width=width))
|
|
73
|
+
if boxy:
|
|
74
|
+
for repository in repositories:
|
|
75
|
+
console.print(build_repository_panel(repository))
|
|
76
|
+
if not repositories:
|
|
77
|
+
console.print(Panel("No repositories found.", width=width))
|
|
78
|
+
return
|
|
79
|
+
console.print(build_repository_table(repositories))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def build_repository_panel(repository: dict) -> Panel:
|
|
83
|
+
"""Build the box-style repository summary used by ``recon list --boxy``."""
|
|
84
|
+
from app.commands.repo import format_topics
|
|
85
|
+
|
|
86
|
+
visibility = repository.get("visibility") or (
|
|
87
|
+
"private" if repository.get("private") else "public"
|
|
88
|
+
)
|
|
89
|
+
language = repository.get("language") or "—"
|
|
90
|
+
branch = repository.get("default_branch") or "—"
|
|
91
|
+
topics = repository.get("topics") or []
|
|
92
|
+
topic_text = (
|
|
93
|
+
format_topics([str(topic) for topic in topics]).replace(", ", " · ")
|
|
94
|
+
if topics
|
|
95
|
+
else "—"
|
|
96
|
+
)
|
|
97
|
+
content = Text()
|
|
98
|
+
content.append(
|
|
99
|
+
f"{str(visibility).capitalize()} · {language} · {branch}\n",
|
|
100
|
+
style="cyan",
|
|
101
|
+
)
|
|
102
|
+
content.append(
|
|
103
|
+
f"Stars {_format_count(repository.get('stargazers_count'))} "
|
|
104
|
+
f"Forks {_format_count(repository.get('forks_count'))} "
|
|
105
|
+
f"Issues {_format_count(repository.get('open_issues_count'))}\n"
|
|
106
|
+
)
|
|
107
|
+
content.append(topic_text, style="dim")
|
|
108
|
+
return Panel(
|
|
109
|
+
content,
|
|
110
|
+
title=str(repository.get("full_name") or repository.get("name") or "—"),
|
|
111
|
+
width=_repository_box_width(),
|
|
112
|
+
border_style="cyan",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _repository_table_width() -> int:
|
|
117
|
+
"""Calculate the shared width for the list table and header."""
|
|
118
|
+
return min(max(1, console.width - 1), 98)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _repository_box_width() -> int:
|
|
122
|
+
"""Calculate the narrower width used by boxy repository summaries."""
|
|
123
|
+
return min(max(1, console.width - 1), 72)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _format_count(value: object) -> str:
|
|
127
|
+
"""Format repository counters while keeping missing values predictable."""
|
|
128
|
+
if value is None:
|
|
129
|
+
return "0"
|
|
130
|
+
return f"{value:,}" if isinstance(value, int) else str(value)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _format_updated(value: object) -> str:
|
|
134
|
+
"""Show the date portion of GitHub's ISO timestamp."""
|
|
135
|
+
if not value:
|
|
136
|
+
return "—"
|
|
137
|
+
return str(value).split("T", 1)[0]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def build_view_header(title: str, subtitle: str, width: int = 60) -> Panel:
|
|
11
141
|
"""Create the shared header used by focused repository views."""
|
|
12
142
|
content = Text(justify="center")
|
|
13
143
|
content.append(title.upper(), style="bold cyan")
|
|
14
144
|
content.append("\n")
|
|
15
145
|
content.append(subtitle, style="bold")
|
|
16
|
-
return Panel(content, width=
|
|
146
|
+
return Panel(content, width=width, border_style="cyan")
|
|
17
147
|
|
|
18
148
|
|
|
19
149
|
def display_view_header(title: str, subtitle: str) -> None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "recon-github"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.7"
|
|
4
4
|
description = "Recon is a powerful CLI companion for developers and tools for agents, turning GitHub activity and repository data into useful insights from your terminal"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
from unittest.mock import Mock
|
|
2
|
+
|
|
3
|
+
from typer.testing import CliRunner
|
|
4
|
+
|
|
5
|
+
from app.cli import app
|
|
6
|
+
from app.commands import auth as auth_command
|
|
7
|
+
from app.commands import me as me_command
|
|
8
|
+
from app.ui.repo import build_repository_table
|
|
9
|
+
|
|
10
|
+
runner = CliRunner()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_login_logout_delegate(monkeypatch):
|
|
14
|
+
login, logout = Mock(), Mock()
|
|
15
|
+
monkeypatch.setattr(auth_command.auth, "login", login)
|
|
16
|
+
monkeypatch.setattr(auth_command.auth, "logout", logout)
|
|
17
|
+
assert runner.invoke(app, ["login"]).exit_code == 0
|
|
18
|
+
assert runner.invoke(app, ["logout"]).exit_code == 0
|
|
19
|
+
login.assert_called_once_with()
|
|
20
|
+
logout.assert_called_once_with()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def user():
|
|
24
|
+
return {
|
|
25
|
+
"login": "octo",
|
|
26
|
+
"name": "Octo",
|
|
27
|
+
"public_repos": 1,
|
|
28
|
+
"followers": 2,
|
|
29
|
+
"following": 3,
|
|
30
|
+
"created_at": "2020-01-01T00:00:00Z",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_me_and_scout_display_user(monkeypatch):
|
|
35
|
+
get_me, get_user, display = (
|
|
36
|
+
Mock(return_value=user()),
|
|
37
|
+
Mock(return_value=user()),
|
|
38
|
+
Mock(),
|
|
39
|
+
)
|
|
40
|
+
monkeypatch.setattr("app.services.github.get_authenticated_user", get_me)
|
|
41
|
+
monkeypatch.setattr("app.services.github.get_user", get_user)
|
|
42
|
+
monkeypatch.setattr(me_command, "display_user", display)
|
|
43
|
+
assert runner.invoke(app, ["me"]).exit_code == 0
|
|
44
|
+
assert runner.invoke(app, ["scout", "octo"]).exit_code == 0
|
|
45
|
+
get_me.assert_called_once_with()
|
|
46
|
+
get_user.assert_called_once_with("octo")
|
|
47
|
+
assert display.call_count == 2
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_list_handles_empty_and_missing_optional_fields(monkeypatch):
|
|
51
|
+
repo = {"full_name": "o/r", "html_url": "https://github.com/o/r"}
|
|
52
|
+
monkeypatch.setattr(
|
|
53
|
+
"app.services.github.get_authenticated_user",
|
|
54
|
+
Mock(return_value={"login": "octo"}),
|
|
55
|
+
)
|
|
56
|
+
monkeypatch.setattr(
|
|
57
|
+
"app.services.github.get_user_repos", Mock(side_effect=[[], [repo]])
|
|
58
|
+
)
|
|
59
|
+
assert runner.invoke(app, ["list"]).exit_code == 0
|
|
60
|
+
result = runner.invoke(app, ["list"])
|
|
61
|
+
assert result.exit_code == 0
|
|
62
|
+
assert "o/r" in result.output
|
|
63
|
+
assert "LIST" in result.output
|
|
64
|
+
assert "octo" in result.output
|
|
65
|
+
assert "Repositories (1)" in result.output
|
|
66
|
+
assert "—" in result.output
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_list_renders_repository_columns(monkeypatch):
|
|
70
|
+
monkeypatch.setattr(
|
|
71
|
+
"app.services.github.get_authenticated_user",
|
|
72
|
+
Mock(return_value={"login": "octo"}),
|
|
73
|
+
)
|
|
74
|
+
monkeypatch.setattr(
|
|
75
|
+
"app.services.github.get_user_repos",
|
|
76
|
+
Mock(
|
|
77
|
+
return_value=[
|
|
78
|
+
{
|
|
79
|
+
"full_name": "octo/a-really-long-repository-name",
|
|
80
|
+
"visibility": "public",
|
|
81
|
+
"language": "Python",
|
|
82
|
+
"updated_at": "2026-09-25T12:34:56Z",
|
|
83
|
+
"default_branch": "main",
|
|
84
|
+
"stargazers_count": 1_234,
|
|
85
|
+
"forks_count": 56,
|
|
86
|
+
"open_issues_count": 7,
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
result = runner.invoke(app, ["list"])
|
|
93
|
+
|
|
94
|
+
assert result.exit_code == 0
|
|
95
|
+
assert [column.header for column in build_repository_table([{}]).columns] == [
|
|
96
|
+
"Repository",
|
|
97
|
+
"Visibility",
|
|
98
|
+
"Language",
|
|
99
|
+
"Updated",
|
|
100
|
+
"Branch",
|
|
101
|
+
"Stars",
|
|
102
|
+
"Forks",
|
|
103
|
+
"Issues",
|
|
104
|
+
]
|
|
105
|
+
assert "1,234" in result.output
|
|
106
|
+
assert "2026-09-25" in result.output
|
|
107
|
+
assert "Visibility" in result.output
|
|
108
|
+
assert "Language" in result.output
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_list_boxy_renders_repository_panels(monkeypatch):
|
|
112
|
+
monkeypatch.setattr(
|
|
113
|
+
"app.services.github.get_authenticated_user",
|
|
114
|
+
Mock(return_value={"login": "octo"}),
|
|
115
|
+
)
|
|
116
|
+
monkeypatch.setattr(
|
|
117
|
+
"app.services.github.get_user_repos",
|
|
118
|
+
Mock(
|
|
119
|
+
return_value=[
|
|
120
|
+
{
|
|
121
|
+
"full_name": "octo/reconcli",
|
|
122
|
+
"visibility": "public",
|
|
123
|
+
"language": "Python",
|
|
124
|
+
"default_branch": "main",
|
|
125
|
+
"stargazers_count": 0,
|
|
126
|
+
"forks_count": 0,
|
|
127
|
+
"open_issues_count": 0,
|
|
128
|
+
"topics": [
|
|
129
|
+
"cli",
|
|
130
|
+
"developer-tools",
|
|
131
|
+
"github",
|
|
132
|
+
"python",
|
|
133
|
+
"rich-cli",
|
|
134
|
+
"terminal",
|
|
135
|
+
],
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
result = runner.invoke(app, ["list", "--boxy"])
|
|
142
|
+
|
|
143
|
+
assert result.exit_code == 0
|
|
144
|
+
assert "LIST" in result.output
|
|
145
|
+
assert "octo/reconcli" in result.output
|
|
146
|
+
assert "Public · Python · main" in result.output
|
|
147
|
+
assert "Stars 0 Forks 0 Issues 0" in result.output
|
|
148
|
+
assert (
|
|
149
|
+
"cli · developer-tools · github · python · rich-cli + 1 more..."
|
|
150
|
+
in result.output
|
|
151
|
+
)
|
|
152
|
+
assert "Repositories (" not in result.output
|
|
153
|
+
assert result.output.count("─" * 70) >= 2
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_details_rejects_both_views():
|
|
157
|
+
result = runner.invoke(app, ["details", "o", "r", "--contributors", "--languages"])
|
|
158
|
+
assert result.exit_code != 0
|
|
159
|
+
assert "Choose either" in result.output
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_details_default_calls_services_and_prints(monkeypatch):
|
|
163
|
+
details = {"full_name": "o/r", "description": "desc", "size": 1}
|
|
164
|
+
monkeypatch.setattr(
|
|
165
|
+
"app.services.github.get_repo_details", Mock(return_value=details)
|
|
166
|
+
)
|
|
167
|
+
monkeypatch.setattr(
|
|
168
|
+
"app.services.github.get_languages", Mock(return_value={"Python": 1})
|
|
169
|
+
)
|
|
170
|
+
monkeypatch.setattr(
|
|
171
|
+
"app.services.github.get_top_contributors", Mock(return_value=[])
|
|
172
|
+
)
|
|
173
|
+
result = runner.invoke(app, ["details", "o", "r"])
|
|
174
|
+
assert result.exit_code == 0
|
|
175
|
+
assert "Repository" in result.output
|
|
176
|
+
assert "Languages" in result.output
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
from unittest.mock import Mock
|
|
2
|
-
|
|
3
|
-
from typer.testing import CliRunner
|
|
4
|
-
|
|
5
|
-
from app.cli import app
|
|
6
|
-
from app.commands import auth as auth_command
|
|
7
|
-
from app.commands import me as me_command
|
|
8
|
-
|
|
9
|
-
runner = CliRunner()
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def test_login_logout_delegate(monkeypatch):
|
|
13
|
-
login, logout = Mock(), Mock()
|
|
14
|
-
monkeypatch.setattr(auth_command.auth, "login", login)
|
|
15
|
-
monkeypatch.setattr(auth_command.auth, "logout", logout)
|
|
16
|
-
assert runner.invoke(app, ["login"]).exit_code == 0
|
|
17
|
-
assert runner.invoke(app, ["logout"]).exit_code == 0
|
|
18
|
-
login.assert_called_once_with()
|
|
19
|
-
logout.assert_called_once_with()
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def user():
|
|
23
|
-
return {
|
|
24
|
-
"login": "octo",
|
|
25
|
-
"name": "Octo",
|
|
26
|
-
"public_repos": 1,
|
|
27
|
-
"followers": 2,
|
|
28
|
-
"following": 3,
|
|
29
|
-
"created_at": "2020-01-01T00:00:00Z",
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def test_me_and_scout_display_user(monkeypatch):
|
|
34
|
-
get_me, get_user, display = (
|
|
35
|
-
Mock(return_value=user()),
|
|
36
|
-
Mock(return_value=user()),
|
|
37
|
-
Mock(),
|
|
38
|
-
)
|
|
39
|
-
monkeypatch.setattr("app.services.github.get_authenticated_user", get_me)
|
|
40
|
-
monkeypatch.setattr("app.services.github.get_user", get_user)
|
|
41
|
-
monkeypatch.setattr(me_command, "display_user", display)
|
|
42
|
-
assert runner.invoke(app, ["me"]).exit_code == 0
|
|
43
|
-
assert runner.invoke(app, ["scout", "octo"]).exit_code == 0
|
|
44
|
-
get_me.assert_called_once_with()
|
|
45
|
-
get_user.assert_called_once_with("octo")
|
|
46
|
-
assert display.call_count == 2
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def test_list_handles_empty_and_missing_optional_fields(monkeypatch):
|
|
50
|
-
repo = {"full_name": "o/r", "html_url": "https://github.com/o/r"}
|
|
51
|
-
monkeypatch.setattr(
|
|
52
|
-
"app.services.github.get_user_repos", Mock(side_effect=[[], [repo]])
|
|
53
|
-
)
|
|
54
|
-
assert runner.invoke(app, ["list"]).exit_code == 0
|
|
55
|
-
result = runner.invoke(app, ["list"])
|
|
56
|
-
assert result.exit_code == 0
|
|
57
|
-
assert "o/r" in result.output
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def test_details_rejects_both_views():
|
|
61
|
-
result = runner.invoke(app, ["details", "o", "r", "--contributors", "--languages"])
|
|
62
|
-
assert result.exit_code != 0
|
|
63
|
-
assert "Choose either" in result.output
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def test_details_default_calls_services_and_prints(monkeypatch):
|
|
67
|
-
details = {"full_name": "o/r", "description": "desc", "size": 1}
|
|
68
|
-
monkeypatch.setattr(
|
|
69
|
-
"app.services.github.get_repo_details", Mock(return_value=details)
|
|
70
|
-
)
|
|
71
|
-
monkeypatch.setattr(
|
|
72
|
-
"app.services.github.get_languages", Mock(return_value={"Python": 1})
|
|
73
|
-
)
|
|
74
|
-
monkeypatch.setattr(
|
|
75
|
-
"app.services.github.get_top_contributors", Mock(return_value=[])
|
|
76
|
-
)
|
|
77
|
-
result = runner.invoke(app, ["details", "o", "r"])
|
|
78
|
-
assert result.exit_code == 0
|
|
79
|
-
assert "Repository" in result.output
|
|
80
|
-
assert "Languages" in result.output
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|