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.
- code_mower/__init__.py +3 -0
- code_mower/adapters/__init__.py +39 -0
- code_mower/adapters/_base.py +148 -0
- code_mower/adapters/cursor_bugbot.py +97 -0
- code_mower/adapters/gitar.py +111 -0
- code_mower/adapters/greptile.py +191 -0
- code_mower/adapters/qodo.py +140 -0
- code_mower/antigravity_cli_audit_pr.py +241 -0
- code_mower/audit_handoff_log.py +345 -0
- code_mower/audit_labeler_lib.py +311 -0
- code_mower/audit_progress.py +203 -0
- code_mower/blind_review_artifacts.py +562 -0
- code_mower/blind_review_coordinator.py +276 -0
- code_mower/bootstrap.py +524 -0
- code_mower/builder_experiment.py +539 -0
- code_mower/calibration/__init__.py +181 -0
- code_mower/calibration/arms.py +267 -0
- code_mower/calibration/auto_discovery.py +348 -0
- code_mower/calibration/commands.py +192 -0
- code_mower/calibration/context_inputs.py +199 -0
- code_mower/calibration/corpus.py +91 -0
- code_mower/calibration/evidence.py +20 -0
- code_mower/calibration/evidence_report.py +360 -0
- code_mower/calibration/identity.py +27 -0
- code_mower/calibration/metrics.py +18 -0
- code_mower/calibration/overlap.py +103 -0
- code_mower/calibration/planning.py +309 -0
- code_mower/calibration/policy.py +207 -0
- code_mower/calibration/results.py +315 -0
- code_mower/calibration/run_results.py +147 -0
- code_mower/calibration/run_status.py +64 -0
- code_mower/calibration/runner.py +360 -0
- code_mower/calibration/truth.py +188 -0
- code_mower/calibration/value_report.py +142 -0
- code_mower/checks.py +402 -0
- code_mower/claude_audit_pr.py +1126 -0
- code_mower/claude_cli_bounce.py +303 -0
- code_mower/claude_cli_environment.py +73 -0
- code_mower/clear_stale.py +374 -0
- code_mower/cli.py +537 -0
- code_mower/cloud.py +674 -0
- code_mower/cloud_client/__init__.py +173 -0
- code_mower/cloud_client/bundle.py +155 -0
- code_mower/cloud_client/doctor.py +206 -0
- code_mower/cloud_client/dogfood.py +78 -0
- code_mower/cloud_client/endpoints.py +114 -0
- code_mower/cloud_client/errors.py +7 -0
- code_mower/cloud_client/events.py +278 -0
- code_mower/cloud_client/export.py +272 -0
- code_mower/cloud_client/git_metadata.py +46 -0
- code_mower/cloud_client/manifest.py +39 -0
- code_mower/cloud_client/operations.py +448 -0
- code_mower/cloud_client/reports.py +45 -0
- code_mower/cloud_client/setup.py +205 -0
- code_mower/cloud_client/upload.py +97 -0
- code_mower/code_mower_calibration.py +598 -0
- code_mower/code_mower_context_packs.py +591 -0
- code_mower/code_mower_merge.py +227 -0
- code_mower/code_mower_telemetry.py +561 -0
- code_mower/coderabbit_cli_audit_pr.py +526 -0
- code_mower/codex_audit_env_preflight.py +220 -0
- code_mower/codex_audit_pr.py +1738 -0
- code_mower/codex_audit_schema_smoke.py +160 -0
- code_mower/codex_audit_verdict.schema.json +44 -0
- code_mower/config.py +655 -0
- code_mower/doctor.py +161 -0
- code_mower/doctor_checks/__init__.py +104 -0
- code_mower/doctor_checks/cloud.py +129 -0
- code_mower/doctor_checks/common.py +215 -0
- code_mower/doctor_checks/github.py +128 -0
- code_mower/doctor_checks/github_actions.py +11 -0
- code_mower/doctor_checks/github_actions_cost.py +99 -0
- code_mower/doctor_checks/github_actions_cost_summary.py +111 -0
- code_mower/doctor_checks/github_actions_failure_annotations.py +27 -0
- code_mower/doctor_checks/github_actions_failure_models.py +47 -0
- code_mower/doctor_checks/github_actions_failure_scan.py +200 -0
- code_mower/doctor_checks/github_actions_failure_selection.py +63 -0
- code_mower/doctor_checks/github_actions_failures.py +103 -0
- code_mower/doctor_checks/github_actions_permissions.py +55 -0
- code_mower/doctor_checks/github_api.py +79 -0
- code_mower/doctor_checks/github_branch.py +56 -0
- code_mower/doctor_checks/github_config.py +25 -0
- code_mower/doctor_checks/github_provider.py +61 -0
- code_mower/doctor_checks/github_repo.py +120 -0
- code_mower/doctor_checks/groups.py +36 -0
- code_mower/doctor_checks/models.py +96 -0
- code_mower/doctor_checks/output.py +86 -0
- code_mower/doctor_checks/presets.py +64 -0
- code_mower/doctor_checks/privacy.py +20 -0
- code_mower/doctor_checks/provider_api_model.py +138 -0
- code_mower/doctor_checks/provider_api_model_openai.py +29 -0
- code_mower/doctor_checks/provider_api_model_profiles.py +137 -0
- code_mower/doctor_checks/provider_env.py +113 -0
- code_mower/doctor_checks/provider_env_required.py +56 -0
- code_mower/doctor_checks/provider_env_tokens.py +100 -0
- code_mower/doctor_checks/provider_local_cli.py +162 -0
- code_mower/doctor_checks/provider_local_cli_commands.py +47 -0
- code_mower/doctor_checks/provider_local_cli_probe_config.py +70 -0
- code_mower/doctor_checks/provider_probe.py +20 -0
- code_mower/doctor_checks/provider_probe_auth.py +52 -0
- code_mower/doctor_checks/provider_probe_evaluation.py +109 -0
- code_mower/doctor_checks/provider_probe_json.py +45 -0
- code_mower/doctor_checks/provider_probe_remediation.py +39 -0
- code_mower/doctor_checks/providers.py +159 -0
- code_mower/doctor_checks/registry.py +69 -0
- code_mower/doctor_checks/runner.py +188 -0
- code_mower/doctor_checks/runtime.py +89 -0
- code_mower/doctor_checks/runtime_github_auth.py +148 -0
- code_mower/gemini_cli_audit_pr.py +897 -0
- code_mower/hermes_cli_audit_pr.py +436 -0
- code_mower/init.py +888 -0
- code_mower/lane_configs/__init__.py +37 -0
- code_mower/lane_configs/aider.py +32 -0
- code_mower/lane_configs/antigravity_cli.py +35 -0
- code_mower/lane_configs/claude.py +35 -0
- code_mower/lane_configs/codex.py +32 -0
- code_mower/lane_configs/devin.py +33 -0
- code_mower/lane_configs/gemini_cli.py +35 -0
- code_mower/lane_configs/hermes_cli.py +35 -0
- code_mower/lane_configs/local_llm.py +31 -0
- code_mower/local_llm_audit_pr.py +1364 -0
- code_mower/local_llm_bakeoff.py +458 -0
- code_mower/local_llm_calibration.py +441 -0
- code_mower/local_llm_profiles.py +66 -0
- code_mower/migration.py +508 -0
- code_mower/migration_install.py +292 -0
- code_mower/migration_mirror.py +392 -0
- code_mower/migration_readiness.py +237 -0
- code_mower/migration_rehearsal.py +718 -0
- code_mower/next_steps.py +441 -0
- code_mower/package.py +673 -0
- code_mower/package_content.py +444 -0
- code_mower/package_manifest.py +452 -0
- code_mower/package_paths.py +53 -0
- code_mower/package_rendering.py +90 -0
- code_mower/package_static.py +585 -0
- code_mower/prompts.py +267 -0
- code_mower/provider_registry.py +469 -0
- code_mower/provider_runners/__init__.py +60 -0
- code_mower/provider_runners/comments.py +31 -0
- code_mower/provider_runners/git.py +46 -0
- code_mower/provider_runners/github_auth.py +61 -0
- code_mower/provider_runners/github_pr.py +120 -0
- code_mower/provider_runners/process.py +58 -0
- code_mower/provider_runners/repo_paths.py +23 -0
- code_mower/provider_runners/text_schema.py +41 -0
- code_mower/provider_runners/verdict_artifacts.py +103 -0
- code_mower/provider_runners/workspace.py +57 -0
- code_mower/release_readiness.py +549 -0
- code_mower/reviewer_metrics.py +389 -0
- code_mower/saas_reviewer_labeler.py +809 -0
- code_mower/secrets.py +89 -0
- code_mower/templates/builder-experiment.example.json +55 -0
- code_mower/templates/calibration-corpus.example.json +129 -0
- code_mower/templates/calibration-corpus.json +129 -0
- code_mower/templates/code-mower.example.yml +423 -0
- code_mower/templates/context-packs.example.json +150 -0
- code_mower/templates/lane_prompts/base-audit.md +22 -0
- code_mower/templates/lane_prompts/calibration-policy.md +21 -0
- code_mower/templates/lane_prompts/context-driven-quality.md +21 -0
- code_mower/templates/lane_prompts/docs-design.md +12 -0
- code_mower/templates/lane_prompts/generic-programming.md +21 -0
- code_mower/templates/lane_prompts/operability.md +22 -0
- code_mower/templates/lane_prompts/package-runtime.md +12 -0
- code_mower/templates/lane_prompts/security-threat-model.md +22 -0
- code_mower/templates/product-support/code_mower +216 -0
- code_mower/templates/product-support/code_mower_standalone_pin.env +7 -0
- code_mower/templates/product-support/code_mower_standalone_shadow.sh +151 -0
- code_mower/templates/product-support/run_claude_audit_pr.sh +32 -0
- code_mower/templates/product-support/run_codex_audit_pr.sh +32 -0
- code_mower/templates/product-support/safe_gh_comment.py +96 -0
- code_mower/templates/providers.yml +454 -0
- code_mower/templates/reviewer-spend.example.json +28 -0
- code_mower/templates/reviewer-value-report.example.md +20 -0
- code_mower/templates/workflows/private-standalone-shadow.yml.j2 +106 -0
- code_mower/templates/workflows/review-clear-stale.yml.j2 +83 -0
- code_mower/trailer_comment_labeler.py +207 -0
- code_mower/versioning.py +32 -0
- code_mower-0.5.0b5.dist-info/METADATA +302 -0
- code_mower-0.5.0b5.dist-info/RECORD +185 -0
- code_mower-0.5.0b5.dist-info/WHEEL +5 -0
- code_mower-0.5.0b5.dist-info/entry_points.txt +2 -0
- code_mower-0.5.0b5.dist-info/licenses/LICENSE +202 -0
- code_mower-0.5.0b5.dist-info/licenses/NOTICE +10 -0
- code_mower-0.5.0b5.dist-info/top_level.txt +1 -0
code_mower/package.py
ADDED
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Render the future standalone Code Mower package/install dry-run."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
import shutil
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Mapping
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _render_provider_catalog_json_fallback(data: Mapping[str, Any]) -> str:
|
|
16
|
+
return json.dumps(data, indent=2, sort_keys=True) + "\n"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if __package__ in {None, ""}:
|
|
20
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
21
|
+
|
|
22
|
+
if __package__ in {None, "", "tools"}:
|
|
23
|
+
from tools.code_mower_config import (
|
|
24
|
+
ConfigError,
|
|
25
|
+
RenderedPlan,
|
|
26
|
+
SAFE_IDENTIFIER_RE,
|
|
27
|
+
_format_issues,
|
|
28
|
+
load_config,
|
|
29
|
+
validate_config,
|
|
30
|
+
)
|
|
31
|
+
from tools.code_mower_package_paths import (
|
|
32
|
+
DEFAULT_PROVIDER_TEMPLATES,
|
|
33
|
+
_as_mapping,
|
|
34
|
+
load_provider_templates,
|
|
35
|
+
resolve_provider_templates_path,
|
|
36
|
+
)
|
|
37
|
+
else: # pragma: no cover - exercised after package extraction.
|
|
38
|
+
from .config import (
|
|
39
|
+
ConfigError,
|
|
40
|
+
RenderedPlan,
|
|
41
|
+
SAFE_IDENTIFIER_RE,
|
|
42
|
+
_format_issues,
|
|
43
|
+
load_config,
|
|
44
|
+
validate_config,
|
|
45
|
+
)
|
|
46
|
+
from .package_paths import (
|
|
47
|
+
DEFAULT_PROVIDER_TEMPLATES,
|
|
48
|
+
_as_mapping,
|
|
49
|
+
load_provider_templates,
|
|
50
|
+
resolve_provider_templates_path,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if __package__ in {None, "", "tools"}: # pragma: no cover - legacy product-local layout.
|
|
54
|
+
try:
|
|
55
|
+
from tools.code_mower_package_content import (
|
|
56
|
+
cli_commands,
|
|
57
|
+
_config_template_text,
|
|
58
|
+
_init_py_text,
|
|
59
|
+
_pyproject_text,
|
|
60
|
+
_workflow_template_text,
|
|
61
|
+
)
|
|
62
|
+
except ModuleNotFoundError:
|
|
63
|
+
from code_mower.package_content import (
|
|
64
|
+
cli_commands,
|
|
65
|
+
_config_template_text,
|
|
66
|
+
_init_py_text,
|
|
67
|
+
_pyproject_text,
|
|
68
|
+
_workflow_template_text,
|
|
69
|
+
)
|
|
70
|
+
else: # pragma: no cover - exercised after package extraction.
|
|
71
|
+
from .package_content import (
|
|
72
|
+
cli_commands,
|
|
73
|
+
_config_template_text,
|
|
74
|
+
_init_py_text,
|
|
75
|
+
_pyproject_text,
|
|
76
|
+
_workflow_template_text,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if __package__ in {None, ""}:
|
|
80
|
+
from code_mower.package_manifest import (
|
|
81
|
+
DEFAULT_PACKAGE_CONFIG,
|
|
82
|
+
DEFERRED_PACKAGE_FILES,
|
|
83
|
+
PACKAGE_FILES,
|
|
84
|
+
TEMPLATE_FILES,
|
|
85
|
+
)
|
|
86
|
+
if __package__ in {None, "", "tools"}: # pragma: no cover - legacy product-local layout.
|
|
87
|
+
try:
|
|
88
|
+
from tools.code_mower_package_manifest import (
|
|
89
|
+
DEFAULT_PACKAGE_CONFIG,
|
|
90
|
+
DEFERRED_PACKAGE_FILES,
|
|
91
|
+
PACKAGE_FILES,
|
|
92
|
+
TEMPLATE_FILES,
|
|
93
|
+
)
|
|
94
|
+
except ModuleNotFoundError:
|
|
95
|
+
from code_mower.package_manifest import (
|
|
96
|
+
DEFAULT_PACKAGE_CONFIG,
|
|
97
|
+
DEFERRED_PACKAGE_FILES,
|
|
98
|
+
PACKAGE_FILES,
|
|
99
|
+
TEMPLATE_FILES,
|
|
100
|
+
)
|
|
101
|
+
else: # pragma: no cover - exercised after package extraction.
|
|
102
|
+
from .package_manifest import (
|
|
103
|
+
DEFAULT_PACKAGE_CONFIG,
|
|
104
|
+
DEFERRED_PACKAGE_FILES,
|
|
105
|
+
PACKAGE_FILES,
|
|
106
|
+
TEMPLATE_FILES,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
if __package__ in {None, "", "tools"}: # pragma: no cover - legacy product-local layout.
|
|
110
|
+
try:
|
|
111
|
+
from tools.code_mower_package_static import STATIC_PACKAGE_FILES
|
|
112
|
+
except ModuleNotFoundError:
|
|
113
|
+
from code_mower.package_static import STATIC_PACKAGE_FILES
|
|
114
|
+
else: # pragma: no cover - exercised after package extraction.
|
|
115
|
+
from .package_static import STATIC_PACKAGE_FILES
|
|
116
|
+
|
|
117
|
+
if __package__ in {None, "", "tools"}: # pragma: no cover - legacy product-local layout.
|
|
118
|
+
try:
|
|
119
|
+
from tools.code_mower_package_rendering import _render_provider_catalog
|
|
120
|
+
except ModuleNotFoundError:
|
|
121
|
+
try:
|
|
122
|
+
from code_mower.package_rendering import _render_provider_catalog
|
|
123
|
+
except ModuleNotFoundError:
|
|
124
|
+
_render_provider_catalog = _render_provider_catalog_json_fallback
|
|
125
|
+
else: # pragma: no cover - exercised after package extraction.
|
|
126
|
+
from .package_rendering import _render_provider_catalog
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _running_code_mower_version(repo_root: Path | None = None) -> str:
|
|
130
|
+
roots = [repo_root] if repo_root is not None else []
|
|
131
|
+
roots.extend(_candidate_package_source_roots())
|
|
132
|
+
version_re = re.compile(r'__version__\s*=\s*"([^"]+)"')
|
|
133
|
+
for root in roots:
|
|
134
|
+
if root is None:
|
|
135
|
+
continue
|
|
136
|
+
init_path = root / "src/code_mower/__init__.py"
|
|
137
|
+
if not init_path.is_file():
|
|
138
|
+
continue
|
|
139
|
+
match = version_re.search(init_path.read_text(encoding="utf-8"))
|
|
140
|
+
if match:
|
|
141
|
+
return match.group(1)
|
|
142
|
+
|
|
143
|
+
loaded_package = sys.modules.get("code_mower")
|
|
144
|
+
loaded_version = getattr(loaded_package, "__version__", "")
|
|
145
|
+
if isinstance(loaded_version, str) and loaded_version:
|
|
146
|
+
return loaded_version
|
|
147
|
+
|
|
148
|
+
return "0.0.0"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def resolve_package_config_path(path_text: str, *, explicit: bool = False) -> Path:
|
|
152
|
+
path = Path(path_text)
|
|
153
|
+
if explicit or path_text != DEFAULT_PACKAGE_CONFIG or path.is_absolute():
|
|
154
|
+
return path
|
|
155
|
+
|
|
156
|
+
for root in _candidate_package_source_roots():
|
|
157
|
+
source_path = _resolve_package_source(
|
|
158
|
+
root,
|
|
159
|
+
DEFAULT_PACKAGE_CONFIG,
|
|
160
|
+
"src/code_mower/templates/code-mower.example.yml",
|
|
161
|
+
)
|
|
162
|
+
if source_path is not None:
|
|
163
|
+
return source_path
|
|
164
|
+
|
|
165
|
+
return Path.cwd() / DEFAULT_PACKAGE_CONFIG
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _provider_template_rows(
|
|
169
|
+
config: Mapping[str, Any],
|
|
170
|
+
provider_templates: Mapping[str, Any],
|
|
171
|
+
) -> list[dict[str, Any]]:
|
|
172
|
+
lanes = _as_mapping(config.get("lanes"), "lanes")
|
|
173
|
+
templates = _as_mapping(provider_templates.get("provider_templates"), "provider_templates")
|
|
174
|
+
rows: list[dict[str, Any]] = []
|
|
175
|
+
missing = sorted(set(lanes) - set(templates))
|
|
176
|
+
|
|
177
|
+
for lane_id in sorted(templates):
|
|
178
|
+
if not SAFE_IDENTIFIER_RE.fullmatch(str(lane_id)):
|
|
179
|
+
raise ConfigError(
|
|
180
|
+
"provider template lane ids must match "
|
|
181
|
+
f"[A-Za-z0-9][A-Za-z0-9_-]*: {lane_id}"
|
|
182
|
+
)
|
|
183
|
+
template = templates.get(lane_id)
|
|
184
|
+
if not isinstance(template, Mapping):
|
|
185
|
+
raise ConfigError(f"provider template for {lane_id} must be a mapping")
|
|
186
|
+
lane = lanes.get(lane_id, {})
|
|
187
|
+
if not isinstance(lane, Mapping):
|
|
188
|
+
lane = {}
|
|
189
|
+
token_env = lane.get("token_env", template.get("token_env", []))
|
|
190
|
+
token_env_any = lane.get("token_env_any", template.get("token_env_any", []))
|
|
191
|
+
review_hygiene = lane.get("review_hygiene", template.get("review_hygiene", {}))
|
|
192
|
+
if not token_env and isinstance(review_hygiene, Mapping):
|
|
193
|
+
review_token = review_hygiene.get("token_env")
|
|
194
|
+
token_env = [review_token] if review_token else []
|
|
195
|
+
rows.append(
|
|
196
|
+
{
|
|
197
|
+
"lane": str(lane_id),
|
|
198
|
+
"provider": str(lane.get("provider", template.get("provider", ""))),
|
|
199
|
+
"driver": str(lane.get("driver", template.get("driver", ""))),
|
|
200
|
+
"type": str(lane.get("type", template.get("type", ""))),
|
|
201
|
+
"adapter": lane.get("adapter", template.get("adapter")),
|
|
202
|
+
"trailer_lane": lane.get("trailer_lane", template.get("trailer_lane")),
|
|
203
|
+
"spend_policy": str(lane.get("spend_policy", template.get("spend_policy", "none"))),
|
|
204
|
+
"merge_authority": bool(lane.get("merge_authority", template.get("merge_authority", False))),
|
|
205
|
+
"informational": bool(lane.get("informational", template.get("informational", False))),
|
|
206
|
+
"enabled_by_default": lane.get(
|
|
207
|
+
"enabled_by_default",
|
|
208
|
+
template.get("enabled_by_default", True),
|
|
209
|
+
),
|
|
210
|
+
"events": list(lane.get("events", template.get("events", []))),
|
|
211
|
+
"token_env": list(token_env),
|
|
212
|
+
"token_env_any": list(token_env_any),
|
|
213
|
+
"trigger_policy": lane.get(
|
|
214
|
+
"trigger_policy",
|
|
215
|
+
template.get("trigger_policy", "label"),
|
|
216
|
+
),
|
|
217
|
+
"provider_config": lane.get(
|
|
218
|
+
"provider_config",
|
|
219
|
+
template.get("provider_config", {}),
|
|
220
|
+
),
|
|
221
|
+
"review_hygiene": review_hygiene if isinstance(review_hygiene, Mapping) else {},
|
|
222
|
+
"template_path": f"templates/providers/{lane_id}.yml",
|
|
223
|
+
}
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
if missing:
|
|
227
|
+
missing_text = ", ".join(sorted(missing))
|
|
228
|
+
raise ConfigError(f"provider templates missing configured lanes: {missing_text}")
|
|
229
|
+
return rows
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def render_package_plan(
|
|
233
|
+
config: Mapping[str, Any],
|
|
234
|
+
provider_templates: Mapping[str, Any],
|
|
235
|
+
package_name: str = "code-mower",
|
|
236
|
+
) -> RenderedPlan:
|
|
237
|
+
if not SAFE_IDENTIFIER_RE.fullmatch(package_name):
|
|
238
|
+
raise ConfigError("package name must match [A-Za-z0-9][A-Za-z0-9_-]*")
|
|
239
|
+
|
|
240
|
+
issues = validate_config(config)
|
|
241
|
+
if issues:
|
|
242
|
+
raise ConfigError(f"invalid Code Mower config:\n{_format_issues(issues)}")
|
|
243
|
+
|
|
244
|
+
template_rows = _provider_template_rows(config, provider_templates)
|
|
245
|
+
catalog_profiles = _as_mapping(provider_templates.get("profiles"), "profiles")
|
|
246
|
+
repo_profiles = _as_mapping(config.get("profiles", {}), "profiles")
|
|
247
|
+
profiles: dict[str, dict[str, Any]] = {
|
|
248
|
+
profile_id: {
|
|
249
|
+
"description": profile.get("description", ""),
|
|
250
|
+
"lanes": profile.get("lanes", []),
|
|
251
|
+
"source": "catalog",
|
|
252
|
+
}
|
|
253
|
+
for profile_id, profile in catalog_profiles.items()
|
|
254
|
+
}
|
|
255
|
+
for profile_id, profile in repo_profiles.items():
|
|
256
|
+
profiles[profile_id] = {
|
|
257
|
+
"description": profile.get(
|
|
258
|
+
"description",
|
|
259
|
+
profiles.get(profile_id, {}).get("description", "Repo-local profile."),
|
|
260
|
+
),
|
|
261
|
+
"lanes": profile.get("lanes", []),
|
|
262
|
+
"source": "repo",
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
package_files = [
|
|
266
|
+
{"source": source, "target": target, "kind": kind}
|
|
267
|
+
for source, target, kind in PACKAGE_FILES
|
|
268
|
+
]
|
|
269
|
+
deferred_package_files = [
|
|
270
|
+
{"source": source, "target": target, "reason": reason}
|
|
271
|
+
for source, target, reason in DEFERRED_PACKAGE_FILES
|
|
272
|
+
]
|
|
273
|
+
template_files = [
|
|
274
|
+
{"kind": kind, "target": target}
|
|
275
|
+
for kind, target in TEMPLATE_FILES
|
|
276
|
+
]
|
|
277
|
+
template_files.extend(
|
|
278
|
+
{"kind": "provider-template", "target": row["template_path"]}
|
|
279
|
+
for row in template_rows
|
|
280
|
+
)
|
|
281
|
+
package_version = _running_code_mower_version()
|
|
282
|
+
command_inventory = cli_commands(package_version)
|
|
283
|
+
data = {
|
|
284
|
+
"mode": "dry-run",
|
|
285
|
+
"package": {
|
|
286
|
+
"name": package_name,
|
|
287
|
+
"module": "code_mower",
|
|
288
|
+
"console_script": f"{package_name}=code_mower.cli:main",
|
|
289
|
+
"source_layout": "src/code_mower",
|
|
290
|
+
"version": package_version,
|
|
291
|
+
},
|
|
292
|
+
"package_files": package_files,
|
|
293
|
+
"deferred_package_files": deferred_package_files,
|
|
294
|
+
"template_files": template_files,
|
|
295
|
+
"provider_templates": template_rows,
|
|
296
|
+
"profiles": profiles,
|
|
297
|
+
"cli_commands": list(command_inventory),
|
|
298
|
+
"install_docs": [
|
|
299
|
+
"README.md",
|
|
300
|
+
"docs/getting-started.md",
|
|
301
|
+
"docs/package-skeleton.md",
|
|
302
|
+
"docs/package-customization.md",
|
|
303
|
+
"docs/repo-strategy.md",
|
|
304
|
+
"docs/mirror-removal-runbook.md",
|
|
305
|
+
"docs/commercial-boundary.md",
|
|
306
|
+
"docs/public-release-checklist.md",
|
|
307
|
+
"docs/github-setup.md",
|
|
308
|
+
"docs/troubleshooting.md",
|
|
309
|
+
"docs/provider-matrix.md",
|
|
310
|
+
"docs/providers.md",
|
|
311
|
+
"docs/security.md",
|
|
312
|
+
"docs/workflow-templates.md",
|
|
313
|
+
],
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
lines = [
|
|
317
|
+
"Code Mower OSS package dry-run",
|
|
318
|
+
f"Package: {data['package']['name']}",
|
|
319
|
+
f"Module: {data['package']['module']}",
|
|
320
|
+
f"Console script: {data['package']['console_script']}",
|
|
321
|
+
"",
|
|
322
|
+
"Package files to extract:",
|
|
323
|
+
]
|
|
324
|
+
lines.extend(
|
|
325
|
+
f"- {entry['source']} -> {entry['target']} [{entry['kind']}]"
|
|
326
|
+
for entry in package_files
|
|
327
|
+
)
|
|
328
|
+
lines.extend(["", "Deferred package files:"])
|
|
329
|
+
lines.extend(
|
|
330
|
+
f"- {entry['source']} -> {entry['target']} ({entry['reason']})"
|
|
331
|
+
for entry in deferred_package_files
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
lines.extend(["", "Template files to ship:"])
|
|
335
|
+
lines.extend(f"- {entry['target']} [{entry['kind']}]" for entry in template_files)
|
|
336
|
+
|
|
337
|
+
lines.extend(["", "Provider templates:"])
|
|
338
|
+
lines.extend(
|
|
339
|
+
f"- {entry['lane']}: {entry['driver']} / {entry['provider']} "
|
|
340
|
+
f"({entry['spend_policy']}) -> {entry['template_path']}"
|
|
341
|
+
for entry in template_rows
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
lines.extend(["", "CLI commands to document:"])
|
|
345
|
+
lines.extend(f"- {command}" for command in command_inventory)
|
|
346
|
+
|
|
347
|
+
return RenderedPlan(text="\n".join(lines) + "\n", data=data)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _write_text(path: Path, content: str, *, force: bool) -> None:
|
|
351
|
+
if path.exists() and not force:
|
|
352
|
+
raise ConfigError(f"refusing to overwrite existing file: {path}")
|
|
353
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
354
|
+
path.write_text(content, encoding="utf-8")
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _copy_file(source: Path, target: Path, *, force: bool) -> None:
|
|
358
|
+
if not source.is_file():
|
|
359
|
+
raise ConfigError(f"package source file does not exist: {source}")
|
|
360
|
+
if target.exists() and not force:
|
|
361
|
+
raise ConfigError(f"refusing to overwrite existing file: {target}")
|
|
362
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
363
|
+
shutil.copyfile(source, target)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _resolve_package_source(repo_root: Path, source: str, target: str) -> Path | None:
|
|
367
|
+
source_path = repo_root / source
|
|
368
|
+
if source_path.is_file():
|
|
369
|
+
return source_path
|
|
370
|
+
target_path = repo_root / target
|
|
371
|
+
if target_path.is_file():
|
|
372
|
+
return target_path
|
|
373
|
+
return None
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _missing_package_sources(repo_root: Path) -> list[str]:
|
|
377
|
+
return sorted(
|
|
378
|
+
source
|
|
379
|
+
for source, target, _ in PACKAGE_FILES
|
|
380
|
+
if _resolve_package_source(repo_root, source, target) is None
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _candidate_package_source_roots() -> list[Path]:
|
|
385
|
+
module_path = Path(__file__).resolve()
|
|
386
|
+
# Prefer the checkout that loaded this module. Fall back to cwd for
|
|
387
|
+
# installed-package runs that intentionally materialize a local checkout.
|
|
388
|
+
candidates = [
|
|
389
|
+
module_path.parents[2],
|
|
390
|
+
module_path.parents[1],
|
|
391
|
+
Path.cwd().resolve(),
|
|
392
|
+
]
|
|
393
|
+
unique: list[Path] = []
|
|
394
|
+
seen: set[Path] = set()
|
|
395
|
+
for candidate in candidates:
|
|
396
|
+
if candidate in seen:
|
|
397
|
+
continue
|
|
398
|
+
seen.add(candidate)
|
|
399
|
+
unique.append(candidate)
|
|
400
|
+
return unique
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _default_package_source_root() -> Path:
|
|
404
|
+
candidates = _candidate_package_source_roots()
|
|
405
|
+
for candidate in candidates:
|
|
406
|
+
if not _missing_package_sources(candidate):
|
|
407
|
+
return candidate
|
|
408
|
+
return candidates[0]
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _planned_materialized_targets(plan: RenderedPlan) -> list[str]:
|
|
412
|
+
targets = [target for _, target, _ in PACKAGE_FILES]
|
|
413
|
+
targets.extend(target for target, _ in STATIC_PACKAGE_FILES)
|
|
414
|
+
targets.extend(entry["target"] for entry in plan.data["template_files"])
|
|
415
|
+
targets.extend(
|
|
416
|
+
[
|
|
417
|
+
"pyproject.toml",
|
|
418
|
+
"src/code_mower/templates/providers.yml",
|
|
419
|
+
"code-mower-package-manifest.json",
|
|
420
|
+
]
|
|
421
|
+
)
|
|
422
|
+
targets.extend(
|
|
423
|
+
f"templates/providers/{row['lane']}.yml"
|
|
424
|
+
for row in plan.data["provider_templates"]
|
|
425
|
+
)
|
|
426
|
+
return sorted(set(targets))
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _preflight_output_collisions(plan: RenderedPlan, output_dir: Path) -> None:
|
|
430
|
+
collisions = [
|
|
431
|
+
target
|
|
432
|
+
for target in _planned_materialized_targets(plan)
|
|
433
|
+
if (output_dir / target).exists()
|
|
434
|
+
]
|
|
435
|
+
parent_collisions: list[str] = []
|
|
436
|
+
for target in _planned_materialized_targets(plan):
|
|
437
|
+
relative_parent = Path(target).parent
|
|
438
|
+
while str(relative_parent) not in {"", "."}:
|
|
439
|
+
parent_path = output_dir / relative_parent
|
|
440
|
+
if parent_path.exists() and not parent_path.is_dir():
|
|
441
|
+
parent_collisions.append(str(relative_parent))
|
|
442
|
+
break
|
|
443
|
+
relative_parent = relative_parent.parent
|
|
444
|
+
collisions.extend(sorted(set(parent_collisions)))
|
|
445
|
+
if not collisions:
|
|
446
|
+
return
|
|
447
|
+
sample = ", ".join(collisions[:3])
|
|
448
|
+
if len(collisions) > 3:
|
|
449
|
+
sample += f", ... ({len(collisions)} total)"
|
|
450
|
+
raise ConfigError(f"refusing to overwrite existing file(s): {sample}")
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def materialize_package_plan(
|
|
454
|
+
plan: RenderedPlan,
|
|
455
|
+
*,
|
|
456
|
+
output_dir: Path,
|
|
457
|
+
repo_root: Path | None = None,
|
|
458
|
+
force: bool = False,
|
|
459
|
+
) -> RenderedPlan:
|
|
460
|
+
"""Write the planned standalone package tree to ``output_dir``."""
|
|
461
|
+
|
|
462
|
+
repo_root = repo_root or Path.cwd()
|
|
463
|
+
missing_sources = _missing_package_sources(repo_root)
|
|
464
|
+
if missing_sources:
|
|
465
|
+
sample = ", ".join(missing_sources[:3])
|
|
466
|
+
if len(missing_sources) > 3:
|
|
467
|
+
sample += f", ... ({len(missing_sources)} total)"
|
|
468
|
+
raise ConfigError(
|
|
469
|
+
"package materialization requires the reference repository checkout; "
|
|
470
|
+
f"missing source file(s): {sample}"
|
|
471
|
+
)
|
|
472
|
+
if output_dir.exists() and not output_dir.is_dir():
|
|
473
|
+
raise ConfigError(f"output path is not a directory: {output_dir}")
|
|
474
|
+
if not force:
|
|
475
|
+
_preflight_output_collisions(plan, output_dir)
|
|
476
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
477
|
+
written: list[dict[str, str]] = []
|
|
478
|
+
package_version = _running_code_mower_version(repo_root)
|
|
479
|
+
|
|
480
|
+
for source, target, kind in PACKAGE_FILES:
|
|
481
|
+
source_path = _resolve_package_source(repo_root, source, target)
|
|
482
|
+
if source_path is None: # pragma: no cover - guarded by preflight above.
|
|
483
|
+
raise ConfigError(f"package source file does not exist: {repo_root / source}")
|
|
484
|
+
target_path = output_dir / target
|
|
485
|
+
_copy_file(source_path, target_path, force=force)
|
|
486
|
+
written.append(
|
|
487
|
+
{
|
|
488
|
+
"target": target,
|
|
489
|
+
"source": source_path.relative_to(repo_root).as_posix(),
|
|
490
|
+
"kind": kind,
|
|
491
|
+
}
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
for target, content in STATIC_PACKAGE_FILES:
|
|
495
|
+
if target == "src/code_mower/__init__.py":
|
|
496
|
+
content = _init_py_text(package_version)
|
|
497
|
+
elif target == "scripts/smoke_easy_mode.py":
|
|
498
|
+
content = content.replace(
|
|
499
|
+
"__CODE_MOWER_CONSOLE_SCRIPT__",
|
|
500
|
+
str(plan.data["package"]["name"]),
|
|
501
|
+
)
|
|
502
|
+
_write_text(output_dir / target, content, force=force)
|
|
503
|
+
written.append({"target": target, "source": "generated", "kind": "package"})
|
|
504
|
+
_write_text(
|
|
505
|
+
output_dir / "pyproject.toml",
|
|
506
|
+
_pyproject_text(str(plan.data["package"]["name"]), version=package_version),
|
|
507
|
+
force=force,
|
|
508
|
+
)
|
|
509
|
+
written.append(
|
|
510
|
+
{"target": "pyproject.toml", "source": "generated", "kind": "package"}
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
requirements_dir = output_dir / "requirements"
|
|
514
|
+
requirements_dir.mkdir(parents=True, exist_ok=True)
|
|
515
|
+
|
|
516
|
+
provider_templates = {
|
|
517
|
+
row["lane"]: {
|
|
518
|
+
key: value
|
|
519
|
+
for key, value in row.items()
|
|
520
|
+
if key not in {"lane", "template_path"}
|
|
521
|
+
}
|
|
522
|
+
for row in plan.data["provider_templates"]
|
|
523
|
+
}
|
|
524
|
+
provider_catalog = {
|
|
525
|
+
"version": 1,
|
|
526
|
+
"provider_templates": provider_templates,
|
|
527
|
+
"profiles": plan.data["profiles"],
|
|
528
|
+
}
|
|
529
|
+
catalog_target = "src/code_mower/templates/providers.yml"
|
|
530
|
+
_write_text(
|
|
531
|
+
output_dir / catalog_target,
|
|
532
|
+
_render_provider_catalog(provider_catalog),
|
|
533
|
+
force=force,
|
|
534
|
+
)
|
|
535
|
+
written.append(
|
|
536
|
+
{"target": catalog_target, "source": "generated", "kind": "provider-catalog"}
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
for entry in plan.data["template_files"]:
|
|
540
|
+
target = entry["target"]
|
|
541
|
+
kind = entry["kind"]
|
|
542
|
+
if target in {"pyproject.toml", "README.md", "MANIFEST.in", catalog_target}:
|
|
543
|
+
continue
|
|
544
|
+
if kind == "provider-template":
|
|
545
|
+
continue
|
|
546
|
+
if target == "templates/providers.yml":
|
|
547
|
+
content = _render_provider_catalog(provider_catalog)
|
|
548
|
+
elif target == "templates/code-mower.yml.j2":
|
|
549
|
+
content = _config_template_text()
|
|
550
|
+
elif target.startswith("templates/workflows/") or target.startswith("src/code_mower/templates/workflows/"):
|
|
551
|
+
content = _workflow_template_text(target)
|
|
552
|
+
else:
|
|
553
|
+
continue
|
|
554
|
+
_write_text(output_dir / target, content, force=force)
|
|
555
|
+
written.append({"target": target, "source": "generated", "kind": kind})
|
|
556
|
+
|
|
557
|
+
for row in plan.data["provider_templates"]:
|
|
558
|
+
lane = row["lane"]
|
|
559
|
+
target = f"templates/providers/{lane}.yml"
|
|
560
|
+
_write_text(
|
|
561
|
+
output_dir / target,
|
|
562
|
+
_render_provider_catalog({lane: provider_templates[lane]}),
|
|
563
|
+
force=force,
|
|
564
|
+
)
|
|
565
|
+
written.append(
|
|
566
|
+
{"target": target, "source": "generated", "kind": "provider-template"}
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
manifest = {
|
|
570
|
+
"mode": "materialize",
|
|
571
|
+
"package": plan.data["package"],
|
|
572
|
+
"output_dir": str(output_dir),
|
|
573
|
+
"files_written": written,
|
|
574
|
+
"deferred_package_files": list(plan.data["deferred_package_files"]),
|
|
575
|
+
}
|
|
576
|
+
manifest_path = output_dir / "code-mower-package-manifest.json"
|
|
577
|
+
written.append(
|
|
578
|
+
{
|
|
579
|
+
"target": "code-mower-package-manifest.json",
|
|
580
|
+
"source": "generated",
|
|
581
|
+
"kind": "manifest",
|
|
582
|
+
}
|
|
583
|
+
)
|
|
584
|
+
manifest["files_written"] = written
|
|
585
|
+
_write_text(
|
|
586
|
+
manifest_path,
|
|
587
|
+
json.dumps(manifest, indent=2, sort_keys=True) + "\n",
|
|
588
|
+
force=force,
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
lines = [
|
|
592
|
+
"Code Mower package materialized",
|
|
593
|
+
f"Output: {output_dir}",
|
|
594
|
+
f"Files written: {len(written)}",
|
|
595
|
+
"",
|
|
596
|
+
"Deferred package files:",
|
|
597
|
+
]
|
|
598
|
+
if manifest["deferred_package_files"]:
|
|
599
|
+
lines.extend(
|
|
600
|
+
f"- {entry['source']} ({entry['reason']})"
|
|
601
|
+
for entry in manifest["deferred_package_files"]
|
|
602
|
+
)
|
|
603
|
+
else:
|
|
604
|
+
lines.append("- none")
|
|
605
|
+
|
|
606
|
+
return RenderedPlan(text="\n".join(lines) + "\n", data=manifest)
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def main(argv: list[str] | None = None) -> int:
|
|
610
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
611
|
+
parser.add_argument("config", nargs="?")
|
|
612
|
+
parser.add_argument(
|
|
613
|
+
"--provider-templates",
|
|
614
|
+
default=None,
|
|
615
|
+
help="provider template catalog to include in the package plan",
|
|
616
|
+
)
|
|
617
|
+
parser.add_argument("--package-name", default="code-mower")
|
|
618
|
+
parser.add_argument("--dry-run", action="store_true", help="render the package plan")
|
|
619
|
+
parser.add_argument(
|
|
620
|
+
"--output-dir",
|
|
621
|
+
type=Path,
|
|
622
|
+
default=None,
|
|
623
|
+
help="materialize the package tree into this directory",
|
|
624
|
+
)
|
|
625
|
+
parser.add_argument(
|
|
626
|
+
"--force",
|
|
627
|
+
action="store_true",
|
|
628
|
+
help="overwrite existing files under --output-dir",
|
|
629
|
+
)
|
|
630
|
+
parser.add_argument("--json", action="store_true", help="emit package plan as JSON")
|
|
631
|
+
args = parser.parse_args(argv)
|
|
632
|
+
|
|
633
|
+
if not args.dry_run and args.output_dir is None:
|
|
634
|
+
print("error: pass --dry-run or --output-dir", file=sys.stderr)
|
|
635
|
+
return 1
|
|
636
|
+
if args.dry_run and args.output_dir is not None:
|
|
637
|
+
print("error: --dry-run and --output-dir are mutually exclusive", file=sys.stderr)
|
|
638
|
+
return 1
|
|
639
|
+
|
|
640
|
+
try:
|
|
641
|
+
config_path = resolve_package_config_path(
|
|
642
|
+
args.config or DEFAULT_PACKAGE_CONFIG,
|
|
643
|
+
explicit=args.config is not None,
|
|
644
|
+
)
|
|
645
|
+
plan = render_package_plan(
|
|
646
|
+
load_config(config_path),
|
|
647
|
+
load_provider_templates(
|
|
648
|
+
resolve_provider_templates_path(DEFAULT_PROVIDER_TEMPLATES)
|
|
649
|
+
if args.provider_templates is None
|
|
650
|
+
else Path(args.provider_templates)
|
|
651
|
+
),
|
|
652
|
+
package_name=args.package_name,
|
|
653
|
+
)
|
|
654
|
+
if args.output_dir is not None:
|
|
655
|
+
plan = materialize_package_plan(
|
|
656
|
+
plan,
|
|
657
|
+
output_dir=args.output_dir,
|
|
658
|
+
repo_root=_default_package_source_root(),
|
|
659
|
+
force=args.force,
|
|
660
|
+
)
|
|
661
|
+
except ConfigError as exc:
|
|
662
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
663
|
+
return 1
|
|
664
|
+
|
|
665
|
+
if args.json:
|
|
666
|
+
print(json.dumps(plan.data, indent=2, sort_keys=True))
|
|
667
|
+
else:
|
|
668
|
+
print(plan.text, end="")
|
|
669
|
+
return 0
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
if __name__ == "__main__":
|
|
673
|
+
raise SystemExit(main())
|