rio-cli 0.1.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.
- rio_cli-0.1.0/.gitignore +45 -0
- rio_cli-0.1.0/LICENSE +15 -0
- rio_cli-0.1.0/PKG-INFO +66 -0
- rio_cli-0.1.0/README.md +49 -0
- rio_cli-0.1.0/pyproject.toml +38 -0
- rio_cli-0.1.0/rio_cli/__init__.py +0 -0
- rio_cli-0.1.0/rio_cli/config.py +40 -0
- rio_cli-0.1.0/rio_cli/git.py +90 -0
- rio_cli-0.1.0/rio_cli/main.py +82 -0
- rio_cli-0.1.0/rio_cli/output.py +39 -0
- rio_cli-0.1.0/rio_cli/post.py +35 -0
- rio_cli-0.1.0/rio_cli/rio_config.py +34 -0
- rio_cli-0.1.0/rio_cli/utils.py +33 -0
rio_cli-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
2
|
+
|
|
3
|
+
# Dependencies
|
|
4
|
+
node_modules
|
|
5
|
+
.pnp
|
|
6
|
+
.pnp.js
|
|
7
|
+
|
|
8
|
+
# Local env files
|
|
9
|
+
.env
|
|
10
|
+
.env.local
|
|
11
|
+
.env.development.local
|
|
12
|
+
.env.test.local
|
|
13
|
+
.env.production.local
|
|
14
|
+
|
|
15
|
+
# Testing
|
|
16
|
+
coverage
|
|
17
|
+
|
|
18
|
+
# Turbo
|
|
19
|
+
.turbo
|
|
20
|
+
|
|
21
|
+
# Vercel
|
|
22
|
+
.vercel
|
|
23
|
+
|
|
24
|
+
# Build Outputs
|
|
25
|
+
.next/
|
|
26
|
+
out/
|
|
27
|
+
build
|
|
28
|
+
dist
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Debug
|
|
32
|
+
npm-debug.log*
|
|
33
|
+
yarn-debug.log*
|
|
34
|
+
yarn-error.log*
|
|
35
|
+
|
|
36
|
+
# Misc
|
|
37
|
+
.DS_Store
|
|
38
|
+
*.pem
|
|
39
|
+
.venv/
|
|
40
|
+
__pycache__/
|
|
41
|
+
*.pyc
|
|
42
|
+
|
|
43
|
+
tsconfig.tsbuildinfo
|
|
44
|
+
|
|
45
|
+
.config/
|
rio_cli-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, JayTheCoder77
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
rio_cli-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: rio-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Command-line client for Rio, an automated code review tool.
|
|
5
|
+
Project-URL: Homepage, https://github.com/JayTheCoder77/rio
|
|
6
|
+
Project-URL: Repository, https://github.com/JayTheCoder77/rio
|
|
7
|
+
Author: JayTheCoder77
|
|
8
|
+
License-Expression: ISC
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Python: <3.13,>=3.12
|
|
11
|
+
Requires-Dist: httpx>=0.28.1
|
|
12
|
+
Requires-Dist: pyyaml>=6.0
|
|
13
|
+
Requires-Dist: rich>=15.0.0
|
|
14
|
+
Requires-Dist: rio-review-core<0.2.0,>=0.1.0
|
|
15
|
+
Requires-Dist: typer>=0.27.1
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# rio-cli
|
|
19
|
+
|
|
20
|
+
The command-line client for [Rio](https://github.com/JayTheCoder77/rio), an automated
|
|
21
|
+
code review tool. Review your local diffs from the terminal, backed by the same
|
|
22
|
+
AI review engine used by Rio's GitHub App.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
pip install rio-cli
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
This installs the `rio` command.
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
# Review currently staged changes
|
|
36
|
+
rio review --staged
|
|
37
|
+
|
|
38
|
+
# Review uncommitted changes (staged + unstaged)
|
|
39
|
+
rio review --uncommitted
|
|
40
|
+
|
|
41
|
+
# Review the last commit
|
|
42
|
+
rio review --committed
|
|
43
|
+
|
|
44
|
+
# Review a diff from a file
|
|
45
|
+
rio review --diff path/to.diff
|
|
46
|
+
|
|
47
|
+
# Include untracked files alongside any of the above
|
|
48
|
+
rio review --staged --include-untracked
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Configuration
|
|
52
|
+
|
|
53
|
+
Rio reads `~/.config/rio/config.toml` for the AI engine URL and API key:
|
|
54
|
+
|
|
55
|
+
```toml
|
|
56
|
+
[api]
|
|
57
|
+
url = "https://your-rio-instance.example.com"
|
|
58
|
+
api_key = "your-api-key"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
If no config file is found, the CLI defaults to `http://localhost:8000` for local
|
|
62
|
+
development against a self-hosted `ai-engine`.
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
[ISC](LICENSE) © 2026 JayTheCoder77
|
rio_cli-0.1.0/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# rio-cli
|
|
2
|
+
|
|
3
|
+
The command-line client for [Rio](https://github.com/JayTheCoder77/rio), an automated
|
|
4
|
+
code review tool. Review your local diffs from the terminal, backed by the same
|
|
5
|
+
AI review engine used by Rio's GitHub App.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pip install rio-cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This installs the `rio` command.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
# Review currently staged changes
|
|
19
|
+
rio review --staged
|
|
20
|
+
|
|
21
|
+
# Review uncommitted changes (staged + unstaged)
|
|
22
|
+
rio review --uncommitted
|
|
23
|
+
|
|
24
|
+
# Review the last commit
|
|
25
|
+
rio review --committed
|
|
26
|
+
|
|
27
|
+
# Review a diff from a file
|
|
28
|
+
rio review --diff path/to.diff
|
|
29
|
+
|
|
30
|
+
# Include untracked files alongside any of the above
|
|
31
|
+
rio review --staged --include-untracked
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Configuration
|
|
35
|
+
|
|
36
|
+
Rio reads `~/.config/rio/config.toml` for the AI engine URL and API key:
|
|
37
|
+
|
|
38
|
+
```toml
|
|
39
|
+
[api]
|
|
40
|
+
url = "https://your-rio-instance.example.com"
|
|
41
|
+
api_key = "your-api-key"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
If no config file is found, the CLI defaults to `http://localhost:8000` for local
|
|
45
|
+
development against a self-hosted `ai-engine`.
|
|
46
|
+
|
|
47
|
+
## License
|
|
48
|
+
|
|
49
|
+
[ISC](LICENSE) © 2026 JayTheCoder77
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "rio-cli"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Command-line client for Rio, an automated code review tool."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "ISC"
|
|
7
|
+
authors = [{ name = "JayTheCoder77" }]
|
|
8
|
+
requires-python = ">=3.12,<3.13"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"rio-review-core>=0.1.0,<0.2.0",
|
|
11
|
+
"httpx>=0.28.1",
|
|
12
|
+
"rich>=15.0.0",
|
|
13
|
+
"typer>=0.27.1",
|
|
14
|
+
"pyyaml>=6.0"
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
rio = "rio_cli.main:app"
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = "https://github.com/JayTheCoder77/rio"
|
|
22
|
+
Repository = "https://github.com/JayTheCoder77/rio"
|
|
23
|
+
|
|
24
|
+
[tool.uv.sources]
|
|
25
|
+
rio-review-core = { workspace = true }
|
|
26
|
+
|
|
27
|
+
[tool.uv]
|
|
28
|
+
package = true
|
|
29
|
+
|
|
30
|
+
[build-system]
|
|
31
|
+
requires = ["hatchling"]
|
|
32
|
+
build-backend = "hatchling.build"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
packages = ["rio_cli"]
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.sdist]
|
|
38
|
+
include = ["rio_cli", "README.md", "LICENSE"]
|
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import tomllib
|
|
4
|
+
|
|
5
|
+
from rio_cli.utils import _fail
|
|
6
|
+
|
|
7
|
+
DEFAULT_AI_ENGINE_URL = "http://localhost:8000"
|
|
8
|
+
CONFIG_PATH = Path.home() / ".config" / "rio" / "config.toml"
|
|
9
|
+
|
|
10
|
+
def get_ai_engine_url() -> str:
|
|
11
|
+
if not CONFIG_PATH.exists():
|
|
12
|
+
return DEFAULT_AI_ENGINE_URL
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
with CONFIG_PATH.open("rb") as f:
|
|
16
|
+
data = tomllib.load(f)
|
|
17
|
+
except tomllib.TOMLDecodeError as e:
|
|
18
|
+
_fail(f"Error : invalid config file at {CONFIG_PATH} : {e}")
|
|
19
|
+
|
|
20
|
+
url = data.get("api", {}).get("url")
|
|
21
|
+
if not url:
|
|
22
|
+
_fail(f"Error : {CONFIG_PATH} exists but has no [api] url set.")
|
|
23
|
+
|
|
24
|
+
return url
|
|
25
|
+
|
|
26
|
+
def get_api_key() -> str | None:
|
|
27
|
+
if not CONFIG_PATH.exists():
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
with CONFIG_PATH.open("rb") as f:
|
|
32
|
+
data = tomllib.load(f)
|
|
33
|
+
except tomllib.TOMLDecodeError as e:
|
|
34
|
+
_fail(f"Error : invalid config file at {CONFIG_PATH} : {e}")
|
|
35
|
+
|
|
36
|
+
api_key = data.get("api" , {}).get("api_key")
|
|
37
|
+
if api_key is None:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
return api_key
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from rio_cli.utils import _fail, _run_git_diff, _try_run_git_diff
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _get_staged_diff() -> str:
|
|
8
|
+
return _run_git_diff(["--staged"], "No staged changes found.")
|
|
9
|
+
|
|
10
|
+
def _get_uncommitted_diff() -> str:
|
|
11
|
+
return _run_git_diff(["HEAD"], "No uncommitted changes found.")
|
|
12
|
+
|
|
13
|
+
def _get_diff_from_file(path: str) -> str:
|
|
14
|
+
try:
|
|
15
|
+
return Path(path).read_text()
|
|
16
|
+
except FileNotFoundError:
|
|
17
|
+
_fail(f"Error: File not found : {path}")
|
|
18
|
+
|
|
19
|
+
def _try_staged_diff() -> str:
|
|
20
|
+
return _try_run_git_diff(["--staged"])
|
|
21
|
+
|
|
22
|
+
def _try_uncommitted_diff() -> str:
|
|
23
|
+
return _try_run_git_diff(["HEAD"])
|
|
24
|
+
|
|
25
|
+
def _try_committed_diff() -> str:
|
|
26
|
+
upstream_check = subprocess.run(
|
|
27
|
+
["git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"],
|
|
28
|
+
capture_output=True,
|
|
29
|
+
text=True,
|
|
30
|
+
check=False,
|
|
31
|
+
)
|
|
32
|
+
if upstream_check.returncode != 0:
|
|
33
|
+
return "" # no upstream configured — treat as "nothing to show", not an error
|
|
34
|
+
|
|
35
|
+
upstream_ref = upstream_check.stdout.strip()
|
|
36
|
+
return _try_run_git_diff([f"{upstream_ref}...HEAD"])
|
|
37
|
+
|
|
38
|
+
def _get_committed_diff() -> str:
|
|
39
|
+
"""Hard-fail variant for the explicit --committed flag."""
|
|
40
|
+
upstream_check = subprocess.run(
|
|
41
|
+
["git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"],
|
|
42
|
+
capture_output=True,
|
|
43
|
+
text=True,
|
|
44
|
+
check=False,
|
|
45
|
+
)
|
|
46
|
+
if upstream_check.returncode != 0:
|
|
47
|
+
_fail("Error: no upstream branch configured — can't determine committed-but-unpushed changes.")
|
|
48
|
+
|
|
49
|
+
upstream_ref = upstream_check.stdout.strip()
|
|
50
|
+
return _run_git_diff([f"{upstream_ref}...HEAD"],"No committed (unpushed) changes found.")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _get_combined_diff() -> str:
|
|
54
|
+
"""Default (no-flags) behavior: committed-unpushed + uncommitted, whichever exist."""
|
|
55
|
+
committed = _try_committed_diff()
|
|
56
|
+
uncommitted = _try_run_git_diff(["HEAD"])
|
|
57
|
+
|
|
58
|
+
combined = committed + uncommitted
|
|
59
|
+
if not combined.strip():
|
|
60
|
+
_fail("No committed or uncommitted changes found.")
|
|
61
|
+
|
|
62
|
+
return combined
|
|
63
|
+
|
|
64
|
+
def _get_untracked_files() -> list[str]:
|
|
65
|
+
result = subprocess.run(["git", "ls-files", "--others","--exclude-standard"],
|
|
66
|
+
capture_output=True,
|
|
67
|
+
text=True,
|
|
68
|
+
check=False,
|
|
69
|
+
)
|
|
70
|
+
if result.returncode != 0:
|
|
71
|
+
return []
|
|
72
|
+
return [line for line in result.stdout.splitlines() if line.strip()]
|
|
73
|
+
|
|
74
|
+
def _get_untracked_diff() -> str:
|
|
75
|
+
"""Synthetic diffs for untracked files, via git diff --no-index against /dev/null."""
|
|
76
|
+
paths = _get_untracked_files()
|
|
77
|
+
diffs = []
|
|
78
|
+
for path in paths:
|
|
79
|
+
result = subprocess.run(
|
|
80
|
+
["git", "diff", "--no-index", "/dev/null", path],
|
|
81
|
+
capture_output=True,
|
|
82
|
+
text=True,
|
|
83
|
+
check=False,
|
|
84
|
+
)
|
|
85
|
+
if result.returncode not in (0, 1):
|
|
86
|
+
continue # real error for this file — skip rather than fail the whole command
|
|
87
|
+
if result.stdout.strip():
|
|
88
|
+
diffs.append(result.stdout)
|
|
89
|
+
|
|
90
|
+
return "\n".join(diffs)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
|
|
3
|
+
from rio_cli.git import (
|
|
4
|
+
_get_combined_diff,
|
|
5
|
+
_get_committed_diff,
|
|
6
|
+
_get_diff_from_file,
|
|
7
|
+
_get_staged_diff,
|
|
8
|
+
_get_uncommitted_diff,
|
|
9
|
+
_get_untracked_diff,
|
|
10
|
+
_try_committed_diff,
|
|
11
|
+
_try_staged_diff,
|
|
12
|
+
_try_uncommitted_diff,
|
|
13
|
+
)
|
|
14
|
+
from rio_cli.output import render_findings
|
|
15
|
+
from rio_cli.post import _post_review
|
|
16
|
+
from rio_cli.utils import _fail
|
|
17
|
+
|
|
18
|
+
app = typer.Typer()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@app.callback()
|
|
22
|
+
def callback():
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
def _resolve_diff(
|
|
26
|
+
staged: bool,
|
|
27
|
+
uncommitted: bool,
|
|
28
|
+
committed: bool,
|
|
29
|
+
diff_file: str | None,
|
|
30
|
+
include_untracked: bool,
|
|
31
|
+
) -> str:
|
|
32
|
+
if include_untracked:
|
|
33
|
+
# soft: let empty scopes pass through, only fail if everything (incl. untracked) is empty
|
|
34
|
+
if diff_file:
|
|
35
|
+
base = _get_diff_from_file(diff_file)
|
|
36
|
+
elif uncommitted:
|
|
37
|
+
base = _try_uncommitted_diff()
|
|
38
|
+
elif committed:
|
|
39
|
+
base = _try_committed_diff()
|
|
40
|
+
elif staged:
|
|
41
|
+
base = _try_staged_diff()
|
|
42
|
+
else:
|
|
43
|
+
base = _try_committed_diff() + _try_uncommitted_diff()
|
|
44
|
+
|
|
45
|
+
untracked = _get_untracked_diff()
|
|
46
|
+
combined = base + untracked
|
|
47
|
+
if not combined.strip():
|
|
48
|
+
_fail("No changes found (including untracked files).")
|
|
49
|
+
return combined
|
|
50
|
+
|
|
51
|
+
# hard: existing behavior, each scope fails loudly if empty
|
|
52
|
+
if diff_file:
|
|
53
|
+
return _get_diff_from_file(diff_file)
|
|
54
|
+
elif uncommitted:
|
|
55
|
+
return _get_uncommitted_diff()
|
|
56
|
+
elif committed:
|
|
57
|
+
return _get_committed_diff()
|
|
58
|
+
elif staged:
|
|
59
|
+
return _get_staged_diff()
|
|
60
|
+
else:
|
|
61
|
+
return _get_combined_diff()
|
|
62
|
+
|
|
63
|
+
@app.command()
|
|
64
|
+
def review(
|
|
65
|
+
staged : bool = typer.Option(False , "--staged"),
|
|
66
|
+
uncommitted: bool = typer.Option(False, "--uncommitted"),
|
|
67
|
+
committed: bool = typer.Option(False, "--committed"),
|
|
68
|
+
diff_file : str | None = typer.Option(None , "--diff"),
|
|
69
|
+
include_untracked: bool = typer.Option(False, "--include-untracked"),
|
|
70
|
+
):
|
|
71
|
+
""" Print Findings for a local diff """
|
|
72
|
+
flags_set = sum([staged, uncommitted, committed, diff_file is not None])
|
|
73
|
+
if flags_set > 1:
|
|
74
|
+
_fail("Error: --staged, --uncommitted, --committed, and --diff cannot be used together.")
|
|
75
|
+
|
|
76
|
+
diff_text = _resolve_diff(staged, uncommitted,committed, diff_file, include_untracked)
|
|
77
|
+
|
|
78
|
+
result = _post_review(diff_text)
|
|
79
|
+
render_findings(result)
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__":
|
|
82
|
+
app()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.panel import Panel
|
|
5
|
+
from rio_core.models import Finding
|
|
6
|
+
|
|
7
|
+
console = Console()
|
|
8
|
+
|
|
9
|
+
SEVERITY_ORDER = ["critical", "warning", "info"]
|
|
10
|
+
SEVERITY_COLOR = {
|
|
11
|
+
"critical": "red",
|
|
12
|
+
"warning": "yellow",
|
|
13
|
+
"info": "cyan",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
def render_findings(findings : list[Finding]) -> None:
|
|
17
|
+
if not findings:
|
|
18
|
+
console.print("[green]No findings - looks clean.[/green]")
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
grouped : dict[str, list[Finding]] = defaultdict(list)
|
|
22
|
+
|
|
23
|
+
for f in findings:
|
|
24
|
+
grouped[f.severity].append(f)
|
|
25
|
+
|
|
26
|
+
for severity in SEVERITY_ORDER:
|
|
27
|
+
group = grouped.get(severity)
|
|
28
|
+
if not group:
|
|
29
|
+
continue
|
|
30
|
+
|
|
31
|
+
color = SEVERITY_COLOR[severity]
|
|
32
|
+
console.print(f"\n[bold {color}]{severity.upper()} ({len(group)})[/bold {color}]")
|
|
33
|
+
|
|
34
|
+
for f in group:
|
|
35
|
+
console.print(Panel(
|
|
36
|
+
f"{f.message}\n\n[dim]{f.rationale}[/dim]",
|
|
37
|
+
title=f"{f.file}:{f.line}",
|
|
38
|
+
border_style=color,
|
|
39
|
+
))
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
from rio_core.models import Finding
|
|
3
|
+
|
|
4
|
+
from rio_cli.config import get_ai_engine_url, get_api_key
|
|
5
|
+
from rio_cli.rio_config import load_rio_config
|
|
6
|
+
from rio_cli.utils import _fail
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _post_review(diff_text : str) -> list[Finding]:
|
|
10
|
+
headers : dict = {}
|
|
11
|
+
api_key = get_api_key()
|
|
12
|
+
if api_key:
|
|
13
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
14
|
+
|
|
15
|
+
rio_config = load_rio_config()
|
|
16
|
+
ai_engine_url = get_ai_engine_url()
|
|
17
|
+
try:
|
|
18
|
+
response = httpx.post(
|
|
19
|
+
f"{ai_engine_url}/v1/review",
|
|
20
|
+
json={"diff" : diff_text , "config" : rio_config.model_dump()},
|
|
21
|
+
headers=headers,
|
|
22
|
+
timeout=120.0
|
|
23
|
+
)
|
|
24
|
+
except httpx.ConnectError:
|
|
25
|
+
_fail(f"Error : could not connect to ai-engine at {ai_engine_url}.")
|
|
26
|
+
|
|
27
|
+
except httpx.TimeoutException:
|
|
28
|
+
_fail("Error: ai-engine request timed out.")
|
|
29
|
+
|
|
30
|
+
if response.status_code != 200:
|
|
31
|
+
_fail(f"Error: ai-engine returned {response.status_code}: {response.text}")
|
|
32
|
+
|
|
33
|
+
data = response.json()
|
|
34
|
+
return [Finding(**f) for f in data["findings"]]
|
|
35
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import yaml
|
|
5
|
+
from rio_core.config import RioConfig
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _get_repo_root() -> Path | None:
|
|
9
|
+
result = subprocess.run(
|
|
10
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
11
|
+
capture_output=True,
|
|
12
|
+
text=True,
|
|
13
|
+
check=False,
|
|
14
|
+
)
|
|
15
|
+
if result.returncode != 0:
|
|
16
|
+
return None
|
|
17
|
+
return Path(result.stdout.strip())
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def load_rio_config() -> RioConfig:
|
|
21
|
+
repo_root = _get_repo_root()
|
|
22
|
+
if repo_root is None:
|
|
23
|
+
return RioConfig()
|
|
24
|
+
|
|
25
|
+
config_path = repo_root / ".rio.yml"
|
|
26
|
+
if not config_path.exists():
|
|
27
|
+
return RioConfig()
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
data = yaml.safe_load(config_path.read_text())
|
|
31
|
+
except yaml.YAMLError:
|
|
32
|
+
return RioConfig()
|
|
33
|
+
|
|
34
|
+
return RioConfig(**(data or {}))
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from typing import NoReturn
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _fail(message : str) -> NoReturn:
|
|
8
|
+
typer.echo(message , err=True)
|
|
9
|
+
raise typer.Exit(code=1)
|
|
10
|
+
|
|
11
|
+
def _try_run_git_diff(args: list[str]) -> str:
|
|
12
|
+
"""Run git diff with the given args. Returns ""on any failure or empty result."""
|
|
13
|
+
try:
|
|
14
|
+
result = subprocess.run(
|
|
15
|
+
["git", "diff", *args],
|
|
16
|
+
capture_output=True,
|
|
17
|
+
text=True,
|
|
18
|
+
check=False,
|
|
19
|
+
)
|
|
20
|
+
except FileNotFoundError:
|
|
21
|
+
return ""
|
|
22
|
+
|
|
23
|
+
if result.returncode != 0:
|
|
24
|
+
return ""
|
|
25
|
+
|
|
26
|
+
return result.stdout
|
|
27
|
+
|
|
28
|
+
def _run_git_diff(args: list[str], empty_message: str) -> str:
|
|
29
|
+
"""Run git diff, hard-failing (via _fail) on any error or empty result."""
|
|
30
|
+
output = _try_run_git_diff(args)
|
|
31
|
+
if not output.strip():
|
|
32
|
+
_fail(empty_message)
|
|
33
|
+
return output
|