recon-github 0.1.7__tar.gz → 1.0.0__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.
Files changed (31) hide show
  1. recon_github-0.1.7/.agents/rich-ui-architect.md → recon_github-1.0.0/.github/agents/rich-ui-architect.agent.md +5 -0
  2. {recon_github-0.1.7 → recon_github-1.0.0}/PKG-INFO +1 -1
  3. {recon_github-0.1.7 → recon_github-1.0.0}/app/commands/repo.py +2 -40
  4. {recon_github-0.1.7 → recon_github-1.0.0}/app/ui/repo.py +76 -64
  5. {recon_github-0.1.7 → recon_github-1.0.0}/pyproject.toml +1 -1
  6. {recon_github-0.1.7 → recon_github-1.0.0}/tests/test_cli_commands.py +39 -3
  7. {recon_github-0.1.7 → recon_github-1.0.0}/uv.lock +1 -1
  8. {recon_github-0.1.7 → recon_github-1.0.0}/.github/workflows/cd.yml +0 -0
  9. {recon_github-0.1.7 → recon_github-1.0.0}/.github/workflows/ci.yml +0 -0
  10. {recon_github-0.1.7 → recon_github-1.0.0}/.gitignore +0 -0
  11. {recon_github-0.1.7 → recon_github-1.0.0}/.python-version +0 -0
  12. {recon_github-0.1.7 → recon_github-1.0.0}/LICENSE +0 -0
  13. {recon_github-0.1.7 → recon_github-1.0.0}/README.md +0 -0
  14. {recon_github-0.1.7 → recon_github-1.0.0}/ROADMAP.md +0 -0
  15. {recon_github-0.1.7 → recon_github-1.0.0}/app/cli.py +0 -0
  16. {recon_github-0.1.7 → recon_github-1.0.0}/app/commands/auth.py +0 -0
  17. {recon_github-0.1.7 → recon_github-1.0.0}/app/commands/me.py +0 -0
  18. {recon_github-0.1.7 → recon_github-1.0.0}/app/services/auth.py +0 -0
  19. {recon_github-0.1.7 → recon_github-1.0.0}/app/services/github.py +0 -0
  20. {recon_github-0.1.7 → recon_github-1.0.0}/app/services/github_errors.py +0 -0
  21. {recon_github-0.1.7 → recon_github-1.0.0}/app/services/storage.py +0 -0
  22. {recon_github-0.1.7 → recon_github-1.0.0}/app/ui/avatar.py +0 -0
  23. {recon_github-0.1.7 → recon_github-1.0.0}/app/ui/display.py +0 -0
  24. {recon_github-0.1.7 → recon_github-1.0.0}/main.py +0 -0
  25. {recon_github-0.1.7 → recon_github-1.0.0}/tests/test_auth_service.py +0 -0
  26. {recon_github-0.1.7 → recon_github-1.0.0}/tests/test_cli.py +0 -0
  27. {recon_github-0.1.7 → recon_github-1.0.0}/tests/test_github_errors.py +0 -0
  28. {recon_github-0.1.7 → recon_github-1.0.0}/tests/test_github_service.py +0 -0
  29. {recon_github-0.1.7 → recon_github-1.0.0}/tests/test_repo_helpers.py +0 -0
  30. {recon_github-0.1.7 → recon_github-1.0.0}/tests/test_storage.py +0 -0
  31. {recon_github-0.1.7 → recon_github-1.0.0}/tests/test_ui_repo.py +0 -0
@@ -1,3 +1,8 @@
1
+ ---
2
+ name: rich-ui-architect
3
+ description: "Design and implement consistent, reusable Rich terminal UI for the Recon CLI."
4
+ ---
5
+
1
6
  # Recon CLI — UI/UX & Rich Component Agent
2
7
 
3
8
  You are the UI/UX and terminal-interface agent for **Recon CLI**, a Python CLI application for exploring GitHub profiles and repositories.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: recon-github
3
- Version: 0.1.7
3
+ Version: 1.0.0
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
@@ -105,7 +105,6 @@ def details(
105
105
  get_repo_details,
106
106
  get_top_contributors,
107
107
  )
108
- from app.ui.repo import display_view_header
109
108
 
110
109
  details = get_repo_details(owner, repo)
111
110
 
@@ -153,43 +152,6 @@ def details(
153
152
  get_top_contributors(owner, repo, limit=5),
154
153
  )
155
154
 
156
- display_view_header("Details", details.get("full_name", f"{owner}/{repo}"))
155
+ from app.ui.repo import display_repo_details
157
156
 
158
- # Repo Details
159
- typer.echo("Repository")
160
- typer.echo("-----------")
161
- typer.echo(f"{details.get('full_name', f'{owner}/{repo}')}") # Name
162
- description = details.get("description") or "—"
163
- typer.echo(f"description: {format_description(description)}") # Description
164
- typer.echo(
165
- f"visibility: {details.get('visibility') or ('private' if details.get('private') else 'public')}" # Visibility
166
- )
167
- typer.echo(
168
- f"url: {details.get('html_url') or f'https://github.com/{owner}/{repo}'}"
169
- ) # URL
170
-
171
- # Stats
172
- typer.echo("")
173
- typer.echo("Stats")
174
- typer.echo("-----------")
175
- typer.echo(f"stars: {details.get('stargazers_count') or 0}") # Stars
176
- typer.echo(f"forks: {details.get('forks_count')}") # Forks
177
- typer.echo(f"issues: {details.get('open_issues_count') or 0}") # Issues
178
- typer.echo(f"size: {format_size(details.get('size'))}") # Size
179
-
180
- typer.echo("")
181
- typer.echo("Contributions")
182
- typer.echo("-----------")
183
- if not contributions:
184
- typer.echo("No contributor data returned.")
185
- else:
186
- for contributor in contributions:
187
- typer.echo(f"{contributor['login']}: {contributor['commits']} commits")
188
-
189
- # Tech
190
- typer.echo("")
191
- typer.echo("Languages")
192
- typer.echo("---------")
193
-
194
- for language in format_languages(language_data):
195
- typer.echo(language)
157
+ display_repo_details(details, language_data, contributions)
@@ -187,7 +187,7 @@ def format_bytes(size: int | None) -> str:
187
187
 
188
188
  def display_repo_details(
189
189
  details: dict,
190
- languages: list[str],
190
+ languages: dict[str, int],
191
191
  contributions: list[dict],
192
192
  ):
193
193
  """Display a repository overview."""
@@ -197,70 +197,51 @@ def display_repo_details(
197
197
  visibility = details.get("visibility") or (
198
198
  "private" if details.get("private") else "public"
199
199
  )
200
- primary_language = details.get("language") or "—"
201
200
  url = details.get("html_url") or "—"
202
201
 
203
202
  header_panel = build_view_header("Details", full_name)
204
203
 
205
- # Statistics
206
- stats = Table(
207
- show_header=False,
208
- box=None,
209
- pad_edge=False,
210
- )
211
-
212
- stats.add_column("Metric", style="dim")
213
- stats.add_column("Value", justify="right")
214
-
215
- stats.add_row(
216
- "★ Stars",
217
- str(details.get("stargazers_count") or 0),
218
- )
219
- stats.add_row(
220
- "⑂ Forks",
221
- str(details.get("forks_count") or 0),
222
- )
223
- stats.add_row(
224
- "! Issues",
225
- str(details.get("open_issues_count") or 0),
226
- )
227
- stats.add_row(
228
- "Size",
229
- format_size(details.get("size")),
230
- )
231
-
232
- stats_panel = Panel(
233
- stats,
234
- title="Statistics",
235
- expand=False,
236
- )
237
-
238
204
  # Contributions
239
205
  contribution_table = Table(
240
206
  show_header=False,
241
207
  box=None,
242
208
  pad_edge=False,
209
+ padding=(0, 1),
243
210
  )
244
211
 
245
212
  contribution_table.add_column("Contributor")
246
213
  contribution_table.add_column("Commits", justify="right")
214
+ contribution_table.add_column("Share", justify="right")
247
215
 
248
- if contributions:
249
- for contributor in contributions:
216
+ ranked_contributions = contributions[:5]
217
+ if ranked_contributions:
218
+ total_commits = sum(
219
+ int(contributor.get("commits") or 0) for contributor in ranked_contributions
220
+ )
221
+ for index, contributor in enumerate(ranked_contributions):
222
+ commits = int(contributor.get("commits") or 0)
223
+ percentage = commits / total_commits * 100 if total_commits else 0
250
224
  contribution_table.add_row(
251
- contributor["login"],
252
- str(contributor["commits"]),
225
+ str(contributor.get("login") or "Unknown"),
226
+ _format_count(commits),
227
+ _overview_bar_label(
228
+ percentage, spaced=index < len(ranked_contributions) - 1
229
+ ),
253
230
  )
254
231
  else:
255
232
  contribution_table.add_row(
256
233
  "No contributor data",
257
234
  "—",
235
+ "",
258
236
  )
237
+ for _ in range(len(ranked_contributions) if ranked_contributions else 1, 5):
238
+ contribution_table.add_row("", "", "")
259
239
 
260
240
  contributions_panel = Panel(
261
241
  contribution_table,
262
- title="Contributions",
242
+ title="Contributors",
263
243
  expand=False,
244
+ border_style="cyan",
264
245
  )
265
246
 
266
247
  # Languages
@@ -268,51 +249,82 @@ def display_repo_details(
268
249
  show_header=False,
269
250
  box=None,
270
251
  pad_edge=False,
252
+ expand=True,
253
+ padding=(0, 1),
271
254
  )
272
255
 
273
256
  language_table.add_column("Language")
274
- language_table.add_column("Usage")
257
+ language_table.add_column("Usage", justify="right")
275
258
 
276
- for language in languages:
277
- language_name, percentage = language.split(": ")
278
- language_table.add_row(
279
- language_name,
280
- percentage,
281
- )
259
+ if not languages:
260
+ language_table.add_row("No language data returned.", "")
261
+ else:
262
+ total_bytes = sum(languages.values())
263
+ ranked_languages = sorted(
264
+ languages.items(), key=lambda item: item[1], reverse=True
265
+ )[:5]
266
+ for index, (language, byte_count) in enumerate(ranked_languages):
267
+ percentage = byte_count / total_bytes * 100 if total_bytes else 0
268
+ language_table.add_row(
269
+ language,
270
+ _overview_bar_label(
271
+ percentage, spaced=index < len(ranked_languages) - 1
272
+ ),
273
+ )
274
+ for _ in range(min(len(languages), 5), 5):
275
+ language_table.add_row("", "")
276
+ if not languages:
277
+ for _ in range(1, 5):
278
+ language_table.add_row("", "")
282
279
 
283
280
  languages_panel = Panel(
284
281
  language_table,
285
282
  title="Languages",
286
283
  expand=False,
284
+ border_style="cyan",
287
285
  )
288
286
 
289
- console.print(header_panel)
290
- console.print(Text(description, style="dim"))
291
- console.print(
292
- Text.assemble(
293
- (visibility.capitalize(), "cyan"),
294
- (" "),
295
- (primary_language, "green"),
296
- )
287
+ repository = Table(show_header=False, box=None, pad_edge=False, expand=True)
288
+ repository.add_column("Label", style="dim", no_wrap=True)
289
+ repository.add_column("Value", overflow="fold")
290
+ repository.add_row("Description", description)
291
+ repository.add_row("Visibility", visibility.capitalize())
292
+ repository.add_row("URL", url)
293
+ repository.add_row("★ Stars", str(details.get("stargazers_count") or 0))
294
+ repository.add_row("⑂ Forks", str(details.get("forks_count") or 0))
295
+ repository.add_row("! Issues", str(details.get("open_issues_count") or 0))
296
+ repository.add_row("Size", format_size(details.get("size")))
297
+ repository_panel = Panel(
298
+ repository,
299
+ title="Repository",
300
+ border_style="cyan",
301
+ width=60,
297
302
  )
298
- console.print()
299
303
 
304
+ console.print(header_panel)
305
+ console.print()
300
306
  console.print(
301
307
  Columns(
302
- [stats_panel, contributions_panel],
308
+ [contributions_panel, languages_panel],
303
309
  expand=False,
304
310
  equal=True,
311
+ padding=(0, 0),
305
312
  )
306
313
  )
314
+ console.print()
315
+ console.print(repository_panel)
307
316
 
308
- console.print(languages_panel)
309
317
 
310
- console.print(
311
- Text(
312
- f"↗ {url}",
313
- style="dim",
314
- )
315
- )
318
+ def _progress_bar(percentage: float, width: int = 12) -> str:
319
+ """Build a compact proportional bar for overview panels."""
320
+ filled = round(max(0, min(100, percentage)) / 100 * width)
321
+ return f"[green]{'█' * filled}[/green]"
322
+
323
+
324
+ def _overview_bar_label(percentage: float, spaced: bool = True) -> str:
325
+ """Render an overview bar with a compact line of breathing room."""
326
+ suffix = "\n" if spaced else ""
327
+ return f"{_progress_bar(percentage)} {percentage:.1f}%{suffix}"
316
328
 
317
329
 
318
330
  def display_contributors(
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "recon-github"
3
- version = "0.1.7"
3
+ version = "1.0.0"
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"
@@ -160,17 +160,53 @@ def test_details_rejects_both_views():
160
160
 
161
161
 
162
162
  def test_details_default_calls_services_and_prints(monkeypatch):
163
- details = {"full_name": "o/r", "description": "desc", "size": 1}
163
+ details = {
164
+ "full_name": "o/r",
165
+ "description": "desc",
166
+ "visibility": "public",
167
+ "html_url": "https://github.com/o/r",
168
+ "stargazers_count": 3,
169
+ "forks_count": 2,
170
+ "open_issues_count": 1,
171
+ "size": 1,
172
+ }
164
173
  monkeypatch.setattr(
165
174
  "app.services.github.get_repo_details", Mock(return_value=details)
166
175
  )
167
176
  monkeypatch.setattr(
168
- "app.services.github.get_languages", Mock(return_value={"Python": 1})
177
+ "app.services.github.get_languages",
178
+ Mock(
179
+ return_value={
180
+ "Python": 6,
181
+ "Rust": 5,
182
+ "Go": 4,
183
+ "JavaScript": 3,
184
+ "HTML": 2,
185
+ "CSS": 1,
186
+ }
187
+ ),
169
188
  )
170
189
  monkeypatch.setattr(
171
- "app.services.github.get_top_contributors", Mock(return_value=[])
190
+ "app.services.github.get_top_contributors",
191
+ Mock(
192
+ return_value=[
193
+ {"login": "octo", "commits": 6},
194
+ {"login": "user2", "commits": 5},
195
+ {"login": "user3", "commits": 4},
196
+ {"login": "user4", "commits": 3},
197
+ {"login": "user5", "commits": 2},
198
+ {"login": "user6", "commits": 1},
199
+ ]
200
+ ),
172
201
  )
173
202
  result = runner.invoke(app, ["details", "o", "r"])
174
203
  assert result.exit_code == 0
204
+ assert "DETAILS" in result.output
205
+ assert "Contributors" in result.output
175
206
  assert "Repository" in result.output
176
207
  assert "Languages" in result.output
208
+ assert "octo" in result.output
209
+ assert "user5" in result.output
210
+ assert "user6" not in result.output
211
+ assert "CSS" not in result.output
212
+ assert "Stats" not in result.output
@@ -423,7 +423,7 @@ wheels = [
423
423
 
424
424
  [[package]]
425
425
  name = "recon-github"
426
- version = "0.1.7"
426
+ version = "1.0.0"
427
427
  source = { editable = "." }
428
428
  dependencies = [
429
429
  { name = "dotenv" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes