codhc 0.0.1__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.
Potentially problematic release.
This version of codhc might be problematic. Click here for more details.
- codhc-0.0.1/PKG-INFO +25 -0
- codhc-0.0.1/README.md +15 -0
- codhc-0.0.1/pyproject.toml +80 -0
- codhc-0.0.1/src/codhc/__init__.py +0 -0
- codhc-0.0.1/src/codhc/cli.py +28 -0
- codhc-0.0.1/src/codhc/hook_runner.py +82 -0
- codhc-0.0.1/src/codhc/py.typed +0 -0
codhc-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: codhc
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary:
|
|
5
|
+
Author: narumi
|
|
6
|
+
Author-email: narumi <toucans-cutouts0f@icloud.com>
|
|
7
|
+
Requires-Dist: typer>=0.24.1
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# Codex Hook Command
|
|
12
|
+
|
|
13
|
+
`chc` is a small CLI wrapper for Codex hooks. It runs an external command,
|
|
14
|
+
inspects the hook payload from `stdin`, and prints a JSON response that Codex
|
|
15
|
+
can consume.
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
chc uv run ruff check --fix
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
For `Stop` hooks, the first failure returns a continuation payload using the
|
|
24
|
+
legacy `decision: "block"` shape. Success, or failure after the continuation
|
|
25
|
+
pass, returns `continue: false` with a `systemMessage`.
|
codhc-0.0.1/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Codex Hook Command
|
|
2
|
+
|
|
3
|
+
`chc` is a small CLI wrapper for Codex hooks. It runs an external command,
|
|
4
|
+
inspects the hook payload from `stdin`, and prints a JSON response that Codex
|
|
5
|
+
can consume.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
chc uv run ruff check --fix
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
For `Stop` hooks, the first failure returns a continuation payload using the
|
|
14
|
+
legacy `decision: "block"` shape. Success, or failure after the continuation
|
|
15
|
+
pass, returns `continue: false` with a `systemMessage`.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "codhc"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = ""
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
authors = [{ name = "narumi", email = "toucans-cutouts0f@icloud.com" }]
|
|
8
|
+
dependencies = [
|
|
9
|
+
"typer>=0.24.1",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[project.scripts]
|
|
13
|
+
codhc = "codhc.cli:main"
|
|
14
|
+
|
|
15
|
+
[dependency-groups]
|
|
16
|
+
dev = [
|
|
17
|
+
"pytest>=9.0.3",
|
|
18
|
+
"pytest-cov>=7.1.0",
|
|
19
|
+
"ruff>=0.15.10",
|
|
20
|
+
"ty>=0.0.29",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["uv_build>=0.8.17,<0.9.0"]
|
|
25
|
+
build-backend = "uv_build"
|
|
26
|
+
|
|
27
|
+
[tool.bumpversion]
|
|
28
|
+
current_version = "0.0.1"
|
|
29
|
+
tag = true
|
|
30
|
+
commit = true
|
|
31
|
+
pre_commit_hooks = ["uv lock", "git add uv.lock"]
|
|
32
|
+
|
|
33
|
+
[tool.pytest]
|
|
34
|
+
filterwarnings = ["ignore::DeprecationWarning"]
|
|
35
|
+
|
|
36
|
+
[tool.ruff]
|
|
37
|
+
line-length = 120
|
|
38
|
+
|
|
39
|
+
[tool.ruff.lint]
|
|
40
|
+
select = [
|
|
41
|
+
"ANN", # flake8-annotations
|
|
42
|
+
"ASYNC", # flake8-async
|
|
43
|
+
"B", # flake8-bugbear
|
|
44
|
+
"BLE", # flake8-blind-except
|
|
45
|
+
"C", # flake8-comprehensions
|
|
46
|
+
"DTZ", # flake8-datetimez
|
|
47
|
+
"E", # pycodestyle errors
|
|
48
|
+
"F", # pyflakes
|
|
49
|
+
"FURB", # refurb
|
|
50
|
+
"I", # isort
|
|
51
|
+
"G", # flake8-logging-format
|
|
52
|
+
"N", # pep8-naming
|
|
53
|
+
"PERF", # perflint
|
|
54
|
+
"PIE", # flake8-pie
|
|
55
|
+
"PTH", # flake8-use-pathlib
|
|
56
|
+
"RET", # flake8-return
|
|
57
|
+
"RSE", # flake8-raise
|
|
58
|
+
"RUF", # ruff-specific rules
|
|
59
|
+
"SIM", # flake8-simplify
|
|
60
|
+
"TID", # flake8-tidy-imports
|
|
61
|
+
"TRY002", # raise-vanilla-class
|
|
62
|
+
"TRY004", # type-check-without-type-error
|
|
63
|
+
"TRY203", # useless-try-except
|
|
64
|
+
"TRY300", # try-consider-else
|
|
65
|
+
"UP", # pyupgrade
|
|
66
|
+
"W", # pycodestyle warnings
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
[tool.ruff.lint.per-file-ignores]
|
|
70
|
+
"tests/**.py" = ["ANN", "TRY", "PERF"]
|
|
71
|
+
|
|
72
|
+
[tool.ruff.lint.isort]
|
|
73
|
+
force-single-line = true
|
|
74
|
+
|
|
75
|
+
[tool.tombi.format.rules]
|
|
76
|
+
key-value-equals-sign-alignment = true
|
|
77
|
+
trailing-comment-alignment = true
|
|
78
|
+
|
|
79
|
+
[tool.ty.rules]
|
|
80
|
+
unresolved-import = "ignore"
|
|
File without changes
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
from typing import TextIO
|
|
4
|
+
|
|
5
|
+
from chc.hook_runner import run_hook_command
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def run_cli(*, argv: list[str], stdin_text: str) -> tuple[int, str, str]:
|
|
9
|
+
if not argv:
|
|
10
|
+
return (2, "", "Usage: chc <command> [args...]\n")
|
|
11
|
+
|
|
12
|
+
result = run_hook_command(command=argv, stdin_text=stdin_text)
|
|
13
|
+
return (0, f"{json.dumps(result.payload)}\n", result.stderr)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def read_stdin_text(stdin: TextIO) -> str:
|
|
17
|
+
if stdin.isatty():
|
|
18
|
+
return ""
|
|
19
|
+
return stdin.read()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main() -> None:
|
|
23
|
+
exit_code, stdout_text, stderr_text = run_cli(argv=sys.argv[1:], stdin_text=read_stdin_text(sys.stdin))
|
|
24
|
+
if stderr_text:
|
|
25
|
+
sys.stderr.write(stderr_text)
|
|
26
|
+
if stdout_text:
|
|
27
|
+
sys.stdout.write(stdout_text)
|
|
28
|
+
raise SystemExit(exit_code)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shlex
|
|
5
|
+
import subprocess
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class HookResult:
|
|
12
|
+
payload: dict[str, Any]
|
|
13
|
+
stderr: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def run_hook_command(*, command: list[str], stdin_text: str) -> HookResult:
|
|
17
|
+
stop_hook_active = _parse_stop_hook_active(stdin_text)
|
|
18
|
+
command_display = f"`{shlex.join(command)}`"
|
|
19
|
+
completed = _run_subprocess(command)
|
|
20
|
+
|
|
21
|
+
if completed.returncode == 0:
|
|
22
|
+
return HookResult(
|
|
23
|
+
payload={
|
|
24
|
+
"continue": False,
|
|
25
|
+
"systemMessage": f"{command_display} passed.",
|
|
26
|
+
},
|
|
27
|
+
stderr="",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if stop_hook_active:
|
|
31
|
+
return HookResult(
|
|
32
|
+
payload={
|
|
33
|
+
"continue": False,
|
|
34
|
+
"systemMessage": f"{command_display} failed after the Stop continuation pass.",
|
|
35
|
+
},
|
|
36
|
+
stderr=completed.output,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
return HookResult(
|
|
40
|
+
payload={
|
|
41
|
+
"decision": "block",
|
|
42
|
+
"reason": f"{command_display} failed. Inspect the command output, fix the issues, then stop again.",
|
|
43
|
+
},
|
|
44
|
+
stderr=completed.output,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class CommandResult:
|
|
50
|
+
returncode: int
|
|
51
|
+
output: str
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _parse_stop_hook_active(stdin_text: str) -> bool:
|
|
55
|
+
if not stdin_text.strip():
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
payload = json.loads(stdin_text)
|
|
60
|
+
except json.JSONDecodeError:
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
if not isinstance(payload, dict):
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
return payload.get("stop_hook_active") is True
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _run_subprocess(command: list[str]) -> CommandResult:
|
|
70
|
+
try:
|
|
71
|
+
completed = subprocess.run(
|
|
72
|
+
command,
|
|
73
|
+
check=False,
|
|
74
|
+
stdout=subprocess.PIPE,
|
|
75
|
+
stderr=subprocess.STDOUT,
|
|
76
|
+
text=True,
|
|
77
|
+
)
|
|
78
|
+
except FileNotFoundError as exc:
|
|
79
|
+
missing = exc.filename or command[0]
|
|
80
|
+
return CommandResult(returncode=127, output=f"{missing}: command not found\n")
|
|
81
|
+
|
|
82
|
+
return CommandResult(returncode=completed.returncode, output=completed.stdout)
|
|
File without changes
|