code-mower 0.5.0b5__py3-none-any.whl

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.
Files changed (185) hide show
  1. code_mower/__init__.py +3 -0
  2. code_mower/adapters/__init__.py +39 -0
  3. code_mower/adapters/_base.py +148 -0
  4. code_mower/adapters/cursor_bugbot.py +97 -0
  5. code_mower/adapters/gitar.py +111 -0
  6. code_mower/adapters/greptile.py +191 -0
  7. code_mower/adapters/qodo.py +140 -0
  8. code_mower/antigravity_cli_audit_pr.py +241 -0
  9. code_mower/audit_handoff_log.py +345 -0
  10. code_mower/audit_labeler_lib.py +311 -0
  11. code_mower/audit_progress.py +203 -0
  12. code_mower/blind_review_artifacts.py +562 -0
  13. code_mower/blind_review_coordinator.py +276 -0
  14. code_mower/bootstrap.py +524 -0
  15. code_mower/builder_experiment.py +539 -0
  16. code_mower/calibration/__init__.py +181 -0
  17. code_mower/calibration/arms.py +267 -0
  18. code_mower/calibration/auto_discovery.py +348 -0
  19. code_mower/calibration/commands.py +192 -0
  20. code_mower/calibration/context_inputs.py +199 -0
  21. code_mower/calibration/corpus.py +91 -0
  22. code_mower/calibration/evidence.py +20 -0
  23. code_mower/calibration/evidence_report.py +360 -0
  24. code_mower/calibration/identity.py +27 -0
  25. code_mower/calibration/metrics.py +18 -0
  26. code_mower/calibration/overlap.py +103 -0
  27. code_mower/calibration/planning.py +309 -0
  28. code_mower/calibration/policy.py +207 -0
  29. code_mower/calibration/results.py +315 -0
  30. code_mower/calibration/run_results.py +147 -0
  31. code_mower/calibration/run_status.py +64 -0
  32. code_mower/calibration/runner.py +360 -0
  33. code_mower/calibration/truth.py +188 -0
  34. code_mower/calibration/value_report.py +142 -0
  35. code_mower/checks.py +402 -0
  36. code_mower/claude_audit_pr.py +1126 -0
  37. code_mower/claude_cli_bounce.py +303 -0
  38. code_mower/claude_cli_environment.py +73 -0
  39. code_mower/clear_stale.py +374 -0
  40. code_mower/cli.py +537 -0
  41. code_mower/cloud.py +674 -0
  42. code_mower/cloud_client/__init__.py +173 -0
  43. code_mower/cloud_client/bundle.py +155 -0
  44. code_mower/cloud_client/doctor.py +206 -0
  45. code_mower/cloud_client/dogfood.py +78 -0
  46. code_mower/cloud_client/endpoints.py +114 -0
  47. code_mower/cloud_client/errors.py +7 -0
  48. code_mower/cloud_client/events.py +278 -0
  49. code_mower/cloud_client/export.py +272 -0
  50. code_mower/cloud_client/git_metadata.py +46 -0
  51. code_mower/cloud_client/manifest.py +39 -0
  52. code_mower/cloud_client/operations.py +448 -0
  53. code_mower/cloud_client/reports.py +45 -0
  54. code_mower/cloud_client/setup.py +205 -0
  55. code_mower/cloud_client/upload.py +97 -0
  56. code_mower/code_mower_calibration.py +598 -0
  57. code_mower/code_mower_context_packs.py +591 -0
  58. code_mower/code_mower_merge.py +227 -0
  59. code_mower/code_mower_telemetry.py +561 -0
  60. code_mower/coderabbit_cli_audit_pr.py +526 -0
  61. code_mower/codex_audit_env_preflight.py +220 -0
  62. code_mower/codex_audit_pr.py +1738 -0
  63. code_mower/codex_audit_schema_smoke.py +160 -0
  64. code_mower/codex_audit_verdict.schema.json +44 -0
  65. code_mower/config.py +655 -0
  66. code_mower/doctor.py +161 -0
  67. code_mower/doctor_checks/__init__.py +104 -0
  68. code_mower/doctor_checks/cloud.py +129 -0
  69. code_mower/doctor_checks/common.py +215 -0
  70. code_mower/doctor_checks/github.py +128 -0
  71. code_mower/doctor_checks/github_actions.py +11 -0
  72. code_mower/doctor_checks/github_actions_cost.py +99 -0
  73. code_mower/doctor_checks/github_actions_cost_summary.py +111 -0
  74. code_mower/doctor_checks/github_actions_failure_annotations.py +27 -0
  75. code_mower/doctor_checks/github_actions_failure_models.py +47 -0
  76. code_mower/doctor_checks/github_actions_failure_scan.py +200 -0
  77. code_mower/doctor_checks/github_actions_failure_selection.py +63 -0
  78. code_mower/doctor_checks/github_actions_failures.py +103 -0
  79. code_mower/doctor_checks/github_actions_permissions.py +55 -0
  80. code_mower/doctor_checks/github_api.py +79 -0
  81. code_mower/doctor_checks/github_branch.py +56 -0
  82. code_mower/doctor_checks/github_config.py +25 -0
  83. code_mower/doctor_checks/github_provider.py +61 -0
  84. code_mower/doctor_checks/github_repo.py +120 -0
  85. code_mower/doctor_checks/groups.py +36 -0
  86. code_mower/doctor_checks/models.py +96 -0
  87. code_mower/doctor_checks/output.py +86 -0
  88. code_mower/doctor_checks/presets.py +64 -0
  89. code_mower/doctor_checks/privacy.py +20 -0
  90. code_mower/doctor_checks/provider_api_model.py +138 -0
  91. code_mower/doctor_checks/provider_api_model_openai.py +29 -0
  92. code_mower/doctor_checks/provider_api_model_profiles.py +137 -0
  93. code_mower/doctor_checks/provider_env.py +113 -0
  94. code_mower/doctor_checks/provider_env_required.py +56 -0
  95. code_mower/doctor_checks/provider_env_tokens.py +100 -0
  96. code_mower/doctor_checks/provider_local_cli.py +162 -0
  97. code_mower/doctor_checks/provider_local_cli_commands.py +47 -0
  98. code_mower/doctor_checks/provider_local_cli_probe_config.py +70 -0
  99. code_mower/doctor_checks/provider_probe.py +20 -0
  100. code_mower/doctor_checks/provider_probe_auth.py +52 -0
  101. code_mower/doctor_checks/provider_probe_evaluation.py +109 -0
  102. code_mower/doctor_checks/provider_probe_json.py +45 -0
  103. code_mower/doctor_checks/provider_probe_remediation.py +39 -0
  104. code_mower/doctor_checks/providers.py +159 -0
  105. code_mower/doctor_checks/registry.py +69 -0
  106. code_mower/doctor_checks/runner.py +188 -0
  107. code_mower/doctor_checks/runtime.py +89 -0
  108. code_mower/doctor_checks/runtime_github_auth.py +148 -0
  109. code_mower/gemini_cli_audit_pr.py +897 -0
  110. code_mower/hermes_cli_audit_pr.py +436 -0
  111. code_mower/init.py +888 -0
  112. code_mower/lane_configs/__init__.py +37 -0
  113. code_mower/lane_configs/aider.py +32 -0
  114. code_mower/lane_configs/antigravity_cli.py +35 -0
  115. code_mower/lane_configs/claude.py +35 -0
  116. code_mower/lane_configs/codex.py +32 -0
  117. code_mower/lane_configs/devin.py +33 -0
  118. code_mower/lane_configs/gemini_cli.py +35 -0
  119. code_mower/lane_configs/hermes_cli.py +35 -0
  120. code_mower/lane_configs/local_llm.py +31 -0
  121. code_mower/local_llm_audit_pr.py +1364 -0
  122. code_mower/local_llm_bakeoff.py +458 -0
  123. code_mower/local_llm_calibration.py +441 -0
  124. code_mower/local_llm_profiles.py +66 -0
  125. code_mower/migration.py +508 -0
  126. code_mower/migration_install.py +292 -0
  127. code_mower/migration_mirror.py +392 -0
  128. code_mower/migration_readiness.py +237 -0
  129. code_mower/migration_rehearsal.py +718 -0
  130. code_mower/next_steps.py +441 -0
  131. code_mower/package.py +673 -0
  132. code_mower/package_content.py +444 -0
  133. code_mower/package_manifest.py +452 -0
  134. code_mower/package_paths.py +53 -0
  135. code_mower/package_rendering.py +90 -0
  136. code_mower/package_static.py +585 -0
  137. code_mower/prompts.py +267 -0
  138. code_mower/provider_registry.py +469 -0
  139. code_mower/provider_runners/__init__.py +60 -0
  140. code_mower/provider_runners/comments.py +31 -0
  141. code_mower/provider_runners/git.py +46 -0
  142. code_mower/provider_runners/github_auth.py +61 -0
  143. code_mower/provider_runners/github_pr.py +120 -0
  144. code_mower/provider_runners/process.py +58 -0
  145. code_mower/provider_runners/repo_paths.py +23 -0
  146. code_mower/provider_runners/text_schema.py +41 -0
  147. code_mower/provider_runners/verdict_artifacts.py +103 -0
  148. code_mower/provider_runners/workspace.py +57 -0
  149. code_mower/release_readiness.py +549 -0
  150. code_mower/reviewer_metrics.py +389 -0
  151. code_mower/saas_reviewer_labeler.py +809 -0
  152. code_mower/secrets.py +89 -0
  153. code_mower/templates/builder-experiment.example.json +55 -0
  154. code_mower/templates/calibration-corpus.example.json +129 -0
  155. code_mower/templates/calibration-corpus.json +129 -0
  156. code_mower/templates/code-mower.example.yml +423 -0
  157. code_mower/templates/context-packs.example.json +150 -0
  158. code_mower/templates/lane_prompts/base-audit.md +22 -0
  159. code_mower/templates/lane_prompts/calibration-policy.md +21 -0
  160. code_mower/templates/lane_prompts/context-driven-quality.md +21 -0
  161. code_mower/templates/lane_prompts/docs-design.md +12 -0
  162. code_mower/templates/lane_prompts/generic-programming.md +21 -0
  163. code_mower/templates/lane_prompts/operability.md +22 -0
  164. code_mower/templates/lane_prompts/package-runtime.md +12 -0
  165. code_mower/templates/lane_prompts/security-threat-model.md +22 -0
  166. code_mower/templates/product-support/code_mower +216 -0
  167. code_mower/templates/product-support/code_mower_standalone_pin.env +7 -0
  168. code_mower/templates/product-support/code_mower_standalone_shadow.sh +151 -0
  169. code_mower/templates/product-support/run_claude_audit_pr.sh +32 -0
  170. code_mower/templates/product-support/run_codex_audit_pr.sh +32 -0
  171. code_mower/templates/product-support/safe_gh_comment.py +96 -0
  172. code_mower/templates/providers.yml +454 -0
  173. code_mower/templates/reviewer-spend.example.json +28 -0
  174. code_mower/templates/reviewer-value-report.example.md +20 -0
  175. code_mower/templates/workflows/private-standalone-shadow.yml.j2 +106 -0
  176. code_mower/templates/workflows/review-clear-stale.yml.j2 +83 -0
  177. code_mower/trailer_comment_labeler.py +207 -0
  178. code_mower/versioning.py +32 -0
  179. code_mower-0.5.0b5.dist-info/METADATA +302 -0
  180. code_mower-0.5.0b5.dist-info/RECORD +185 -0
  181. code_mower-0.5.0b5.dist-info/WHEEL +5 -0
  182. code_mower-0.5.0b5.dist-info/entry_points.txt +2 -0
  183. code_mower-0.5.0b5.dist-info/licenses/LICENSE +202 -0
  184. code_mower-0.5.0b5.dist-info/licenses/NOTICE +10 -0
  185. code_mower-0.5.0b5.dist-info/top_level.txt +1 -0
code_mower/checks.py ADDED
@@ -0,0 +1,402 @@
1
+ #!/usr/bin/env python3
2
+ """Detect and run a repository's native check surface."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import shlex
9
+ import subprocess
10
+ import sys
11
+ import time
12
+ from dataclasses import dataclass
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class CheckSpec:
19
+ id: str
20
+ category: str
21
+ language: str
22
+ tool: str
23
+ command: tuple[str, ...]
24
+ source: str
25
+ reason: str
26
+
27
+ def as_dict(self) -> dict[str, Any]:
28
+ return {
29
+ "id": self.id,
30
+ "category": self.category,
31
+ "language": self.language,
32
+ "tool": self.tool,
33
+ "command": list(self.command),
34
+ "source": self.source,
35
+ "reason": self.reason,
36
+ }
37
+
38
+
39
+ NODE_SCRIPT_CANDIDATES: tuple[tuple[str, str, tuple[str, ...]], ...] = (
40
+ ("node.lint", "lint", ("lint",)),
41
+ ("node.typecheck", "typecheck", ("typecheck", "type-check", "check-types", "tsc")),
42
+ ("node.test", "test", ("test", "test:unit")),
43
+ ("node.build", "build", ("build",)),
44
+ )
45
+
46
+ MAKE_TARGETS: tuple[tuple[str, str], ...] = (
47
+ ("make.lint", "lint"),
48
+ ("make.test", "test"),
49
+ ("make.build", "build"),
50
+ )
51
+
52
+
53
+ def _read_json(path: Path) -> dict[str, Any]:
54
+ try:
55
+ data = json.loads(path.read_text(encoding="utf-8"))
56
+ except (OSError, json.JSONDecodeError):
57
+ return {}
58
+ return data if isinstance(data, dict) else {}
59
+
60
+
61
+ def _node_package_manager(repo_path: Path) -> tuple[str, tuple[str, ...], str]:
62
+ if (repo_path / "pnpm-lock.yaml").exists():
63
+ return "pnpm", ("pnpm", "run"), "pnpm-lock.yaml"
64
+ if (repo_path / "yarn.lock").exists():
65
+ return "yarn", ("yarn",), "yarn.lock"
66
+ if (repo_path / "bun.lockb").exists() or (repo_path / "bun.lock").exists():
67
+ return "bun", ("bun", "run"), "bun lockfile"
68
+ return "npm", ("npm", "run"), "package.json"
69
+
70
+
71
+ def _detect_node_checks(repo_path: Path) -> list[CheckSpec]:
72
+ package_json = repo_path / "package.json"
73
+ if not package_json.exists():
74
+ return []
75
+ data = _read_json(package_json)
76
+ scripts = data.get("scripts")
77
+ if not isinstance(scripts, dict):
78
+ return []
79
+
80
+ manager, prefix, lock_source = _node_package_manager(repo_path)
81
+ checks: list[CheckSpec] = []
82
+ for check_id, category, candidates in NODE_SCRIPT_CANDIDATES:
83
+ script = next((candidate for candidate in candidates if candidate in scripts), None)
84
+ if script is None:
85
+ continue
86
+ checks.append(
87
+ CheckSpec(
88
+ id=check_id,
89
+ category=category,
90
+ language="javascript",
91
+ tool=manager,
92
+ command=(*prefix, script),
93
+ source=f"package.json:scripts.{script}",
94
+ reason=f"{lock_source} selects {manager}; package.json declares {script!r}.",
95
+ )
96
+ )
97
+ return checks
98
+
99
+
100
+ def _python_command(repo_path: Path) -> tuple[str, str]:
101
+ candidates = (
102
+ repo_path / ".venv" / "bin" / "python",
103
+ repo_path / ".venv" / "Scripts" / "python.exe",
104
+ )
105
+ for candidate in candidates:
106
+ if candidate.exists():
107
+ return str(candidate), ".venv"
108
+ return sys.executable, "current interpreter"
109
+
110
+
111
+ def _has_ruff_config(repo_path: Path) -> bool:
112
+ if (repo_path / "ruff.toml").exists() or (repo_path / ".ruff.toml").exists():
113
+ return True
114
+ pyproject = repo_path / "pyproject.toml"
115
+ if not pyproject.exists():
116
+ return False
117
+ try:
118
+ text = pyproject.read_text(encoding="utf-8")
119
+ except OSError:
120
+ return False
121
+ return "[tool.ruff]" in text or "[tool.ruff." in text
122
+
123
+
124
+ def _detect_python_checks(repo_path: Path) -> list[CheckSpec]:
125
+ has_python_project = any(
126
+ (repo_path / name).exists()
127
+ for name in ("pyproject.toml", "requirements.txt", "setup.py", "setup.cfg")
128
+ )
129
+ tests_dir = repo_path / "tests"
130
+ has_python_tests = tests_dir.is_dir() and any(tests_dir.rglob("*.py"))
131
+ if not has_python_project and not has_python_tests:
132
+ return []
133
+
134
+ python, python_source = _python_command(repo_path)
135
+ checks: list[CheckSpec] = []
136
+ if _has_ruff_config(repo_path):
137
+ checks.append(
138
+ CheckSpec(
139
+ id="python.ruff",
140
+ category="lint",
141
+ language="python",
142
+ tool="ruff",
143
+ command=(python, "-m", "ruff", "check", "."),
144
+ source="ruff configuration",
145
+ reason=f"Ruff config is present; using {python_source} Python.",
146
+ )
147
+ )
148
+ if has_python_tests:
149
+ checks.append(
150
+ CheckSpec(
151
+ id="python.pytest",
152
+ category="test",
153
+ language="python",
154
+ tool="pytest",
155
+ command=(python, "-m", "pytest", "tests", "-q"),
156
+ source="tests/",
157
+ reason=f"tests/ exists; using {python_source} Python.",
158
+ )
159
+ )
160
+ return checks
161
+
162
+
163
+ def _make_targets(repo_path: Path) -> set[str]:
164
+ makefile = repo_path / "Makefile"
165
+ if not makefile.exists():
166
+ return set()
167
+ targets: set[str] = set()
168
+ try:
169
+ for line in makefile.read_text(encoding="utf-8").splitlines():
170
+ stripped = line.strip()
171
+ if not stripped or stripped.startswith("#") or stripped.startswith("."):
172
+ continue
173
+ name, separator, _rest = stripped.partition(":")
174
+ if separator and name and all(char not in name for char in " \t=$"):
175
+ targets.add(name)
176
+ except OSError:
177
+ return set()
178
+ return targets
179
+
180
+
181
+ def _detect_make_checks(repo_path: Path) -> list[CheckSpec]:
182
+ targets = _make_targets(repo_path)
183
+ return [
184
+ CheckSpec(
185
+ id=check_id,
186
+ category=target,
187
+ language="generic",
188
+ tool="make",
189
+ command=("make", target),
190
+ source=f"Makefile:{target}",
191
+ reason=f"Makefile declares {target!r}.",
192
+ )
193
+ for check_id, target in MAKE_TARGETS
194
+ if target in targets
195
+ ]
196
+
197
+
198
+ def detect_checks(repo_path: Path) -> list[CheckSpec]:
199
+ repo_path = repo_path.resolve()
200
+ checks = [
201
+ *_detect_node_checks(repo_path),
202
+ *_detect_python_checks(repo_path),
203
+ *_detect_make_checks(repo_path),
204
+ ]
205
+ seen: set[str] = set()
206
+ unique: list[CheckSpec] = []
207
+ for check in checks:
208
+ if check.id in seen:
209
+ continue
210
+ seen.add(check.id)
211
+ unique.append(check)
212
+ return unique
213
+
214
+
215
+ def _command_text(command: tuple[str, ...]) -> str:
216
+ return " ".join(shlex.quote(part) for part in command)
217
+
218
+
219
+ def _output_text(value: str | bytes | None) -> str:
220
+ if value is None:
221
+ return ""
222
+ if isinstance(value, bytes):
223
+ return value.decode("utf-8", errors="replace")
224
+ return value
225
+
226
+
227
+ def _preview(text: str | bytes | None, *, enabled: bool, limit: int = 4000) -> str | None:
228
+ if not enabled:
229
+ return None
230
+ text = _output_text(text)
231
+ if len(text) <= limit:
232
+ return text
233
+ return text[:limit] + "\n... [truncated]"
234
+
235
+
236
+ def run_checks(
237
+ checks: list[CheckSpec],
238
+ *,
239
+ repo_path: Path,
240
+ timeout_seconds: int,
241
+ include_output: bool = False,
242
+ dry_run: bool = False,
243
+ ) -> tuple[list[dict[str, Any]], int]:
244
+ results: list[dict[str, Any]] = []
245
+ exit_code = 0
246
+ for check in checks:
247
+ started = time.monotonic()
248
+ result: dict[str, Any] = {
249
+ **check.as_dict(),
250
+ "command_text": _command_text(check.command),
251
+ }
252
+ if dry_run:
253
+ result.update(
254
+ {
255
+ "status": "planned",
256
+ "returncode": None,
257
+ "duration_seconds": 0.0,
258
+ }
259
+ )
260
+ results.append(result)
261
+ continue
262
+
263
+ try:
264
+ completed = subprocess.run(
265
+ check.command,
266
+ cwd=repo_path,
267
+ text=True,
268
+ capture_output=True,
269
+ timeout=timeout_seconds,
270
+ check=False,
271
+ )
272
+ except FileNotFoundError as exc:
273
+ duration = time.monotonic() - started
274
+ result.update(
275
+ {
276
+ "status": "failed",
277
+ "returncode": 127,
278
+ "duration_seconds": round(duration, 3),
279
+ "error": str(exc),
280
+ }
281
+ )
282
+ exit_code = 1
283
+ except subprocess.TimeoutExpired as exc:
284
+ duration = time.monotonic() - started
285
+ result.update(
286
+ {
287
+ "status": "timeout",
288
+ "returncode": None,
289
+ "duration_seconds": round(duration, 3),
290
+ "stdout_preview": _preview(exc.stdout or "", enabled=include_output),
291
+ "stderr_preview": _preview(exc.stderr or "", enabled=include_output),
292
+ }
293
+ )
294
+ exit_code = 1
295
+ else:
296
+ duration = time.monotonic() - started
297
+ status = "passed" if completed.returncode == 0 else "failed"
298
+ result.update(
299
+ {
300
+ "status": status,
301
+ "returncode": completed.returncode,
302
+ "duration_seconds": round(duration, 3),
303
+ "stdout_preview": _preview(completed.stdout, enabled=include_output),
304
+ "stderr_preview": _preview(completed.stderr, enabled=include_output),
305
+ }
306
+ )
307
+ if completed.returncode != 0:
308
+ exit_code = 1
309
+ results.append(result)
310
+ return results, exit_code
311
+
312
+
313
+ def _filter_checks(checks: list[CheckSpec], only: list[str]) -> list[CheckSpec]:
314
+ if not only:
315
+ return checks
316
+ wanted = {item for group in only for item in group.split(",") if item}
317
+ return [
318
+ check
319
+ for check in checks
320
+ if check.id in wanted or check.category in wanted or check.language in wanted
321
+ ]
322
+
323
+
324
+ def _render_detect_text(checks: list[CheckSpec]) -> str:
325
+ if not checks:
326
+ return "No native checks detected.\n"
327
+ lines = ["Detected native checks:"]
328
+ for check in checks:
329
+ lines.append(f"- {check.id}: {_command_text(check.command)}")
330
+ lines.append(f" source: {check.source}")
331
+ return "\n".join(lines) + "\n"
332
+
333
+
334
+ def _render_run_text(results: list[dict[str, Any]]) -> str:
335
+ if not results:
336
+ return "No native checks detected.\n"
337
+ lines = ["Native check results:"]
338
+ for result in results:
339
+ lines.append(
340
+ f"- {result['id']}: {result['status']} "
341
+ f"({result['duration_seconds']}s) {result['command_text']}"
342
+ )
343
+ return "\n".join(lines) + "\n"
344
+
345
+
346
+ def _json_payload(mode: str, repo_path: Path, checks: list[dict[str, Any]]) -> dict[str, Any]:
347
+ return {
348
+ "mode": mode,
349
+ "repo_path": str(repo_path.resolve()),
350
+ "check_count": len(checks),
351
+ "checks": checks,
352
+ }
353
+
354
+
355
+ def main(argv: list[str] | None = None) -> int:
356
+ parser = argparse.ArgumentParser(prog="code-mower checks")
357
+ subparsers = parser.add_subparsers(dest="command", required=True)
358
+
359
+ detect = subparsers.add_parser("detect")
360
+ detect.add_argument("--repo-path", type=Path, default=Path.cwd())
361
+ detect.add_argument("--json", action="store_true")
362
+
363
+ run = subparsers.add_parser("run")
364
+ run.add_argument("--repo-path", type=Path, default=Path.cwd())
365
+ run.add_argument("--only", action="append", default=[])
366
+ run.add_argument("--timeout", type=int, default=600)
367
+ run.add_argument("--dry-run", action="store_true")
368
+ run.add_argument("--include-output", action="store_true")
369
+ run.add_argument("--json", action="store_true")
370
+
371
+ args = parser.parse_args(argv)
372
+ repo_path = args.repo_path.resolve()
373
+ checks = detect_checks(repo_path)
374
+
375
+ if args.command == "detect":
376
+ check_dicts = [check.as_dict() for check in checks]
377
+ if args.json:
378
+ print(json.dumps(_json_payload("code-mower-checks-detect", repo_path, check_dicts), indent=2))
379
+ else:
380
+ print(_render_detect_text(checks), end="")
381
+ return 0
382
+
383
+ if args.command == "run":
384
+ selected = _filter_checks(checks, args.only)
385
+ results, exit_code = run_checks(
386
+ selected,
387
+ repo_path=repo_path,
388
+ timeout_seconds=args.timeout,
389
+ include_output=args.include_output,
390
+ dry_run=args.dry_run,
391
+ )
392
+ if args.json:
393
+ print(json.dumps(_json_payload("code-mower-checks-run", repo_path, results), indent=2))
394
+ else:
395
+ print(_render_run_text(results), end="")
396
+ return exit_code
397
+
398
+ raise AssertionError(f"unhandled checks command: {args.command}")
399
+
400
+
401
+ if __name__ == "__main__":
402
+ raise SystemExit(main())