gwc-pybundle 1.4.5__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.

Potentially problematic release.


This version of gwc-pybundle might be problematic. Click here for more details.

Files changed (55) hide show
  1. gwc_pybundle-1.4.5.dist-info/METADATA +876 -0
  2. gwc_pybundle-1.4.5.dist-info/RECORD +55 -0
  3. gwc_pybundle-1.4.5.dist-info/WHEEL +5 -0
  4. gwc_pybundle-1.4.5.dist-info/entry_points.txt +2 -0
  5. gwc_pybundle-1.4.5.dist-info/licenses/LICENSE.md +25 -0
  6. gwc_pybundle-1.4.5.dist-info/top_level.txt +1 -0
  7. pybundle/__init__.py +0 -0
  8. pybundle/__main__.py +4 -0
  9. pybundle/cli.py +365 -0
  10. pybundle/context.py +362 -0
  11. pybundle/doctor.py +148 -0
  12. pybundle/filters.py +178 -0
  13. pybundle/manifest.py +77 -0
  14. pybundle/packaging.py +45 -0
  15. pybundle/policy.py +132 -0
  16. pybundle/profiles.py +340 -0
  17. pybundle/roadmap_model.py +42 -0
  18. pybundle/roadmap_scan.py +295 -0
  19. pybundle/root_detect.py +14 -0
  20. pybundle/runner.py +163 -0
  21. pybundle/steps/__init__.py +26 -0
  22. pybundle/steps/bandit.py +72 -0
  23. pybundle/steps/base.py +20 -0
  24. pybundle/steps/compileall.py +76 -0
  25. pybundle/steps/context_expand.py +272 -0
  26. pybundle/steps/copy_pack.py +293 -0
  27. pybundle/steps/coverage.py +101 -0
  28. pybundle/steps/cprofile_step.py +155 -0
  29. pybundle/steps/dependency_sizes.py +120 -0
  30. pybundle/steps/duplication.py +94 -0
  31. pybundle/steps/error_refs.py +204 -0
  32. pybundle/steps/handoff_md.py +167 -0
  33. pybundle/steps/import_time.py +165 -0
  34. pybundle/steps/interrogate.py +84 -0
  35. pybundle/steps/license_scan.py +96 -0
  36. pybundle/steps/line_profiler.py +108 -0
  37. pybundle/steps/memory_profile.py +173 -0
  38. pybundle/steps/mutation_testing.py +136 -0
  39. pybundle/steps/mypy.py +60 -0
  40. pybundle/steps/pip_audit.py +45 -0
  41. pybundle/steps/pipdeptree.py +61 -0
  42. pybundle/steps/pylance.py +562 -0
  43. pybundle/steps/pytest.py +66 -0
  44. pybundle/steps/radon.py +121 -0
  45. pybundle/steps/repro_md.py +161 -0
  46. pybundle/steps/rg_scans.py +78 -0
  47. pybundle/steps/roadmap.py +153 -0
  48. pybundle/steps/ruff.py +111 -0
  49. pybundle/steps/shell.py +74 -0
  50. pybundle/steps/slow_tests.py +170 -0
  51. pybundle/steps/test_flakiness.py +172 -0
  52. pybundle/steps/tree.py +116 -0
  53. pybundle/steps/unused_deps.py +112 -0
  54. pybundle/steps/vulture.py +83 -0
  55. pybundle/tools.py +63 -0
@@ -0,0 +1,55 @@
1
+ gwc_pybundle-1.4.5.dist-info/licenses/LICENSE.md,sha256=ZmD484KG9hysmSMFT824y7aIc8lhFBnjkN-3DJNjXCc,1108
2
+ pybundle/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ pybundle/__main__.py,sha256=MHKZ_ae3fSLGTLUUMOx15fWdeOnJSHhq-zslRP5F5Lc,79
4
+ pybundle/cli.py,sha256=Fg8Jwv7PWNHnsdA46x2OVWNO7kz6SZlQ5xfU5XRTGfw,14544
5
+ pybundle/context.py,sha256=j2m3v1n8SCIs6vw69fqzUuSbT0iSf805KXolqpVccac,12013
6
+ pybundle/doctor.py,sha256=6tpEH5_UVKA8Wbi5kKkUoxXMjTGOrEZr3leBd4pU3Lw,4669
7
+ pybundle/filters.py,sha256=gKluTcoi6g0vt40m8sMbqmT-W2XBo_X7slW0CCAQI_o,3040
8
+ pybundle/manifest.py,sha256=LJB1JXg5kH6nKjgzif-sDWoaaqmIEkF3ctlbHqE0EHQ,2289
9
+ pybundle/packaging.py,sha256=CQ394kUmn6sudpy7ExGpmeanfRVEf_xxwrX_swaotFg,1407
10
+ pybundle/policy.py,sha256=PLk2Fs1hBxfn0jAWE3Jcj1jhlUnwfD5ivOFrUScI8Io,3929
11
+ pybundle/profiles.py,sha256=D_kOG0z477l6h6n5YifWZpuQIP6u4z2JrPKqn_CaHs0,10673
12
+ pybundle/roadmap_model.py,sha256=rcMwphd3uNiwzAVx_tGTE20GYYFRzyvbCoFcYGRS9aY,991
13
+ pybundle/roadmap_scan.py,sha256=ogS7j9pVIhqk_1u7eTBTUDjUGupZdU2RYWy1KcUPrgE,9613
14
+ pybundle/root_detect.py,sha256=d_HdA_usPxK1orc9cnNTY_fZrvdScQnI_f1XvvlHSFA,361
15
+ pybundle/runner.py,sha256=pw0T3QhbIl7cR7xyNcrm48IToE2b8NB8ZVFvtOEjyzw,5220
16
+ pybundle/tools.py,sha256=dLg6m7tW11QC6SoJUzoayYEI-lq_kKuOm2eN2uzqnQo,1926
17
+ pybundle/steps/__init__.py,sha256=3NL5A0Oj4MlTPW7LvdzesrLHMJG98nV-qrcaJuICE4o,394
18
+ pybundle/steps/bandit.py,sha256=4gT9JJerhT9W8O6lF_Vwk6iNjYvJqQcihVcF816NzS4,2439
19
+ pybundle/steps/base.py,sha256=9LZwPYlGAWfnIe5DRbfP89ZmMLhbmOm9SQ-vWmQANHA,356
20
+ pybundle/steps/compileall.py,sha256=uB_eFVq1FSSrdzZvx9hgs4IbNJnrXzPOCjyTfDw0gsc,2446
21
+ pybundle/steps/context_expand.py,sha256=pG1-59RqQFIdTlMj5CJ65b8Or9qNk8LgudntdT10m7M,7986
22
+ pybundle/steps/copy_pack.py,sha256=_I_hDBLiG9f9ETFzpG59lOFhe7nOasGPRvQb4CunBlE,9778
23
+ pybundle/steps/coverage.py,sha256=HIMuQH8PFPeoo_bPsIhn3U2UHzes5qiQIyJRqRnz0yI,3600
24
+ pybundle/steps/cprofile_step.py,sha256=z8KjKb2tydA0z5fnq6eZSJolpiWEld7NnlNQOSfj7AY,5755
25
+ pybundle/steps/dependency_sizes.py,sha256=oAzqUCmD0f7eOJfxE_S_ARgTszKEOJHSP5i-Z6aP0MI,5207
26
+ pybundle/steps/duplication.py,sha256=NmeNXXcEUYnlJWSCcilwb4K9NhTsGBvWJf4EoZZ3tBI,3304
27
+ pybundle/steps/error_refs.py,sha256=mq1DGXREQRudycyPb7H0obkHmv3HvO-QPPbf2zoZDu0,5640
28
+ pybundle/steps/handoff_md.py,sha256=TOv9IEqrss60ZKHlCx7XEzq_FgSZ38jr2sT3Nr7AwNM,6038
29
+ pybundle/steps/import_time.py,sha256=tEoybXsLYAYZ2iQjhsg6dN-VH37utsOThvHP-xwZ16k,6394
30
+ pybundle/steps/interrogate.py,sha256=la_3zy1gulqg382cNI3zFq4obH0OQyMZJQ9_FbCXIzU,2872
31
+ pybundle/steps/license_scan.py,sha256=rEigwhFC80btqAfecL2mFKyBxQkYP19FVESRXmaJaBY,3640
32
+ pybundle/steps/line_profiler.py,sha256=8RDjv8AE-xmnlPZzGXYEaOaLoJ2KlL8V8d_rrUJ7V94,4141
33
+ pybundle/steps/memory_profile.py,sha256=a8Iq3zdDstDrwWY4JmsIeIQV1JVKm5Pbx-D7OZw6AX4,5359
34
+ pybundle/steps/mutation_testing.py,sha256=MIHRrX6PrIfg9Aeal81Qe-G5pKMCPn0kGSbWuykVZUY,5417
35
+ pybundle/steps/mypy.py,sha256=QykLCT5XkQOJHHRxj4VAaPuCycoQOFymj-Xhz8cuc1c,1991
36
+ pybundle/steps/pip_audit.py,sha256=qNkHeyjzUFJ9SSKU4jTKOsUnWnNYAk_jWcmPlUckkAA,1582
37
+ pybundle/steps/pipdeptree.py,sha256=ZIBAC_qEBE_iUh8bX6ogYQVRCSB0blfiCmsGh4gFUbU,2150
38
+ pybundle/steps/pylance.py,sha256=PiIyyYm8gTVv-dihzRjvD4waZnz31qNOEAjYIlscvSY,15715
39
+ pybundle/steps/pytest.py,sha256=vfr2DI6aYCTit_j9QdV3WtKlOZtLMCCSm2E__3tsZt4,2177
40
+ pybundle/steps/radon.py,sha256=2gi_MQibkHhE1y9Bm3kpirlJc5SPQpHgI0yHieQ6VFo,4072
41
+ pybundle/steps/repro_md.py,sha256=EvoNVKaHFelfrv_jfBLpbkORTiDSjah3GgWXW3NvEXY,4856
42
+ pybundle/steps/rg_scans.py,sha256=hcHhzPAJ_MdPthRLcfIzxlkXoQJkDF2bIX2wU1By2gU,2511
43
+ pybundle/steps/roadmap.py,sha256=gFS2Mo9CR1A8wg6xNVP_FnJuT_dm8yWjgRJbtlyN2wk,5213
44
+ pybundle/steps/ruff.py,sha256=xrKSn5QBQ5sLBKW5TQCORANlvlh1eHdikOhxJP2gO54,3815
45
+ pybundle/steps/shell.py,sha256=vYGu9fZ5gDdZnKSfok95bOTIWu5HNvS23atsY5FwtJE,2529
46
+ pybundle/steps/slow_tests.py,sha256=3Gc1h4JB_41yjWthwVaIVMP3flm5xaCyQ94Nw86IB1o,7101
47
+ pybundle/steps/test_flakiness.py,sha256=5W8OGCJT1VsLu3jVcv55-CdtgdCYhk20jBCA9xvTpxc,7283
48
+ pybundle/steps/tree.py,sha256=tz5Z7clOEuKWMW6wjj2xtEu3pNjEto8dYgHYaoDdM0E,3674
49
+ pybundle/steps/unused_deps.py,sha256=L6n0fyu-p2lQkWjMKe8I5Ad5JMkou3muNU2qtqPOgOg,4870
50
+ pybundle/steps/vulture.py,sha256=-3964YRxqoQ-zRrAN6pS0HNlVeUzw-z75JZdbELavEw,2942
51
+ gwc_pybundle-1.4.5.dist-info/METADATA,sha256=iKF1bpXHiM9G1vvPyP2exuQcikA0cZjKR8W5glbF81g,25610
52
+ gwc_pybundle-1.4.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
53
+ gwc_pybundle-1.4.5.dist-info/entry_points.txt,sha256=xgXfIvyx9ZI6jd9MscFWNMzQrcX4scTVJ9L7WwksUHg,47
54
+ gwc_pybundle-1.4.5.dist-info/top_level.txt,sha256=N5x3QutDUtHQn3HwkFmH5PeM9uPY-E5wOKUJSE8PBKM,9
55
+ gwc_pybundle-1.4.5.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pybundle = pybundle.cli:main
@@ -0,0 +1,25 @@
1
+ The MIT License (MIT)
2
+ =====================
3
+
4
+ Copyright © 2025 Jessica Brown
5
+
6
+ Permission is hereby granted, free of charge, to any person
7
+ obtaining a copy of this software and associated documentation
8
+ files (the “Software”), to deal in the Software without
9
+ restriction, including without limitation the rights to use,
10
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the
12
+ Software is furnished to do so, subject to the following
13
+ conditions:
14
+
15
+ The above copyright notice and this permission notice shall be
16
+ included in all copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
19
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
20
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
22
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
23
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
25
+ OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1 @@
1
+ pybundle
pybundle/__init__.py ADDED
File without changes
pybundle/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
pybundle/cli.py ADDED
@@ -0,0 +1,365 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+ import shlex
6
+
7
+ from .context import BundleContext, RunOptions
8
+ from .profiles import get_profile
9
+ from .root_detect import detect_project_root
10
+ from importlib.metadata import PackageNotFoundError, version as pkg_version
11
+ from .runner import run_profile
12
+
13
+
14
+ def get_version() -> str:
15
+ # 1) Canonical for installed distributions (including editable)
16
+ try:
17
+ return pkg_version("gwc-pybundle")
18
+ except PackageNotFoundError:
19
+ pass
20
+
21
+ # 2) Dev fallback: locate pyproject.toml by walking up from this file
22
+ try:
23
+ import tomllib # py3.11+
24
+ except Exception:
25
+ return "unknown"
26
+
27
+ here = Path(__file__).resolve()
28
+ for parent in [here.parent] + list(here.parents):
29
+ pp = parent / "pyproject.toml"
30
+ if pp.is_file():
31
+ try:
32
+ data = tomllib.loads(pp.read_text(encoding="utf-8"))
33
+ return data.get("project", {}).get("version", "unknown")
34
+ except Exception:
35
+ return "unknown"
36
+
37
+ return "unknown"
38
+
39
+
40
+ def add_common_args(sp: argparse.ArgumentParser) -> None:
41
+ sp.add_argument(
42
+ "--project-root",
43
+ type=Path,
44
+ default=None,
45
+ help="Explicit project root (skip auto-detect)",
46
+ )
47
+ sp.add_argument(
48
+ "--outdir",
49
+ type=Path,
50
+ default=None,
51
+ help="Output directory (default: <root>/artifacts)",
52
+ )
53
+ sp.add_argument("--name", default=None, help="Override archive name prefix")
54
+ sp.add_argument(
55
+ "--strict", action="store_true", help="Fail non-zero if any step fails"
56
+ )
57
+ sp.add_argument(
58
+ "--redact",
59
+ action=argparse.BooleanOptionalAction,
60
+ default=True,
61
+ help="Redact secrets in logs/text",
62
+ )
63
+ sp.add_argument(
64
+ "--json",
65
+ action="store_true",
66
+ help="Emit machine-readable JSON to stdout.",
67
+ )
68
+
69
+
70
+ def _resolve_profile_defaults(profile: str, o: RunOptions) -> RunOptions:
71
+ if profile == "ai":
72
+ # AI defaults: skip slow/flake-prone tools unless explicitly enabled
73
+ return RunOptions(
74
+ **{
75
+ **o.__dict__,
76
+ "no_ruff": o.no_ruff if o.no_ruff is not None else True,
77
+ "no_mypy": o.no_mypy if o.no_mypy is not None else True,
78
+ "no_pytest": o.no_pytest if o.no_pytest is not None else True,
79
+ "no_rg": o.no_rg if o.no_rg is not None else True,
80
+ "no_error_refs": o.no_error_refs
81
+ if o.no_error_refs is not None
82
+ else True,
83
+ "no_context": o.no_context if o.no_context is not None else True,
84
+ }
85
+ )
86
+ return o
87
+
88
+
89
+ def add_run_only_args(sp: argparse.ArgumentParser) -> None:
90
+ sp.add_argument(
91
+ "--format",
92
+ choices=["auto", "zip", "tar.gz"],
93
+ default="auto",
94
+ help="Archive format",
95
+ )
96
+ sp.add_argument(
97
+ "--clean-workdir",
98
+ action="store_true",
99
+ help="Delete expanded workdir after packaging",
100
+ )
101
+
102
+
103
+ def add_knobs(sp: argparse.ArgumentParser) -> None:
104
+ # selective skips
105
+ sp.add_argument("--ruff", dest="no_ruff", action="store_false", default=None)
106
+ sp.add_argument("--no-ruff", dest="no_ruff", action="store_true", default=None)
107
+ sp.add_argument("--mypy", dest="no_mypy", action="store_false", default=None)
108
+ sp.add_argument("--no-mypy", dest="no_mypy", action="store_true", default=None)
109
+ sp.add_argument("--pylance", dest="no_pylance", action="store_false", default=None)
110
+ sp.add_argument(
111
+ "--no-pylance", dest="no_pylance", action="store_true", default=None
112
+ )
113
+ sp.add_argument("--pytest", dest="no_pytest", action="store_false", default=None)
114
+ sp.add_argument("--no-pytest", dest="no_pytest", action="store_true", default=None)
115
+ sp.add_argument("--bandit", dest="no_bandit", action="store_false", default=None)
116
+ sp.add_argument("--no-bandit", dest="no_bandit", action="store_true", default=None)
117
+ sp.add_argument(
118
+ "--pip-audit", dest="no_pip_audit", action="store_false", default=None
119
+ )
120
+ sp.add_argument(
121
+ "--no-pip-audit", dest="no_pip_audit", action="store_true", default=None
122
+ )
123
+ sp.add_argument(
124
+ "--coverage", dest="no_coverage", action="store_false", default=None
125
+ )
126
+ sp.add_argument(
127
+ "--no-coverage", dest="no_coverage", action="store_true", default=None
128
+ )
129
+ sp.add_argument("--rg", dest="no_rg", action="store_false", default=None)
130
+ sp.add_argument("--no-rg", dest="no_rg", action="store_true", default=None)
131
+ sp.add_argument(
132
+ "--error-refs", dest="no_error_refs", action="store_false", default=None
133
+ )
134
+ sp.add_argument(
135
+ "--no-error-refs", dest="no_error_refs", action="store_true", default=None
136
+ )
137
+ sp.add_argument("--context", dest="no_context", action="store_false", default=None)
138
+ sp.add_argument(
139
+ "--no-context", dest="no_context", action="store_true", default=None
140
+ )
141
+
142
+ # code quality tools (v1.3.0)
143
+ sp.add_argument("--vulture", dest="no_vulture", action="store_false", default=None)
144
+ sp.add_argument("--no-vulture", dest="no_vulture", action="store_true", default=None)
145
+ sp.add_argument("--radon", dest="no_radon", action="store_false", default=None)
146
+ sp.add_argument("--no-radon", dest="no_radon", action="store_true", default=None)
147
+ sp.add_argument("--interrogate", dest="no_interrogate", action="store_false", default=None)
148
+ sp.add_argument("--no-interrogate", dest="no_interrogate", action="store_true", default=None)
149
+ sp.add_argument("--duplication", dest="no_duplication", action="store_false", default=None)
150
+ sp.add_argument("--no-duplication", dest="no_duplication", action="store_true", default=None)
151
+
152
+ # dependency analysis tools (v1.3.1)
153
+ sp.add_argument("--pipdeptree", dest="no_pipdeptree", action="store_false", default=None)
154
+ sp.add_argument("--no-pipdeptree", dest="no_pipdeptree", action="store_true", default=None)
155
+ sp.add_argument("--unused-deps", dest="no_unused_deps", action="store_false", default=None)
156
+ sp.add_argument("--no-unused-deps", dest="no_unused_deps", action="store_true", default=None)
157
+ sp.add_argument("--license-scan", dest="no_license_scan", action="store_false", default=None)
158
+ sp.add_argument("--no-license-scan", dest="no_license_scan", action="store_true", default=None)
159
+ sp.add_argument("--dependency-sizes", dest="no_dependency_sizes", action="store_false", default=None)
160
+ sp.add_argument("--no-dependency-sizes", dest="no_dependency_sizes", action="store_true", default=None)
161
+
162
+ # performance profiling (v1.4.0)
163
+ sp.add_argument("--profile", dest="no_profile", action="store_false", default=None)
164
+ sp.add_argument("--no-profile", dest="no_profile", action="store_true", default=None)
165
+ sp.add_argument("--profile-entry-point", type=str, default=None,
166
+ help="Entry point for profiling (e.g., main.py or tests/)")
167
+ sp.add_argument("--profile-memory", action="store_true", default=False,
168
+ help="Enable memory profiling with tracemalloc")
169
+ sp.add_argument("--enable-line-profiler", action="store_true", default=False,
170
+ help="Enable line_profiler (requires @profile decorators)")
171
+
172
+ # test quality & coverage (v1.4.1)
173
+ sp.add_argument("--test-flakiness-runs", type=int, default=3,
174
+ help="Number of times to run tests for flakiness detection (default: 3)")
175
+ sp.add_argument("--slow-test-threshold", type=float, default=1.0,
176
+ help="Threshold in seconds for identifying slow tests (default: 1.0)")
177
+ sp.add_argument("--mutation", dest="enable_mutation_testing", action="store_true", default=False,
178
+ help="Enable mutation testing with mutmut (VERY SLOW!)")
179
+ sp.add_argument("--no-mutation", dest="enable_mutation_testing", action="store_false", default=False,
180
+ help="Disable mutation testing (default)")
181
+
182
+ # Security options
183
+ sp.add_argument(
184
+ "--strict-paths",
185
+ action="store_true",
186
+ help="Only use tools from trusted system directories (enhanced security)",
187
+ )
188
+
189
+ # targets / args
190
+ sp.add_argument("--ruff-target", default=".")
191
+ sp.add_argument("--mypy-target", default=".")
192
+ sp.add_argument(
193
+ "--pytest-args",
194
+ default="-q",
195
+ help='Pytest args as a single string, e.g. "--maxfail=1 -q"',
196
+ )
197
+
198
+ # caps
199
+ sp.add_argument("--error-max-files", type=int, default=250)
200
+ sp.add_argument("--context-depth", type=int, default=2)
201
+ sp.add_argument("--context-max-files", type=int, default=600)
202
+
203
+
204
+ def build_parser() -> argparse.ArgumentParser:
205
+ p = argparse.ArgumentParser(
206
+ prog="pybundle", description="Build portable diagnostic bundles for projects."
207
+ )
208
+ sub = p.add_subparsers(dest="cmd", required=True)
209
+ sub.add_parser("version", help="Show version")
210
+ sub.add_parser("list-profiles", help="List available profiles")
211
+
212
+ runp = sub.add_parser("run", help="Run a profile and build an archive")
213
+ runp.add_argument("profile", choices=["analysis", "debug", "backup", "ai"])
214
+ add_common_args(runp)
215
+ add_run_only_args(runp)
216
+ add_knobs(runp)
217
+
218
+ docp = sub.add_parser("doctor", help="Show tool availability and what would run")
219
+ docp.add_argument(
220
+ "profile",
221
+ choices=["analysis", "debug", "backup", "ai"],
222
+ nargs="?",
223
+ default="analysis",
224
+ )
225
+ add_common_args(docp)
226
+ add_knobs(docp)
227
+
228
+ return p
229
+
230
+
231
+ def _build_options(args) -> RunOptions:
232
+ pytest_args = (
233
+ shlex.split(args.pytest_args) if getattr(args, "pytest_args", None) else ["-q"]
234
+ )
235
+ return RunOptions(
236
+ no_ruff=getattr(args, "no_ruff", None),
237
+ no_mypy=getattr(args, "no_mypy", None),
238
+ no_pylance=getattr(args, "no_pylance", None),
239
+ no_pytest=getattr(args, "no_pytest", None),
240
+ no_bandit=getattr(args, "no_bandit", None),
241
+ no_pip_audit=getattr(args, "no_pip_audit", None),
242
+ no_coverage=getattr(args, "no_coverage", None),
243
+ no_rg=getattr(args, "no_rg", None),
244
+ no_error_refs=getattr(args, "no_error_refs", None),
245
+ no_context=getattr(args, "no_context", None),
246
+ no_compileall=getattr(args, "no_compileall", None),
247
+ no_vulture=getattr(args, "no_vulture", None),
248
+ no_radon=getattr(args, "no_radon", None),
249
+ no_interrogate=getattr(args, "no_interrogate", None),
250
+ no_duplication=getattr(args, "no_duplication", None),
251
+ no_pipdeptree=getattr(args, "no_pipdeptree", None),
252
+ no_unused_deps=getattr(args, "no_unused_deps", None),
253
+ no_license_scan=getattr(args, "no_license_scan", None),
254
+ no_dependency_sizes=getattr(args, "no_dependency_sizes", None),
255
+ no_profile=getattr(args, "no_profile", None),
256
+ profile_entry_point=getattr(args, "profile_entry_point", None),
257
+ profile_memory=getattr(args, "profile_memory", False),
258
+ enable_line_profiler=getattr(args, "enable_line_profiler", False),
259
+ test_flakiness_runs=getattr(args, "test_flakiness_runs", 3),
260
+ slow_test_threshold=getattr(args, "slow_test_threshold", 1.0),
261
+ enable_mutation_testing=getattr(args, "enable_mutation_testing", False),
262
+ strict_paths=getattr(args, "strict_paths", False),
263
+ ruff_target=getattr(args, "ruff_target", "."),
264
+ mypy_target=getattr(args, "mypy_target", "."),
265
+ pytest_args=pytest_args,
266
+ error_max_files=getattr(args, "error_max_files", 250),
267
+ context_depth=getattr(args, "context_depth", 2),
268
+ context_max_files=getattr(args, "context_max_files", 600),
269
+ )
270
+
271
+
272
+ def main(argv: list[str] | None = None) -> int:
273
+ args = build_parser().parse_args(argv)
274
+
275
+ if args.cmd == "version":
276
+ print(f"pybundle {get_version()}")
277
+ return 0
278
+
279
+ if args.cmd == "list-profiles":
280
+ print("ai - AI-friendly context bundle (fast, low-flake defaults)")
281
+ print("backup - portable snapshot (scaffold)")
282
+ print("analysis - neutral diagnostic bundle (humans, tools, assistants)")
283
+ print("debug - deeper diagnostics for developers")
284
+ return 0
285
+
286
+ # run + doctor need a root
287
+ root = args.project_root or detect_project_root(Path.cwd())
288
+ if root is None:
289
+ print("❌ Could not detect project root. Use --project-root PATH.")
290
+ return 20
291
+
292
+ outdir = args.outdir or (root / "artifacts")
293
+
294
+ options = _resolve_profile_defaults(args.profile, _build_options(args))
295
+ profile = get_profile(args.profile, options)
296
+
297
+ if args.cmd == "doctor":
298
+ ctx = BundleContext.create(
299
+ root=root,
300
+ outdir=outdir,
301
+ profile_name=args.profile,
302
+ archive_format="auto",
303
+ name_prefix=args.name,
304
+ strict=args.strict,
305
+ redact=args.redact,
306
+ json_mode=args.json,
307
+ keep_workdir=True,
308
+ options=options,
309
+ )
310
+
311
+ if args.json:
312
+ ctx.emit_json(ctx.doctor_report(profile))
313
+ else:
314
+ ctx.print_doctor(profile)
315
+ return 0
316
+
317
+ # cmd == run
318
+ keep_workdir = not args.clean_workdir
319
+
320
+ ctx = BundleContext.create(
321
+ root=root,
322
+ outdir=outdir,
323
+ profile_name=args.profile,
324
+ archive_format=args.format,
325
+ name_prefix=args.name,
326
+ strict=args.strict,
327
+ redact=args.redact,
328
+ json_mode=args.json,
329
+ keep_workdir=keep_workdir,
330
+ options=options,
331
+ )
332
+
333
+ rc = run_profile(ctx, profile)
334
+
335
+ if args.json:
336
+ copied = None
337
+ excluded = None
338
+
339
+ mf = ctx.metadir / "50_copy_manifest.txt"
340
+ if mf.exists():
341
+ data: dict[str, str] = {}
342
+ for line in mf.read_text(encoding="utf-8").splitlines():
343
+ if "=" in line:
344
+ k, v = line.split("=", 1)
345
+ data[k.strip()] = v.strip()
346
+
347
+ copied = int(data.get("copied_files", "0"))
348
+ excluded = int(data.get("excluded_files", "0"))
349
+
350
+ payload = {
351
+ "status": "ok" if rc == 0 else "fail",
352
+ "command": "run",
353
+ "profile": profile.name,
354
+ "files_included": copied if copied is not None else 0,
355
+ "files_excluded": excluded if excluded is not None else 0,
356
+ "duration_ms": ctx.duration_ms or 0,
357
+ "bundle_path": str(ctx.archive_path) if ctx.archive_path else None,
358
+ }
359
+ ctx.emit_json(payload)
360
+
361
+ return rc
362
+
363
+
364
+ if __name__ == "__main__":
365
+ raise SystemExit(main())