Skip to content
Open
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
60 changes: 60 additions & 0 deletions src/oss_dev/cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Professional Typer-based CLI with consistent UX, rich output, and actionable errors.
"""

import json
import re
from pathlib import Path
from typing import Optional
Expand Down Expand Up @@ -43,6 +44,10 @@ def _parse_issue_number(issue_url: str) -> int:
return int(match.group(1))


def _echo_json(data: object) -> None:
typer.echo(json.dumps(data))


@app.callback(invoke_without_command=True)
def main(
version: bool = typer.Option(False, "--version", "-V", help="Show version.", callback=_version_callback, is_eager=True),
Expand All @@ -57,8 +62,22 @@ def discover_repos(
language: Optional[str] = typer.Option(None, "--language", "-l", help="Filter by language."),
good_first_issues: bool = typer.Option(False, "--good-first-issues", "-g", help="Only repos with good first issues."),
limit: int = typer.Option(10, "--limit", help="Maximum results."),
json_output: bool = typer.Option(False, "--json", help="Output structured JSON."),
) -> None:
"""Discover open source repositories to contribute to."""
if json_output:
_echo_json(
{
"repositories": [],
"filters": {
"language": language,
"good_first_issues": good_first_issues,
"limit": limit,
},
}
)
return

typer.echo("Discovering repositories...")


Expand All @@ -68,8 +87,23 @@ def discover_issues(
good_first: bool = typer.Option(False, "--good-first", "-g", help="Good first issues only."),
label: Optional[str] = typer.Option(None, "--label", "-l", help="Filter by label."),
limit: int = typer.Option(10, "--limit", help="Maximum results."),
json_output: bool = typer.Option(False, "--json", help="Output structured JSON."),
) -> None:
"""Discover issues to work on."""
if json_output:
_echo_json(
{
"issues": [],
"filters": {
"repo": repo,
"good_first": good_first,
"label": label,
"limit": limit,
},
}
)
return

typer.echo("Discovering issues...")


Expand All @@ -79,8 +113,21 @@ def issues_list(
state: str = typer.Option("open", "--state", "-s", help="Issue state: open, closed, all."),
label: Optional[str] = typer.Option(None, "--label", "-l", help="Filter by label."),
limit: int = typer.Option(10, "--limit", help="Maximum results."),
json_output: bool = typer.Option(False, "--json", help="Output structured JSON."),
) -> None:
"""List issues for a repository."""
if json_output:
_echo_json(
{
"repo": repo,
"state": state,
"label": label,
"limit": limit,
"issues": [],
}
)
return

typer.echo(f"Listing issues for {repo}...")


Expand All @@ -97,8 +144,21 @@ def issues_show(
def analyze(
target: str = typer.Argument(..., help="Repository path or URL to analyze."),
output: Optional[Path] = typer.Option(None, "--output", "-o", help="Output file for analysis results."),
json_output: bool = typer.Option(False, "--json", help="Output structured JSON."),
) -> None:
"""Analyze a repository or issue for contribution readiness."""
if json_output:
_echo_json(
{
"target": target,
"output": str(output) if output else None,
"analysis": {
"status": "pending",
},
}
)
return

typer.echo(f"Analyzing {target}...")


Expand Down
83 changes: 83 additions & 0 deletions tests/cli/test_new_cli.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import json

from typer.testing import CliRunner

from oss_dev.cli.app import app
Expand Down Expand Up @@ -36,3 +38,84 @@ def test_mentor_rejects_non_positive_issue_number():
assert result.exit_code != 0
assert "positive issue" in result.output
assert "number" in result.output


def test_discover_repos_json_output():
result = runner.invoke(
app,
["discover", "repos", "--language", "Python", "--good-first-issues", "--limit", "5", "--json"],
)

assert result.exit_code == 0
data = json.loads(result.output)
assert data == {
"repositories": [],
"filters": {
"language": "Python",
"good_first_issues": True,
"limit": 5,
},
}


def test_discover_issues_json_output():
result = runner.invoke(
app,
["discover", "issues", "--repo", "owner/repo", "--good-first", "--label", "bug", "--limit", "7", "--json"],
)

assert result.exit_code == 0
data = json.loads(result.output)
assert data == {
"issues": [],
"filters": {
"repo": "owner/repo",
"good_first": True,
"label": "bug",
"limit": 7,
},
}


def test_issues_list_json_output():
result = runner.invoke(
app,
["issues", "list", "owner/repo", "--state", "closed", "--label", "help wanted", "--limit", "3", "--json"],
)

assert result.exit_code == 0
data = json.loads(result.output)
assert data == {
"repo": "owner/repo",
"state": "closed",
"label": "help wanted",
"limit": 3,
"issues": [],
}


def test_analyze_json_output():
result = runner.invoke(
app,
["analyze", "https://github.com/owner/repo", "--output", "analysis.json", "--json"],
)

assert result.exit_code == 0
data = json.loads(result.output)
assert data == {
"target": "https://github.com/owner/repo",
"output": "analysis.json",
"analysis": {
"status": "pending",
},
}


def test_discover_repos_text_output_unchanged_without_json():
result = runner.invoke(
app,
["discover", "repos"],
)

assert result.exit_code == 0
assert result.output == "Discovering repositories...\n"