slopguard-cli 0.1.2__tar.gz → 0.2.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.
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/PKG-INFO +1 -1
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/pyproject.toml +1 -1
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/slopguard/__init__.py +1 -1
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/slopguard/update.py +28 -11
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/tests/test_cli.py +2 -2
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/tests/test_update.py +55 -3
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/.gitignore +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/.ruff.toml +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/Makefile +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/README.md +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/scripts/generate_seed_data.py +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/slopguard/__main__.py +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/slopguard/cli.py +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/slopguard/config.py +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/slopguard/data/__init__.py +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/slopguard/data/hallucinations_seed.json +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/slopguard/data/popular_packages.json +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/slopguard/models.py +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/slopguard/parsers/__init__.py +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/slopguard/parsers/base.py +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/slopguard/parsers/npm.py +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/slopguard/parsers/python.py +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/slopguard/registry/__init__.py +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/slopguard/registry/base.py +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/slopguard/registry/npm.py +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/slopguard/registry/pypi.py +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/slopguard/report/__init__.py +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/slopguard/report/json.py +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/slopguard/report/terminal.py +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/slopguard/saas/__init__.py +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/slopguard/saas/client.py +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/slopguard/saas/credentials.py +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/slopguard/scoring/__init__.py +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/slopguard/scoring/engine.py +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/slopguard/scoring/signals.py +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/tests/__init__.py +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/tests/conftest.py +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/tests/fixtures/.slopguard.yaml +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/tests/fixtures/package.json +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/tests/fixtures/pyproject.toml +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/tests/fixtures/requirements.txt +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/tests/test_misc.py +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/tests/test_parsers_npm.py +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/tests/test_parsers_python.py +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/tests/test_registry_npm.py +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/tests/test_registry_pypi.py +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/tests/test_saas_auth.py +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/tests/test_saas_client.py +0 -0
- {slopguard_cli-0.1.2 → slopguard_cli-0.2.0}/tests/test_scoring_engine.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: slopguard-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Defend developers and AI coding agents against slopsquatting (hallucinated package names).
|
|
5
5
|
Project-URL: Homepage, https://github.com/hariomunknownslab/slopguard
|
|
6
6
|
Project-URL: Repository, https://github.com/hariomunknownslab/slopguard
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "slopguard-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Defend developers and AI coding agents against slopsquatting (hallucinated package names)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -1,13 +1,21 @@
|
|
|
1
|
-
"""``slopguard update`` — refresh the local hallucination DB
|
|
1
|
+
"""``slopguard update`` — refresh the local hallucination DB.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
``~/.cache/slopguard/hallucinations_db.json``. The next ``slopguard
|
|
6
|
-
prefers this cached file over the bundled seed (see
|
|
3
|
+
Fetches the curated DB from the SlopGuard GitHub Pages site (no API,
|
|
4
|
+
no account, no auth) and writes a seed-shaped file to
|
|
5
|
+
``~/.cache/slopguard/hallucinations_db.json``. The next ``slopguard
|
|
6
|
+
scan`` prefers this cached file over the bundled seed (see
|
|
7
7
|
``slopguard.data.load_hallucination_db``).
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
Source of truth:
|
|
10
|
+
https://hariomunknownslab.github.io/slopguard/db.json
|
|
11
|
+
|
|
12
|
+
Updated daily by the probe-cron GitHub Action; each entry goes through
|
|
13
|
+
PR review before publication (see probe-data/README.md in the repo).
|
|
14
|
+
|
|
15
|
+
Override the URL with ``SLOPGUARD_DB_URL`` to fetch from a fork or
|
|
16
|
+
mirror. The legacy ``SLOPGUARD_API_URL`` env var still works for the
|
|
17
|
+
0.1.x SaaS endpoint shape — used as a fallback for backward
|
|
18
|
+
compatibility.
|
|
11
19
|
"""
|
|
12
20
|
|
|
13
21
|
from __future__ import annotations
|
|
@@ -20,12 +28,21 @@ from typing import Any
|
|
|
20
28
|
|
|
21
29
|
import httpx
|
|
22
30
|
|
|
23
|
-
|
|
31
|
+
DEFAULT_DB_URL = "https://hariomunknownslab.github.io/slopguard/db.json"
|
|
24
32
|
CACHE_PATH = Path.home() / ".cache" / "slopguard" / "hallucinations_db.json"
|
|
25
33
|
|
|
26
34
|
|
|
27
|
-
def
|
|
28
|
-
|
|
35
|
+
def _db_url() -> str:
|
|
36
|
+
"""Where to fetch the published DB.
|
|
37
|
+
|
|
38
|
+
Precedence: ``SLOPGUARD_DB_URL`` > legacy
|
|
39
|
+
``SLOPGUARD_API_URL/v1/hallucinations`` > default GitHub Pages.
|
|
40
|
+
"""
|
|
41
|
+
if explicit := os.environ.get("SLOPGUARD_DB_URL"):
|
|
42
|
+
return explicit
|
|
43
|
+
if legacy := os.environ.get("SLOPGUARD_API_URL"):
|
|
44
|
+
return legacy.rstrip("/") + "/v1/hallucinations"
|
|
45
|
+
return DEFAULT_DB_URL
|
|
29
46
|
|
|
30
47
|
|
|
31
48
|
def _convert(wire: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -53,7 +70,7 @@ def _convert(wire: dict[str, Any]) -> dict[str, Any]:
|
|
|
53
70
|
|
|
54
71
|
|
|
55
72
|
def run() -> int:
|
|
56
|
-
url =
|
|
73
|
+
url = _db_url()
|
|
57
74
|
try:
|
|
58
75
|
resp = httpx.get(url, timeout=15.0)
|
|
59
76
|
resp.raise_for_status()
|
|
@@ -107,7 +107,7 @@ def runner() -> CliRunner:
|
|
|
107
107
|
def test_version_subcommand(runner: CliRunner) -> None:
|
|
108
108
|
result = runner.invoke(app, ["version"])
|
|
109
109
|
assert result.exit_code == 0
|
|
110
|
-
assert result.stdout.strip() == "0.
|
|
110
|
+
assert result.stdout.strip() == "0.2.0"
|
|
111
111
|
|
|
112
112
|
|
|
113
113
|
def test_scan_fixtures_no_network_terminal(runner: CliRunner, fixtures_dir: Path) -> None:
|
|
@@ -183,4 +183,4 @@ def test_module_main_runs() -> None:
|
|
|
183
183
|
check=False,
|
|
184
184
|
)
|
|
185
185
|
assert result.returncode == 0
|
|
186
|
-
assert result.stdout.strip() == "0.
|
|
186
|
+
assert result.stdout.strip() == "0.2.0"
|
|
@@ -67,9 +67,7 @@ def test_update_writes_cache(
|
|
|
67
67
|
def test_update_handles_network_failure(
|
|
68
68
|
cache_at_tmp: Path, api_at_test: None, capsys: pytest.CaptureFixture[str]
|
|
69
69
|
) -> None:
|
|
70
|
-
respx.get("http://api.test/v1/hallucinations").mock(
|
|
71
|
-
side_effect=httpx.ConnectError("nope")
|
|
72
|
-
)
|
|
70
|
+
respx.get("http://api.test/v1/hallucinations").mock(side_effect=httpx.ConnectError("nope"))
|
|
73
71
|
code = update.run()
|
|
74
72
|
assert code == 1
|
|
75
73
|
assert not cache_at_tmp.exists()
|
|
@@ -87,3 +85,57 @@ def test_update_rejects_malformed_payload(
|
|
|
87
85
|
assert code == 1
|
|
88
86
|
assert not cache_at_tmp.exists()
|
|
89
87
|
assert "invalid response" in capsys.readouterr().out
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_default_db_url_is_github_pages(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
91
|
+
"""When neither override env var is set, fetch from GitHub Pages."""
|
|
92
|
+
monkeypatch.delenv("SLOPGUARD_DB_URL", raising=False)
|
|
93
|
+
monkeypatch.delenv("SLOPGUARD_API_URL", raising=False)
|
|
94
|
+
assert update._db_url() == update.DEFAULT_DB_URL
|
|
95
|
+
assert update.DEFAULT_DB_URL.startswith("https://hariomunknownslab.github.io/")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_slopguard_db_url_takes_precedence(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
99
|
+
monkeypatch.setenv("SLOPGUARD_DB_URL", "https://my.mirror/db.json")
|
|
100
|
+
monkeypatch.setenv("SLOPGUARD_API_URL", "https://legacy/api") # should be ignored
|
|
101
|
+
assert update._db_url() == "https://my.mirror/db.json"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_legacy_api_url_still_works(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
105
|
+
"""0.1.x users with SLOPGUARD_API_URL set keep working — we append
|
|
106
|
+
/v1/hallucinations the way the SaaS endpoint expected."""
|
|
107
|
+
monkeypatch.delenv("SLOPGUARD_DB_URL", raising=False)
|
|
108
|
+
monkeypatch.setenv("SLOPGUARD_API_URL", "https://legacy.example/")
|
|
109
|
+
assert update._db_url() == "https://legacy.example/v1/hallucinations"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@respx.mock
|
|
113
|
+
def test_update_fetches_from_pages_by_default(
|
|
114
|
+
cache_at_tmp: Path, monkeypatch: pytest.MonkeyPatch
|
|
115
|
+
) -> None:
|
|
116
|
+
"""End-to-end: no env overrides, hits the Pages URL."""
|
|
117
|
+
monkeypatch.delenv("SLOPGUARD_DB_URL", raising=False)
|
|
118
|
+
monkeypatch.delenv("SLOPGUARD_API_URL", raising=False)
|
|
119
|
+
respx.get(update.DEFAULT_DB_URL).mock(
|
|
120
|
+
return_value=httpx.Response(
|
|
121
|
+
200,
|
|
122
|
+
json={
|
|
123
|
+
"generated_at": "2026-05-24T03:17:00Z",
|
|
124
|
+
"count": 1,
|
|
125
|
+
"entries": [
|
|
126
|
+
{
|
|
127
|
+
"package_name": "react-secure-auth-helper",
|
|
128
|
+
"ecosystem": "npm",
|
|
129
|
+
"total_observations": 7,
|
|
130
|
+
"recurrence_rate": 0.18,
|
|
131
|
+
"first_observed_at": "2026-05-21T00:00:00Z",
|
|
132
|
+
"last_observed_at": "2026-05-23T00:00:00Z",
|
|
133
|
+
"published_at": "2026-05-24T03:17:00Z",
|
|
134
|
+
}
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
assert update.run() == 0
|
|
140
|
+
cached = json.loads(cache_at_tmp.read_text())
|
|
141
|
+
assert cached["entries"][0]["name"] == "react-secure-auth-helper"
|
|
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
|
|
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
|