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 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