core-cli 1.0.2__tar.gz → 2.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.
- {core_cli-1.0.2 → core_cli-2.0.0}/.github/workflows/ci.yml +9 -5
- core_cli-2.0.0/PKG-INFO +107 -0
- core_cli-2.0.0/README.md +78 -0
- {core_cli-1.0.2 → core_cli-2.0.0}/pyproject.toml +22 -3
- core_cli-2.0.0/src/core_cli/client.py +319 -0
- core_cli-2.0.0/src/core_cli/command.py +77 -0
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/main.py +0 -2
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/code/__init__.py +0 -1
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/code/actions.py +3 -17
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/code/audit_duplicates.py +3 -3
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/code/bridges.py +4 -6
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/code/check_imports.py +3 -3
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/code/docstrings.py +3 -3
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/code/fix_atomic.py +3 -17
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/code/format.py +4 -4
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/code/integrity.py +2 -2
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/code/lint.py +3 -3
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/code/logging.py +3 -17
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/code/test.py +3 -3
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/lane/claim.py +4 -4
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/lane/list.py +4 -4
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/lane/next.py +6 -6
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/lane/propose.py +5 -6
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/project/__init__.py +2 -3
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/project/onboard.py +4 -4
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/project/scout.py +3 -3
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/proposals/__init__.py +2 -1
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/proposals/create.py +3 -3
- core_cli-2.0.0/src/core_cli/resources/proposals/integrate.py +37 -0
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/proposals/list.py +3 -3
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/proposals/manage.py +6 -6
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/symbols/__init__.py +0 -1
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/symbols/audit.py +10 -6
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/symbols/fix_ids.py +12 -6
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/symbols/resolve_duplicates.py +14 -6
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/vectors/__init__.py +1 -7
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/vectors/hub.py +1 -1
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/vectors/query.py +5 -5
- core_cli-2.0.0/tests/test_client.py +54 -0
- {core_cli-1.0.2 → core_cli-2.0.0}/tests/test_code.py +0 -47
- core_cli-2.0.0/tests/test_contract.py +124 -0
- core_cli-1.0.2/PKG-INFO +0 -72
- core_cli-1.0.2/README.md +0 -46
- core_cli-1.0.2/src/core_cli/resources/code/check_ui.py +0 -61
- core_cli-1.0.2/src/core_cli/resources/project/docs.py +0 -41
- core_cli-1.0.2/src/core_cli/resources/proposals/integrate.py +0 -57
- core_cli-1.0.2/src/core_cli/resources/secrets/__init__.py +0 -10
- core_cli-1.0.2/src/core_cli/resources/secrets/hub.py +0 -9
- core_cli-1.0.2/src/core_cli/resources/secrets/manage.py +0 -160
- core_cli-1.0.2/src/core_cli/resources/symbols/sync.py +0 -39
- core_cli-1.0.2/src/core_cli/resources/vectors/rebuild.py +0 -71
- core_cli-1.0.2/src/core_cli/resources/vectors/status.py +0 -40
- core_cli-1.0.2/src/core_cli/resources/vectors/sync.py +0 -50
- core_cli-1.0.2/src/core_cli/resources/vectors/sync_code.py +0 -58
- {core_cli-1.0.2 → core_cli-2.0.0}/.gitignore +0 -0
- {core_cli-1.0.2 → core_cli-2.0.0}/LICENSE +0 -0
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/__init__.py +0 -0
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/__init__.py +0 -0
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/code/hub.py +0 -0
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/lane/__init__.py +0 -0
- {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/symbols/hub.py +0 -0
- {core_cli-1.0.2 → core_cli-2.0.0}/tests/__init__.py +0 -0
- {core_cli-1.0.2 → core_cli-2.0.0}/tests/test_lane.py +0 -0
- {core_cli-1.0.2 → core_cli-2.0.0}/tests/test_project.py +0 -0
- {core_cli-1.0.2 → core_cli-2.0.0}/tests/test_proposals.py +0 -0
|
@@ -19,7 +19,9 @@ jobs:
|
|
|
19
19
|
python-version: "3.12"
|
|
20
20
|
|
|
21
21
|
- name: Install ruff
|
|
22
|
-
|
|
22
|
+
# Pinned: an unpinned ruff picked up new default rules and turned lint
|
|
23
|
+
# red on unchanged code (2026-10). Bump deliberately.
|
|
24
|
+
run: pip install ruff==0.16.10
|
|
23
25
|
|
|
24
26
|
- name: ruff check
|
|
25
27
|
run: ruff check src/
|
|
@@ -37,10 +39,11 @@ jobs:
|
|
|
37
39
|
- name: Install mypy + stubs
|
|
38
40
|
run: pip install mypy types-PyYAML
|
|
39
41
|
|
|
42
|
+
- name: Install package
|
|
43
|
+
run: pip install -e .
|
|
44
|
+
|
|
40
45
|
- name: mypy
|
|
41
|
-
|
|
42
|
-
# checking of core-cli's own code without requiring stub packages.
|
|
43
|
-
run: mypy src/ --ignore-missing-imports --no-error-summary
|
|
46
|
+
run: mypy src/ --no-error-summary
|
|
44
47
|
|
|
45
48
|
test:
|
|
46
49
|
name: Test
|
|
@@ -53,10 +56,11 @@ jobs:
|
|
|
53
56
|
python-version: "3.12"
|
|
54
57
|
|
|
55
58
|
- name: Install package + dev deps
|
|
56
|
-
# core-runtime is on PyPI (>=2.8.0); installs as a transitive dep.
|
|
57
59
|
run: pip install -e ".[dev]"
|
|
58
60
|
|
|
59
61
|
- name: Run tests
|
|
62
|
+
# tests/test_contract.py reads CORE's OpenAPI contract from CORE's main
|
|
63
|
+
# branch on GitHub (no CORE checkout here).
|
|
60
64
|
run: pytest tests/ -v
|
|
61
65
|
|
|
62
66
|
build:
|
core_cli-2.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: core-cli
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Consumer governance CLI for CORE — an HTTP client for governing repositories against CORE constitutional rules.
|
|
5
|
+
Project-URL: Homepage, https://github.com/DariuszNewecki/CORE
|
|
6
|
+
Project-URL: Repository, https://github.com/DariuszNewecki/core-cli
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/DariuszNewecki/core-cli/issues
|
|
8
|
+
Author-email: Dariusz Newecki <d.newecki@gmail.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: cli,code-quality,constitutional-ai,developer-tools,governance
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development
|
|
19
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Requires-Dist: httpx>=0.27
|
|
22
|
+
Requires-Dist: pyyaml>=6.0
|
|
23
|
+
Requires-Dist: rich>=13.7
|
|
24
|
+
Requires-Dist: typer>=0.16
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# core-cli
|
|
31
|
+
|
|
32
|
+
The consumer command-line tool for [CORE](https://github.com/DariuszNewecki/CORE): review
|
|
33
|
+
and approve proposals, run checks and fixes on the repository CORE governs, onboard a
|
|
34
|
+
repository (BYOR), and work the assisted remediation lane. Its binary is called `core`.
|
|
35
|
+
|
|
36
|
+
`core-cli` is an HTTP client and nothing more. Every command talks to a **running CORE
|
|
37
|
+
API**, using only the routes in CORE's published
|
|
38
|
+
[OpenAPI contract](https://github.com/DariuszNewecki/CORE/blob/main/docs/reference/openapi.json);
|
|
39
|
+
it does not install or import CORE. The operator tool, `core-admin`, ships separately in
|
|
40
|
+
`core-runtime` and covers the CORE installation itself: secrets, vector store, database
|
|
41
|
+
sync ([which one do I need?](https://dariusznewecki.github.io/CORE/cli-reference/)).
|
|
42
|
+
|
|
43
|
+
## Prerequisites
|
|
44
|
+
|
|
45
|
+
- Python 3.12+
|
|
46
|
+
- A reachable CORE API, which itself needs PostgreSQL and Qdrant. See CORE's
|
|
47
|
+
[getting started](https://dariusznewecki.github.io/CORE/getting-started/).
|
|
48
|
+
|
|
49
|
+
> **No authentication.** OSS CORE runs in trusted-localhost mode: the API binds to
|
|
50
|
+
> loopback and accepts every request. Anyone who can reach it can approve and execute
|
|
51
|
+
> proposals. Keep it on `127.0.0.1` and do not expose it on a shared network.
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install core-cli
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Its only dependencies are `typer`, `rich`, `httpx` and `PyYAML`.
|
|
60
|
+
|
|
61
|
+
## Configure
|
|
62
|
+
|
|
63
|
+
`core` uses `http://127.0.0.1:8000` unless you point it elsewhere:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
export CORE_API_URL=http://127.0.0.1:8000
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Use
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
core proposals list # proposals awaiting a decision
|
|
73
|
+
core proposals show <id> # risk assessment and planned changes
|
|
74
|
+
core lane list # delegated findings waiting for assisted remediation
|
|
75
|
+
core project onboard <path> # preview delivering the machinery floor (BYOR)
|
|
76
|
+
core project onboard <path> --write # deliver it
|
|
77
|
+
core <group> <command> --help # options for any command
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Commands that change files or data **preview by default** and act only with `--write`.
|
|
81
|
+
|
|
82
|
+
## Commands
|
|
83
|
+
|
|
84
|
+
| Group | What it covers |
|
|
85
|
+
|---|---|
|
|
86
|
+
| `proposals` | `list`, `show`, `create`, `approve`, `reject`, `execute`, `integrate` |
|
|
87
|
+
| `lane` | `list`, `next`, `claim`, `propose`: assisted remediation of delegated findings |
|
|
88
|
+
| `project` | `onboard`, `scout`, `promote`: bring a repository under governance (BYOR) |
|
|
89
|
+
| `code` | quality and verification: `lint`, `format`, `test`, `check-imports`, `audit-duplicates`, … |
|
|
90
|
+
| `symbols` | `audit`, `fix-ids`, `resolve-duplicates` |
|
|
91
|
+
| `vectors` | `query`: semantic search over the governed repository |
|
|
92
|
+
|
|
93
|
+
Every command, option and default is in the generated
|
|
94
|
+
[`core` command reference](https://dariusznewecki.github.io/CORE/reference/core/).
|
|
95
|
+
|
|
96
|
+
## Architecture
|
|
97
|
+
|
|
98
|
+
The split between the consumer CLI (`core`) and the operator CLI (`core-admin`) is
|
|
99
|
+
recorded in
|
|
100
|
+
[ADR-146](https://github.com/DariuszNewecki/CORE/blob/main/.specs/decisions/ADR-146-cli-consumer-operator-split.md):
|
|
101
|
+
`core` covers operations on the repository CORE governs; `core-admin` covers the CORE
|
|
102
|
+
installation. `tests/test_contract.py` checks every route this client calls against CORE's
|
|
103
|
+
OpenAPI contract.
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
MIT. See [LICENSE](LICENSE).
|
core_cli-2.0.0/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# core-cli
|
|
2
|
+
|
|
3
|
+
The consumer command-line tool for [CORE](https://github.com/DariuszNewecki/CORE): review
|
|
4
|
+
and approve proposals, run checks and fixes on the repository CORE governs, onboard a
|
|
5
|
+
repository (BYOR), and work the assisted remediation lane. Its binary is called `core`.
|
|
6
|
+
|
|
7
|
+
`core-cli` is an HTTP client and nothing more. Every command talks to a **running CORE
|
|
8
|
+
API**, using only the routes in CORE's published
|
|
9
|
+
[OpenAPI contract](https://github.com/DariuszNewecki/CORE/blob/main/docs/reference/openapi.json);
|
|
10
|
+
it does not install or import CORE. The operator tool, `core-admin`, ships separately in
|
|
11
|
+
`core-runtime` and covers the CORE installation itself: secrets, vector store, database
|
|
12
|
+
sync ([which one do I need?](https://dariusznewecki.github.io/CORE/cli-reference/)).
|
|
13
|
+
|
|
14
|
+
## Prerequisites
|
|
15
|
+
|
|
16
|
+
- Python 3.12+
|
|
17
|
+
- A reachable CORE API, which itself needs PostgreSQL and Qdrant. See CORE's
|
|
18
|
+
[getting started](https://dariusznewecki.github.io/CORE/getting-started/).
|
|
19
|
+
|
|
20
|
+
> **No authentication.** OSS CORE runs in trusted-localhost mode: the API binds to
|
|
21
|
+
> loopback and accepts every request. Anyone who can reach it can approve and execute
|
|
22
|
+
> proposals. Keep it on `127.0.0.1` and do not expose it on a shared network.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install core-cli
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Its only dependencies are `typer`, `rich`, `httpx` and `PyYAML`.
|
|
31
|
+
|
|
32
|
+
## Configure
|
|
33
|
+
|
|
34
|
+
`core` uses `http://127.0.0.1:8000` unless you point it elsewhere:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
export CORE_API_URL=http://127.0.0.1:8000
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Use
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
core proposals list # proposals awaiting a decision
|
|
44
|
+
core proposals show <id> # risk assessment and planned changes
|
|
45
|
+
core lane list # delegated findings waiting for assisted remediation
|
|
46
|
+
core project onboard <path> # preview delivering the machinery floor (BYOR)
|
|
47
|
+
core project onboard <path> --write # deliver it
|
|
48
|
+
core <group> <command> --help # options for any command
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Commands that change files or data **preview by default** and act only with `--write`.
|
|
52
|
+
|
|
53
|
+
## Commands
|
|
54
|
+
|
|
55
|
+
| Group | What it covers |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `proposals` | `list`, `show`, `create`, `approve`, `reject`, `execute`, `integrate` |
|
|
58
|
+
| `lane` | `list`, `next`, `claim`, `propose`: assisted remediation of delegated findings |
|
|
59
|
+
| `project` | `onboard`, `scout`, `promote`: bring a repository under governance (BYOR) |
|
|
60
|
+
| `code` | quality and verification: `lint`, `format`, `test`, `check-imports`, `audit-duplicates`, … |
|
|
61
|
+
| `symbols` | `audit`, `fix-ids`, `resolve-duplicates` |
|
|
62
|
+
| `vectors` | `query`: semantic search over the governed repository |
|
|
63
|
+
|
|
64
|
+
Every command, option and default is in the generated
|
|
65
|
+
[`core` command reference](https://dariusznewecki.github.io/CORE/reference/core/).
|
|
66
|
+
|
|
67
|
+
## Architecture
|
|
68
|
+
|
|
69
|
+
The split between the consumer CLI (`core`) and the operator CLI (`core-admin`) is
|
|
70
|
+
recorded in
|
|
71
|
+
[ADR-146](https://github.com/DariuszNewecki/CORE/blob/main/.specs/decisions/ADR-146-cli-consumer-operator-split.md):
|
|
72
|
+
`core` covers operations on the repository CORE governs; `core-admin` covers the CORE
|
|
73
|
+
installation. `tests/test_contract.py` checks every route this client calls against CORE's
|
|
74
|
+
OpenAPI contract.
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "core-cli"
|
|
7
|
-
version = "
|
|
8
|
-
description = "Consumer governance CLI for CORE —
|
|
7
|
+
version = "2.0.0"
|
|
8
|
+
description = "Consumer governance CLI for CORE — an HTTP client for governing repositories against CORE constitutional rules."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
11
11
|
requires-python = ">=3.12"
|
|
@@ -23,8 +23,12 @@ classifiers = [
|
|
|
23
23
|
"Intended Audience :: Developers",
|
|
24
24
|
"Development Status :: 4 - Beta",
|
|
25
25
|
]
|
|
26
|
+
# HTTP client only: no dependency on core-runtime (ADR-146, amended 2026-10-03).
|
|
26
27
|
dependencies = [
|
|
27
|
-
"
|
|
28
|
+
"httpx>=0.27",
|
|
29
|
+
"pyyaml>=6.0",
|
|
30
|
+
"rich>=13.7",
|
|
31
|
+
"typer>=0.16",
|
|
28
32
|
]
|
|
29
33
|
|
|
30
34
|
[project.optional-dependencies]
|
|
@@ -48,3 +52,18 @@ packages = ["src/core_cli"]
|
|
|
48
52
|
asyncio_mode = "auto"
|
|
49
53
|
pythonpath = ["src"]
|
|
50
54
|
testpaths = ["tests"]
|
|
55
|
+
|
|
56
|
+
[tool.ruff]
|
|
57
|
+
line-length = 88
|
|
58
|
+
target-version = "py312"
|
|
59
|
+
src = ["src"]
|
|
60
|
+
|
|
61
|
+
[tool.ruff.lint]
|
|
62
|
+
# B008: typer.Option / typer.Argument as parameter defaults is Typer's
|
|
63
|
+
# documented idiom, not a bug (CORE ignores it for the same reason).
|
|
64
|
+
ignore = ["B008"]
|
|
65
|
+
|
|
66
|
+
[tool.ruff.lint.isort]
|
|
67
|
+
# Two blank lines after imports, matching CORE's layout.
|
|
68
|
+
known-first-party = ["core_cli"]
|
|
69
|
+
lines-after-imports = 2
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"""HTTP client for the CORE API — core-cli's only link to CORE.
|
|
2
|
+
|
|
3
|
+
core-cli talks to a running CORE over HTTP and nothing else: it has no
|
|
4
|
+
dependency on core-runtime (ADR-146, amended 2026-10-03). Every route called
|
|
5
|
+
here is in CORE's published OpenAPI contract, ``docs/reference/openapi.json``
|
|
6
|
+
in the CORE repository; ``tests/test_contract.py`` checks that.
|
|
7
|
+
|
|
8
|
+
The base URL comes from the ``CORE_API_URL`` environment variable, defaulting
|
|
9
|
+
to the loopback address CORE binds to.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import os
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import httpx
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
DEFAULT_BASE_URL = "http://127.0.0.1:8000"
|
|
22
|
+
_TIMEOUT_SECONDS = 30.0
|
|
23
|
+
_LONG_TIMEOUT_SECONDS = 300.0
|
|
24
|
+
_POLL_INTERVAL_SECONDS = 1.0
|
|
25
|
+
_POLL_TERMINAL_STATES = frozenset({"completed", "failed"})
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ID: e5aeda69-d26e-4353-81eb-83e998e968e9
|
|
29
|
+
class CoreApiError(httpx.HTTPStatusError):
|
|
30
|
+
"""An error response (status >= 400) from the CORE API.
|
|
31
|
+
|
|
32
|
+
An ``httpx.HTTPStatusError``, so commands can branch on
|
|
33
|
+
``exc.response.status_code``; ``status_code`` and ``detail`` (the API's
|
|
34
|
+
``detail`` field, or the raw body) are also set directly.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, response: httpx.Response) -> None:
|
|
38
|
+
try:
|
|
39
|
+
detail = response.json().get("detail", response.text)
|
|
40
|
+
except ValueError:
|
|
41
|
+
detail = response.text
|
|
42
|
+
self.status_code = response.status_code
|
|
43
|
+
self.detail = detail
|
|
44
|
+
super().__init__(
|
|
45
|
+
f"API error {response.status_code}: {detail}",
|
|
46
|
+
request=response.request,
|
|
47
|
+
response=response,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ID: 49ef4530-5b49-4ebb-a9f6-b59150780e1b
|
|
52
|
+
class CoreApiClient:
|
|
53
|
+
"""Async client for the CORE API.
|
|
54
|
+
|
|
55
|
+
Methods mirror the API: flat methods for single routes, plus small
|
|
56
|
+
namespaces (``inspect``, ``lane``, ``project``, ``symbols``, ``vectors``)
|
|
57
|
+
for route families. A response with status >= 400 raises ``CoreApiError``.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, base_url: str | None = None) -> None:
|
|
61
|
+
self.base_url = base_url or os.environ.get("CORE_API_URL") or DEFAULT_BASE_URL
|
|
62
|
+
self.inspect = _Inspect(self)
|
|
63
|
+
self.lane = _Lane(self)
|
|
64
|
+
self.project = _Project(self)
|
|
65
|
+
self.symbols = _Symbols(self)
|
|
66
|
+
self.vectors = _Vectors(self)
|
|
67
|
+
|
|
68
|
+
async def _request(
|
|
69
|
+
self,
|
|
70
|
+
method: str,
|
|
71
|
+
path: str,
|
|
72
|
+
*,
|
|
73
|
+
timeout: float = _TIMEOUT_SECONDS,
|
|
74
|
+
**kwargs: Any,
|
|
75
|
+
) -> dict:
|
|
76
|
+
async with httpx.AsyncClient(timeout=timeout) as http:
|
|
77
|
+
response = await http.request(method, f"{self.base_url}{path}", **kwargs)
|
|
78
|
+
if response.status_code >= 400:
|
|
79
|
+
raise CoreApiError(response)
|
|
80
|
+
return response.json()
|
|
81
|
+
|
|
82
|
+
async def _poll_run(
|
|
83
|
+
self, run_id: str, timeout_seconds: float = _LONG_TIMEOUT_SECONDS
|
|
84
|
+
) -> dict:
|
|
85
|
+
"""Poll GET /v1/fix/runs/{run_id} until the run completes or fails."""
|
|
86
|
+
async with asyncio.timeout(timeout_seconds):
|
|
87
|
+
while True:
|
|
88
|
+
payload = await self._request("GET", f"/v1/fix/runs/{run_id}")
|
|
89
|
+
if payload.get("status") in _POLL_TERMINAL_STATES:
|
|
90
|
+
return payload
|
|
91
|
+
await asyncio.sleep(_POLL_INTERVAL_SECONDS)
|
|
92
|
+
|
|
93
|
+
# -- actions and fixes ----------------------------------------------
|
|
94
|
+
|
|
95
|
+
# ID: d5e64d0c-4a5f-4518-96d2-d15388306f00
|
|
96
|
+
async def list_actions(self) -> dict:
|
|
97
|
+
"""GET /v1/actions — list the registered atomic actions."""
|
|
98
|
+
return await self._request("GET", "/v1/actions")
|
|
99
|
+
|
|
100
|
+
# ID: 7a49a17d-66a0-413b-b76a-5044c399f109
|
|
101
|
+
async def run_fix(
|
|
102
|
+
self,
|
|
103
|
+
fix_id: str,
|
|
104
|
+
target_files: list[str] | None = None,
|
|
105
|
+
write: bool = False,
|
|
106
|
+
params: dict[str, Any] | None = None,
|
|
107
|
+
) -> dict:
|
|
108
|
+
"""POST /v1/fix/run/{fix_id} — dispatch a fix action; poll with _poll_run."""
|
|
109
|
+
return await self._request(
|
|
110
|
+
"POST",
|
|
111
|
+
f"/v1/fix/run/{fix_id}",
|
|
112
|
+
json={
|
|
113
|
+
"target_files": target_files or [],
|
|
114
|
+
"write": write,
|
|
115
|
+
"params": params or {},
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# -- checks on the governed repository ------------------------------
|
|
120
|
+
|
|
121
|
+
# ID: 80078450-7b0e-4a2e-b62c-b265d179aa74
|
|
122
|
+
async def lint(self) -> dict:
|
|
123
|
+
"""POST /v1/lint — black --check and ruff check."""
|
|
124
|
+
return await self._request("POST", "/v1/lint", timeout=_LONG_TIMEOUT_SECONDS)
|
|
125
|
+
|
|
126
|
+
# ID: e07d0d0b-c466-4613-b63b-0df395730ba6
|
|
127
|
+
async def quality_imports(self) -> dict:
|
|
128
|
+
"""POST /v1/quality/imports — import-resolution check."""
|
|
129
|
+
return await self._request("POST", "/v1/quality/imports")
|
|
130
|
+
|
|
131
|
+
# ID: 58ba647c-a7ce-4d58-966b-c21f55e7d3fe
|
|
132
|
+
async def quality_tests(self, path: str | None = None) -> dict:
|
|
133
|
+
"""POST /v1/quality/tests — pytest, optionally scoped; poll with _poll_run."""
|
|
134
|
+
return await self._request("POST", "/v1/quality/tests", json={"path": path})
|
|
135
|
+
|
|
136
|
+
# ID: 2887d4f0-91a0-4db7-8df9-d33857113212
|
|
137
|
+
async def baseline(self, label: str = "default") -> dict:
|
|
138
|
+
"""POST /v1/integrity/baseline — fingerprint src/."""
|
|
139
|
+
return await self._request(
|
|
140
|
+
"POST", "/v1/integrity/baseline", json={"label": label}
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# ID: 7c860bc0-ec98-4a6f-9589-03c96bbe81b9
|
|
144
|
+
async def verify(self, label: str = "default") -> dict:
|
|
145
|
+
"""POST /v1/integrity/verify — compare src/ against a baseline."""
|
|
146
|
+
return await self._request(
|
|
147
|
+
"POST", "/v1/integrity/verify", json={"label": label}
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# ID: 2798cdc1-6743-4451-9c96-e6ecc2cb1d4e
|
|
151
|
+
async def integrate(self, commit_message: str) -> dict:
|
|
152
|
+
"""POST /v1/integrate — stage, format/lint and commit the working tree."""
|
|
153
|
+
return await self._request(
|
|
154
|
+
"POST",
|
|
155
|
+
"/v1/integrate",
|
|
156
|
+
json={"commit_message": commit_message},
|
|
157
|
+
timeout=_LONG_TIMEOUT_SECONDS,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# -- proposals --------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
# ID: 8bafbb59-8d23-45a5-8fc5-271b694bacbc
|
|
163
|
+
async def list_proposals(self, status: str | None = None, limit: int = 50) -> dict:
|
|
164
|
+
"""GET /v1/proposals."""
|
|
165
|
+
params: dict[str, Any] = {"limit": limit}
|
|
166
|
+
if status is not None:
|
|
167
|
+
params["status"] = status
|
|
168
|
+
return await self._request("GET", "/v1/proposals", params=params)
|
|
169
|
+
|
|
170
|
+
# ID: 957be7d4-98b7-4e47-99d8-115921b3b12f
|
|
171
|
+
async def create_proposal(
|
|
172
|
+
self,
|
|
173
|
+
goal: str,
|
|
174
|
+
actions: list[dict] | None = None,
|
|
175
|
+
files: list[str] | None = None,
|
|
176
|
+
created_by: str = "cli_operator",
|
|
177
|
+
write: bool = True,
|
|
178
|
+
) -> dict:
|
|
179
|
+
"""POST /v1/proposals."""
|
|
180
|
+
return await self._request(
|
|
181
|
+
"POST",
|
|
182
|
+
"/v1/proposals",
|
|
183
|
+
json={
|
|
184
|
+
"goal": goal,
|
|
185
|
+
"actions": actions or [],
|
|
186
|
+
"files": files or [],
|
|
187
|
+
"created_by": created_by,
|
|
188
|
+
"write": write,
|
|
189
|
+
},
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# ID: 4c806d5c-fe6e-4ac9-a0cc-dae70a01af93
|
|
193
|
+
async def get_proposal(self, proposal_id: str) -> dict:
|
|
194
|
+
"""GET /v1/proposals/{proposal_id}."""
|
|
195
|
+
return await self._request("GET", f"/v1/proposals/{proposal_id}")
|
|
196
|
+
|
|
197
|
+
# ID: b01d56fa-f71a-472d-aa2d-c68e420689f0
|
|
198
|
+
async def approve_proposal(
|
|
199
|
+
self, proposal_id: str, approved_by: str, approval_authority: str
|
|
200
|
+
) -> dict:
|
|
201
|
+
"""POST /v1/proposals/{proposal_id}/approve."""
|
|
202
|
+
return await self._request(
|
|
203
|
+
"POST",
|
|
204
|
+
f"/v1/proposals/{proposal_id}/approve",
|
|
205
|
+
json={"approved_by": approved_by, "approval_authority": approval_authority},
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# ID: 0e5c0e42-34bd-44c0-848f-62a1c0f78037
|
|
209
|
+
async def reject_proposal(self, proposal_id: str, reason: str) -> dict:
|
|
210
|
+
"""POST /v1/proposals/{proposal_id}/reject."""
|
|
211
|
+
return await self._request(
|
|
212
|
+
"POST", f"/v1/proposals/{proposal_id}/reject", json={"reason": reason}
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# ID: bef4a752-8d94-4d20-89e3-fc1ba79899d9
|
|
216
|
+
async def execute_proposal(self, proposal_id: str, write: bool = False) -> dict:
|
|
217
|
+
"""POST /v1/proposals/{proposal_id}/execute."""
|
|
218
|
+
return await self._request(
|
|
219
|
+
"POST", f"/v1/proposals/{proposal_id}/execute", json={"write": write}
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class _Namespace:
|
|
224
|
+
def __init__(self, client: CoreApiClient) -> None:
|
|
225
|
+
self._client = client
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class _Inspect(_Namespace):
|
|
229
|
+
async def analysis_duplicates(self, threshold: float = 0.85) -> dict:
|
|
230
|
+
"""GET /v1/analysis/duplicates."""
|
|
231
|
+
return await self._client._request(
|
|
232
|
+
"GET",
|
|
233
|
+
"/v1/analysis/duplicates",
|
|
234
|
+
params={"threshold": threshold},
|
|
235
|
+
timeout=_LONG_TIMEOUT_SECONDS,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
async def analysis_bridges(self, consuming: str | None = None) -> dict:
|
|
239
|
+
"""GET /v1/analysis/bridges."""
|
|
240
|
+
params: dict[str, Any] = {}
|
|
241
|
+
if consuming is not None:
|
|
242
|
+
params["consuming"] = consuming
|
|
243
|
+
return await self._client._request("GET", "/v1/analysis/bridges", params=params)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class _Lane(_Namespace):
|
|
247
|
+
async def list_delegated(self, limit: int = 50) -> dict:
|
|
248
|
+
"""GET /v1/lane."""
|
|
249
|
+
return await self._client._request("GET", "/v1/lane", params={"limit": limit})
|
|
250
|
+
|
|
251
|
+
async def next_delegated(self) -> dict:
|
|
252
|
+
"""GET /v1/lane/next."""
|
|
253
|
+
return await self._client._request("GET", "/v1/lane/next")
|
|
254
|
+
|
|
255
|
+
async def get_delegated(self, finding_id: str) -> dict:
|
|
256
|
+
"""GET /v1/lane/{finding_id}."""
|
|
257
|
+
return await self._client._request("GET", f"/v1/lane/{finding_id}")
|
|
258
|
+
|
|
259
|
+
async def claim(self, finding_id: str, agent: str) -> dict:
|
|
260
|
+
"""POST /v1/lane/{finding_id}/claim."""
|
|
261
|
+
return await self._client._request(
|
|
262
|
+
"POST", f"/v1/lane/{finding_id}/claim", params={"agent": agent}
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
async def propose(
|
|
266
|
+
self, finding_id: str, patch: str, validation_run_id: str
|
|
267
|
+
) -> dict:
|
|
268
|
+
"""POST /v1/lane/{finding_id}/propose."""
|
|
269
|
+
return await self._client._request(
|
|
270
|
+
"POST",
|
|
271
|
+
f"/v1/lane/{finding_id}/propose",
|
|
272
|
+
json={"patch": patch, "validation_run_id": validation_run_id},
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class _Project(_Namespace):
|
|
277
|
+
async def scout(self, path: str, reset: bool = False) -> dict:
|
|
278
|
+
"""POST /v1/project/scout."""
|
|
279
|
+
return await self._client._request(
|
|
280
|
+
"POST", "/v1/project/scout", json={"path": path, "reset": reset}
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
async def onboard(
|
|
284
|
+
self, path: str, write: bool = False, stage: bool = False
|
|
285
|
+
) -> dict:
|
|
286
|
+
"""POST /v1/project/onboard."""
|
|
287
|
+
return await self._client._request(
|
|
288
|
+
"POST",
|
|
289
|
+
"/v1/project/onboard",
|
|
290
|
+
json={"path": path, "write": write, "stage": stage},
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
async def promote(self, path: str) -> dict:
|
|
294
|
+
"""POST /v1/project/onboard/promote."""
|
|
295
|
+
return await self._client._request(
|
|
296
|
+
"POST", "/v1/project/onboard/promote", json={"path": path}
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
class _Symbols(_Namespace):
|
|
301
|
+
async def get_unassigned(self) -> dict:
|
|
302
|
+
"""GET /v1/symbols/unassigned."""
|
|
303
|
+
return await self._client._request("GET", "/v1/symbols/unassigned")
|
|
304
|
+
|
|
305
|
+
async def get_drift(self) -> dict:
|
|
306
|
+
"""GET /v1/symbols/drift."""
|
|
307
|
+
return await self._client._request("GET", "/v1/symbols/drift")
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class _Vectors(_Namespace):
|
|
311
|
+
async def query(
|
|
312
|
+
self, query: str, collection: str = "policies", limit: int = 5
|
|
313
|
+
) -> dict:
|
|
314
|
+
"""POST /v1/vectors/query — semantic search over the governed repository."""
|
|
315
|
+
return await self._client._request(
|
|
316
|
+
"POST",
|
|
317
|
+
"/v1/vectors/query",
|
|
318
|
+
json={"query": query, "collection": collection, "limit": limit},
|
|
319
|
+
)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""``core_command`` — runs an async Typer command and reports failures.
|
|
2
|
+
|
|
3
|
+
core-cli's own replacement for core-runtime's decorator of the same name. It
|
|
4
|
+
does only what an HTTP client needs: run the coroutine, show the dry-run
|
|
5
|
+
banner for dangerous commands, ask for confirmation, and turn an exception
|
|
6
|
+
into a clean error and exit code 1. It never builds a CORE context.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import functools
|
|
13
|
+
import inspect
|
|
14
|
+
import sys
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import typer
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
from rich.markup import escape
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
console = Console()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _confirm(message: str) -> bool:
|
|
27
|
+
if not sys.stdin.isatty():
|
|
28
|
+
console.print(
|
|
29
|
+
f"{message}\n[yellow]No interactive terminal — cancelled. Run it from "
|
|
30
|
+
"a terminal to confirm.[/yellow]"
|
|
31
|
+
)
|
|
32
|
+
return False
|
|
33
|
+
return typer.confirm(message, default=False)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ID: b7b9aa4c-a723-411e-8d37-9eb18903259e
|
|
37
|
+
def core_command(
|
|
38
|
+
*, dangerous: bool = False, confirmation: bool = False
|
|
39
|
+
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
40
|
+
"""Wrap an async command.
|
|
41
|
+
|
|
42
|
+
``dangerous`` commands that take a ``write`` flag print a dry-run banner
|
|
43
|
+
when it is off; with ``confirmation`` they ask before running with it on.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
47
|
+
has_write = "write" in inspect.signature(func).parameters
|
|
48
|
+
|
|
49
|
+
@functools.wraps(func)
|
|
50
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
51
|
+
write = bool(kwargs.get("write", False))
|
|
52
|
+
if dangerous and has_write and not write:
|
|
53
|
+
console.print(
|
|
54
|
+
"[bold yellow]⚠️ DRY RUN MODE[/bold yellow]\n"
|
|
55
|
+
" No changes will be made. Use [cyan]--write[/cyan] to apply.\n"
|
|
56
|
+
)
|
|
57
|
+
if (
|
|
58
|
+
dangerous
|
|
59
|
+
and confirmation
|
|
60
|
+
and write
|
|
61
|
+
and not _confirm("🚨 Confirm this operation?")
|
|
62
|
+
):
|
|
63
|
+
raise typer.Exit(0)
|
|
64
|
+
try:
|
|
65
|
+
return asyncio.run(func(*args, **kwargs))
|
|
66
|
+
except typer.Exit:
|
|
67
|
+
raise
|
|
68
|
+
except Exception as exc:
|
|
69
|
+
console.print(
|
|
70
|
+
"\n[bold red]❌ Command failed:[/bold red]\n "
|
|
71
|
+
+ escape(f"{type(exc).__name__}: {exc}")
|
|
72
|
+
)
|
|
73
|
+
raise typer.Exit(1) from exc
|
|
74
|
+
|
|
75
|
+
return wrapper
|
|
76
|
+
|
|
77
|
+
return decorator
|
|
@@ -8,7 +8,6 @@ from core_cli.resources.code import app as code_app
|
|
|
8
8
|
from core_cli.resources.lane import app as lane_app
|
|
9
9
|
from core_cli.resources.project import app as project_app
|
|
10
10
|
from core_cli.resources.proposals import app as proposals_app
|
|
11
|
-
from core_cli.resources.secrets import app as secrets_app
|
|
12
11
|
from core_cli.resources.symbols import app as symbols_app
|
|
13
12
|
from core_cli.resources.vectors import app as vectors_app
|
|
14
13
|
|
|
@@ -22,7 +21,6 @@ app = typer.Typer(
|
|
|
22
21
|
|
|
23
22
|
app.add_typer(lane_app, name="lane")
|
|
24
23
|
app.add_typer(proposals_app, name="proposals")
|
|
25
|
-
app.add_typer(secrets_app, name="secrets")
|
|
26
24
|
app.add_typer(code_app, name="code")
|
|
27
25
|
app.add_typer(symbols_app, name="symbols")
|
|
28
26
|
app.add_typer(vectors_app, name="vectors")
|