gwc-pybundle 2.1.2__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 (82) hide show
  1. gwc_pybundle-2.1.2.dist-info/METADATA +903 -0
  2. gwc_pybundle-2.1.2.dist-info/RECORD +82 -0
  3. gwc_pybundle-2.1.2.dist-info/WHEEL +5 -0
  4. gwc_pybundle-2.1.2.dist-info/entry_points.txt +2 -0
  5. gwc_pybundle-2.1.2.dist-info/licenses/LICENSE.md +25 -0
  6. gwc_pybundle-2.1.2.dist-info/top_level.txt +1 -0
  7. pybundle/__init__.py +0 -0
  8. pybundle/__main__.py +4 -0
  9. pybundle/cli.py +546 -0
  10. pybundle/context.py +404 -0
  11. pybundle/doctor.py +148 -0
  12. pybundle/filters.py +228 -0
  13. pybundle/manifest.py +77 -0
  14. pybundle/packaging.py +45 -0
  15. pybundle/policy.py +132 -0
  16. pybundle/profiles.py +454 -0
  17. pybundle/roadmap_model.py +42 -0
  18. pybundle/roadmap_scan.py +328 -0
  19. pybundle/root_detect.py +14 -0
  20. pybundle/runner.py +180 -0
  21. pybundle/steps/__init__.py +26 -0
  22. pybundle/steps/ai_context.py +791 -0
  23. pybundle/steps/api_docs.py +219 -0
  24. pybundle/steps/asyncio_analysis.py +358 -0
  25. pybundle/steps/bandit.py +72 -0
  26. pybundle/steps/base.py +20 -0
  27. pybundle/steps/blocking_call_detection.py +291 -0
  28. pybundle/steps/call_graph.py +219 -0
  29. pybundle/steps/compileall.py +76 -0
  30. pybundle/steps/config_docs.py +319 -0
  31. pybundle/steps/config_validation.py +302 -0
  32. pybundle/steps/container_image.py +294 -0
  33. pybundle/steps/context_expand.py +272 -0
  34. pybundle/steps/copy_pack.py +293 -0
  35. pybundle/steps/coverage.py +101 -0
  36. pybundle/steps/cprofile_step.py +166 -0
  37. pybundle/steps/dependency_sizes.py +136 -0
  38. pybundle/steps/django_checks.py +214 -0
  39. pybundle/steps/dockerfile_lint.py +282 -0
  40. pybundle/steps/dockerignore.py +311 -0
  41. pybundle/steps/duplication.py +103 -0
  42. pybundle/steps/env_completeness.py +269 -0
  43. pybundle/steps/env_var_usage.py +253 -0
  44. pybundle/steps/error_refs.py +204 -0
  45. pybundle/steps/event_loop_patterns.py +280 -0
  46. pybundle/steps/exception_patterns.py +190 -0
  47. pybundle/steps/fastapi_integration.py +250 -0
  48. pybundle/steps/flask_debugging.py +312 -0
  49. pybundle/steps/git_analytics.py +315 -0
  50. pybundle/steps/handoff_md.py +176 -0
  51. pybundle/steps/import_time.py +175 -0
  52. pybundle/steps/interrogate.py +106 -0
  53. pybundle/steps/license_scan.py +96 -0
  54. pybundle/steps/line_profiler.py +117 -0
  55. pybundle/steps/link_validation.py +287 -0
  56. pybundle/steps/logging_analysis.py +233 -0
  57. pybundle/steps/memory_profile.py +176 -0
  58. pybundle/steps/migration_history.py +336 -0
  59. pybundle/steps/mutation_testing.py +141 -0
  60. pybundle/steps/mypy.py +103 -0
  61. pybundle/steps/orm_optimization.py +316 -0
  62. pybundle/steps/pip_audit.py +45 -0
  63. pybundle/steps/pipdeptree.py +62 -0
  64. pybundle/steps/pylance.py +562 -0
  65. pybundle/steps/pytest.py +66 -0
  66. pybundle/steps/query_pattern_analysis.py +334 -0
  67. pybundle/steps/radon.py +161 -0
  68. pybundle/steps/repro_md.py +161 -0
  69. pybundle/steps/rg_scans.py +78 -0
  70. pybundle/steps/roadmap.py +153 -0
  71. pybundle/steps/ruff.py +117 -0
  72. pybundle/steps/secrets_detection.py +235 -0
  73. pybundle/steps/security_headers.py +309 -0
  74. pybundle/steps/shell.py +74 -0
  75. pybundle/steps/slow_tests.py +178 -0
  76. pybundle/steps/sqlalchemy_validation.py +269 -0
  77. pybundle/steps/test_flakiness.py +184 -0
  78. pybundle/steps/tree.py +116 -0
  79. pybundle/steps/type_coverage.py +277 -0
  80. pybundle/steps/unused_deps.py +211 -0
  81. pybundle/steps/vulture.py +167 -0
  82. pybundle/tools.py +63 -0
pybundle/context.py ADDED
@@ -0,0 +1,404 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass, field, asdict
5
+ import json
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+ from typing import Iterable, TYPE_CHECKING
9
+
10
+ from .tools import which
11
+
12
+ if TYPE_CHECKING:
13
+ from .steps.base import StepResult
14
+ from .profiles import Profile
15
+
16
+
17
+ def fmt_tool(path: str | None) -> str:
18
+ if path:
19
+ return path
20
+ return "\x1b[31m<missing>\x1b[0m"
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class Tooling:
25
+ git: str | None
26
+ python: str | None
27
+ pip: str | None
28
+ zip: str | None
29
+ tar: str | None
30
+ uname: str | None
31
+
32
+ # analysis/debug tools
33
+ ruff: str | None
34
+ mypy: str | None
35
+ pytest: str | None
36
+ rg: str | None
37
+ tree: str | None
38
+ npm: str | None
39
+
40
+ # code quality tools (v1.3.0)
41
+ vulture: str | None
42
+ radon: str | None
43
+ interrogate: str | None
44
+ pylint: str | None
45
+
46
+ # dependency analysis tools (v1.3.1)
47
+ pipdeptree: str | None
48
+ pip_licenses: str | None
49
+
50
+ # performance profiling tools (v1.4.0)
51
+ line_profiler: str | None
52
+
53
+ # documentation & type quality tools (v1.5.0)
54
+ pdoc: str | None
55
+ markdown_link_check: str | None
56
+
57
+ # security audit tools (commonly used, should be tracked)
58
+ bandit: str | None
59
+ pip_audit: str | None
60
+
61
+ @staticmethod
62
+ def detect(strict_paths: bool = False) -> "Tooling":
63
+ """Detect available tools, optionally enforcing trusted paths.
64
+
65
+ Args:
66
+ strict_paths: If True, only accept tools in trusted system directories
67
+ """
68
+ return Tooling(
69
+ git=which("git", strict=strict_paths),
70
+ python=which("python", strict=strict_paths)
71
+ or which("python3", strict=strict_paths),
72
+ pip=which("pip", strict=strict_paths) or which("pip3", strict=strict_paths),
73
+ zip=which("zip", strict=strict_paths),
74
+ tar=which("tar", strict=strict_paths),
75
+ uname=which("uname", strict=strict_paths),
76
+ ruff=which("ruff", strict=strict_paths),
77
+ mypy=which("mypy", strict=strict_paths),
78
+ pytest=which("pytest", strict=strict_paths),
79
+ rg=which("rg", strict=strict_paths),
80
+ tree=which("tree", strict=strict_paths),
81
+ npm=which("npm", strict=strict_paths),
82
+ vulture=which("vulture", strict=strict_paths),
83
+ radon=which("radon", strict=strict_paths),
84
+ interrogate=which("interrogate", strict=strict_paths),
85
+ pylint=which("pylint", strict=strict_paths),
86
+ pipdeptree=which("pipdeptree", strict=strict_paths),
87
+ pip_licenses=which("pip-licenses", strict=strict_paths),
88
+ line_profiler=which("kernprof", strict=strict_paths),
89
+ pdoc=which("pdoc", strict=strict_paths),
90
+ markdown_link_check=which("markdown-link-check", strict=strict_paths),
91
+ bandit=which("bandit", strict=strict_paths),
92
+ pip_audit=which("pip-audit", strict=strict_paths),
93
+ )
94
+
95
+
96
+ @dataclass(frozen=True)
97
+ class RunOptions:
98
+ no_ruff: bool | None = None
99
+ no_mypy: bool | None = None
100
+ no_pylance: bool | None = None
101
+ no_pytest: bool | None = None
102
+ no_bandit: bool | None = None
103
+ no_pip_audit: bool | None = None
104
+ no_coverage: bool | None = None
105
+ no_rg: bool | None = None
106
+ no_error_refs: bool | None = None
107
+ no_context: bool | None = None
108
+ no_compileall: bool | None = None
109
+
110
+ # code quality tools (v1.3.0)
111
+ no_vulture: bool | None = None
112
+ no_radon: bool | None = None
113
+ no_interrogate: bool | None = None
114
+ no_duplication: bool | None = None
115
+
116
+ # dependency analysis tools (v1.3.1)
117
+ no_pipdeptree: bool | None = None
118
+ no_unused_deps: bool | None = None
119
+ no_license_scan: bool | None = None
120
+ no_dependency_sizes: bool | None = None
121
+
122
+ # performance profiling (v1.4.0)
123
+ no_profile: bool | None = None
124
+ profile_entry_point: str | None = None
125
+ profile_memory: bool = True # v1.4.2: enabled by default
126
+ enable_line_profiler: bool = False # requires @profile decorators
127
+
128
+ # test quality & coverage (v1.4.1)
129
+ test_flakiness_runs: int = 3
130
+ slow_test_threshold: float = 1.0
131
+ enable_mutation_testing: bool = False
132
+
133
+ # documentation & type quality (v1.5.0)
134
+ no_type_coverage: bool | None = None
135
+ no_link_check: bool | None = None
136
+ no_api_docs: bool | None = None
137
+ no_config_docs: bool | None = None
138
+
139
+ # git analytics (v1.5.1)
140
+ no_git_analytics: bool | None = None
141
+ git_blame_depth: int = 100
142
+
143
+ # runtime analysis (v1.5.2)
144
+ no_runtime_analysis: bool | None = None
145
+
146
+ # container & deployment (v2.0.0)
147
+ no_container_analysis: bool | None = None
148
+ docker_image: str | None = None
149
+
150
+ # configuration & security (v2.0.0)
151
+ no_config_security_analysis: bool | None = None
152
+
153
+ # async & modern python (v2.0.0)
154
+ no_async_analysis: bool | None = None
155
+
156
+ # database & data layer (v2.0.0)
157
+ no_db_analysis: bool | None = None
158
+
159
+ # framework-specific tools (v2.0.0)
160
+ no_framework_analysis: bool | None = None
161
+
162
+ strict_paths: bool = False # Enforce trusted path validation
163
+
164
+ ruff_target: str = "."
165
+ mypy_target: str = "."
166
+ pytest_args: list[str] = field(default_factory=lambda: ["-q"])
167
+
168
+ error_max_files: int = 250
169
+ context_depth: int = 2
170
+ context_max_files: int = 600
171
+
172
+
173
+ @dataclass
174
+ class BundleContext:
175
+ root: Path
176
+ options: RunOptions
177
+ outdir: Path
178
+ profile_name: str
179
+ ts: str
180
+ workdir: Path
181
+ srcdir: Path
182
+ logdir: Path
183
+ metadir: Path
184
+ runlog: Path
185
+ summary_json: Path
186
+ manifest_json: Path
187
+ archive_format: str
188
+ name_prefix: str
189
+ strict: bool
190
+ redact: bool
191
+ keep_workdir: bool
192
+ tools: Tooling
193
+ results: list["StepResult"] = field(default_factory=list)
194
+ command_used: str = ""
195
+ json_mode: bool = False
196
+ archive_path: Path | None = None
197
+ duration_ms: int | None = None
198
+
199
+ def have(self, cmd: str) -> bool:
200
+ return getattr(self.tools, cmd, None) is not None
201
+
202
+ @staticmethod
203
+ def utc_ts() -> str:
204
+ return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
205
+
206
+ @classmethod
207
+ def create(
208
+ cls,
209
+ *,
210
+ root: Path,
211
+ options: RunOptions | None = None,
212
+ outdir: Path,
213
+ profile_name: str,
214
+ archive_format: str,
215
+ name_prefix: str | None,
216
+ strict: bool,
217
+ redact: bool,
218
+ keep_workdir: bool,
219
+ json_mode: bool = False,
220
+ ) -> "BundleContext":
221
+ ts = cls.utc_ts()
222
+ outdir.mkdir(parents=True, exist_ok=True)
223
+
224
+ workdir = outdir / f"pybundle_{profile_name}_{ts}"
225
+ srcdir = workdir / "src"
226
+ logdir = workdir / "logs"
227
+ metadir = workdir / "meta"
228
+
229
+ srcdir.mkdir(parents=True, exist_ok=True)
230
+ logdir.mkdir(parents=True, exist_ok=True)
231
+ metadir.mkdir(parents=True, exist_ok=True)
232
+
233
+ runlog = workdir / "RUN_LOG.txt"
234
+ summary_json = workdir / "SUMMARY.json"
235
+ manifest_json = workdir / "MANIFEST.json"
236
+
237
+ options = options or RunOptions()
238
+ tools = Tooling.detect(strict_paths=options.strict_paths)
239
+ prefix = name_prefix or f"pybundle_{profile_name}_{ts}"
240
+
241
+ return cls(
242
+ root=root,
243
+ options=options,
244
+ outdir=outdir,
245
+ profile_name=profile_name,
246
+ ts=ts,
247
+ workdir=workdir,
248
+ srcdir=srcdir,
249
+ logdir=logdir,
250
+ metadir=metadir,
251
+ runlog=runlog,
252
+ summary_json=summary_json,
253
+ manifest_json=manifest_json,
254
+ archive_format=archive_format,
255
+ name_prefix=prefix,
256
+ strict=strict,
257
+ redact=redact,
258
+ keep_workdir=keep_workdir,
259
+ tools=tools,
260
+ json_mode=json_mode,
261
+ )
262
+
263
+ def rel(self, p: Path) -> str:
264
+ try:
265
+ return str(p.relative_to(self.root))
266
+ except Exception:
267
+ return str(p)
268
+
269
+ def redact_text(self, text: str) -> str:
270
+ if not self.redact:
271
+ return text
272
+ # Minimal default redaction rules (you can expand with a rules file later)
273
+ rules: Iterable[tuple[str, str]] = [
274
+ (
275
+ r"(?i)(api[_-]?key)\s*[:=]\s*['\"]?([A-Za-z0-9_\-]{10,})",
276
+ r"\1=<REDACTED>",
277
+ ),
278
+ (r"(?i)(token)\s*[:=]\s*['\"]?([A-Za-z0-9_\-\.]{10,})", r"\1=<REDACTED>"),
279
+ (r"(?i)(password|passwd|pwd)\s*[:=]\s*['\"]?([^'\"\s]+)", r"\1=<REDACTED>"),
280
+ (r"(?i)(dsn)\s*[:=]\s*['\"]?([^'\"\s]+)", r"\1=<REDACTED>"),
281
+ ]
282
+ out = text
283
+ for pat, repl in rules:
284
+ out = re.sub(pat, repl, out)
285
+ return out
286
+
287
+ def write_runlog(self, line: str) -> None:
288
+ self.runlog.parent.mkdir(parents=True, exist_ok=True)
289
+ with self.runlog.open("a", encoding="utf-8") as f:
290
+ f.write(line.rstrip() + "\n")
291
+
292
+ def print_doctor(self, profile: "Profile") -> None:
293
+ from .doctor import plan_for_profile, print_tool_info
294
+
295
+ print(f"Root: {self.root}")
296
+ print(f"Out: {self.outdir}\n")
297
+
298
+ # Enhanced tool information with security validation
299
+ print_tool_info(self)
300
+ print()
301
+
302
+ # Options (super useful)
303
+ print("Options:")
304
+ o = self.options
305
+ print(f" strict_paths: {o.strict_paths}")
306
+ print(f" ruff_target: {o.ruff_target}")
307
+ print(f" mypy_target: {o.mypy_target}")
308
+ print(f" pytest_args: {' '.join(o.pytest_args)}")
309
+ print(f" no_ruff: {o.no_ruff}")
310
+ print(f" no_mypy: {o.no_mypy}")
311
+ print(f" no_pylance: {o.no_pylance}")
312
+ print(f" no_pytest: {o.no_pytest}")
313
+ print(f" no_bandit: {o.no_bandit}")
314
+ print(f" no_pip_audit: {o.no_pip_audit}")
315
+ print(f" no_coverage: {o.no_coverage}")
316
+ print(f" no_rg: {o.no_rg}")
317
+ print(f" no_error_refs: {o.no_error_refs}")
318
+ print(f" no_context: {o.no_context}")
319
+ print(f" error_max_files: {o.error_max_files}")
320
+ print(f" context_depth: {o.context_depth}")
321
+ print(f" context_max_files: {o.context_max_files}")
322
+ print()
323
+
324
+ # Plan
325
+ plan = plan_for_profile(self, profile)
326
+
327
+ print(f"Plan ({profile.name}):")
328
+ for item in plan:
329
+ out = f" -> {item.out_rel}" if item.out_rel else ""
330
+ if item.status == "RUN":
331
+ print(f" RUN {item.name:<28}{out}")
332
+ else:
333
+ why = f" ({item.reason})" if item.reason else ""
334
+ print(f" SKIP {item.name:<28}{out}{why}")
335
+
336
+ def doctor_report(self, profile: "Profile") -> dict:
337
+ from .doctor import plan_for_profile
338
+
339
+ plan = plan_for_profile(self, profile)
340
+
341
+ tools = {}
342
+ for k, v in asdict(self.tools).items():
343
+ tools[k] = {"present": v is not None, "path": v}
344
+
345
+ o = self.options
346
+ return {
347
+ "status": "ok",
348
+ "command": "doctor",
349
+ "profile": profile.name,
350
+ "root": str(self.root),
351
+ "outdir": str(self.outdir),
352
+ "tools": tools,
353
+ "options": {
354
+ "ruff_target": o.ruff_target,
355
+ "mypy_target": o.mypy_target,
356
+ "pytest_args": list(o.pytest_args),
357
+ "no_ruff": o.no_ruff,
358
+ "no_mypy": o.no_mypy,
359
+ "no_pylance": o.no_pylance,
360
+ "no_pytest": o.no_pytest,
361
+ "no_bandit": o.no_bandit,
362
+ "no_pip_audit": o.no_pip_audit,
363
+ "no_coverage": o.no_coverage,
364
+ "no_rg": o.no_rg,
365
+ "no_error_refs": o.no_error_refs,
366
+ "no_context": o.no_context,
367
+ "no_vulture": o.no_vulture,
368
+ "no_radon": o.no_radon,
369
+ "no_interrogate": o.no_interrogate,
370
+ "no_duplication": o.no_duplication,
371
+ "no_pipdeptree": o.no_pipdeptree,
372
+ "no_unused_deps": o.no_unused_deps,
373
+ "no_license_scan": o.no_license_scan,
374
+ "no_dependency_sizes": o.no_dependency_sizes,
375
+ "no_profile": o.no_profile,
376
+ "profile_entry_point": o.profile_entry_point,
377
+ "profile_memory": o.profile_memory,
378
+ "enable_line_profiler": o.enable_line_profiler,
379
+ "test_flakiness_runs": o.test_flakiness_runs,
380
+ "slow_test_threshold": o.slow_test_threshold,
381
+ "enable_mutation_testing": o.enable_mutation_testing,
382
+ "error_max_files": o.error_max_files,
383
+ "context_depth": o.context_depth,
384
+ "context_max_files": o.context_max_files,
385
+ },
386
+ "plan": [
387
+ {
388
+ "name": item.name,
389
+ "status": item.status, # "RUN" or "SKIP"
390
+ "out_rel": item.out_rel,
391
+ "reason": item.reason,
392
+ }
393
+ for item in plan
394
+ ],
395
+ }
396
+
397
+ def emit(self, msg: str) -> None:
398
+ """Human output only."""
399
+ if not self.json_mode:
400
+ print(msg)
401
+
402
+ def emit_json(self, payload: dict) -> None:
403
+ """JSON output only (stdout contract)."""
404
+ print(json.dumps(payload, ensure_ascii=False))
pybundle/doctor.py ADDED
@@ -0,0 +1,148 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Callable, TypeVar
5
+
6
+ from .steps.shell import ShellStep
7
+ from .steps.ruff import RuffCheckStep, RuffFormatCheckStep
8
+ from .steps.mypy import MypyStep
9
+ from .steps.pytest import PytestStep
10
+ from .steps.rg_scans import RipgrepScanStep
11
+ from .tools import is_path_trusted
12
+
13
+ T = TypeVar("T")
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class PlanItem:
18
+ name: str
19
+ status: str # "RUN" | "SKIP"
20
+ out_rel: str | None
21
+ reason: str
22
+
23
+
24
+ EvalFn = Callable[[Any, T], PlanItem]
25
+
26
+
27
+ def _out(step: Any) -> str | None:
28
+ return (
29
+ getattr(step, "out_rel", None)
30
+ or getattr(step, "outfile_rel", None)
31
+ or getattr(step, "outfile", None)
32
+ )
33
+
34
+
35
+ def eval_shell(ctx: Any, step: ShellStep) -> PlanItem:
36
+ if step.require_cmd and not ctx.have(step.require_cmd):
37
+ return PlanItem(
38
+ step.name, "SKIP", _out(step), f"missing tool: {step.require_cmd}"
39
+ )
40
+ return PlanItem(step.name, "RUN", _out(step), "")
41
+
42
+
43
+ def eval_ruff(ctx: Any, step: Any) -> PlanItem:
44
+ if ctx.options.no_ruff:
45
+ return PlanItem(step.name, "SKIP", _out(step), "disabled by --no-ruff")
46
+ if not ctx.have("ruff"):
47
+ return PlanItem(step.name, "SKIP", _out(step), "missing tool: ruff")
48
+ return PlanItem(step.name, "RUN", _out(step), "")
49
+
50
+
51
+ def eval_mypy(ctx: Any, step: MypyStep) -> PlanItem:
52
+ if ctx.options.no_mypy:
53
+ return PlanItem(step.name, "SKIP", _out(step), "disabled by --no-mypy")
54
+ if not ctx.have("mypy"):
55
+ return PlanItem(step.name, "SKIP", _out(step), "missing tool: mypy")
56
+ return PlanItem(step.name, "RUN", _out(step), "")
57
+
58
+
59
+ def eval_pytest(ctx: Any, step: PytestStep) -> PlanItem:
60
+ if ctx.options.no_pytest:
61
+ return PlanItem(step.name, "SKIP", _out(step), "disabled by --no-pytest")
62
+ if not ctx.have("pytest"):
63
+ return PlanItem(step.name, "SKIP", _out(step), "missing tool: pytest")
64
+ if (
65
+ not (ctx.root / "tests").exists()
66
+ and not (ctx.root / "sentra" / "tests").exists()
67
+ ):
68
+ return PlanItem(step.name, "SKIP", _out(step), "no tests/ directory found")
69
+ return PlanItem(step.name, "RUN", _out(step), "")
70
+
71
+
72
+ def eval_rg(ctx: Any, step: Any) -> PlanItem:
73
+ if ctx.options.no_rg:
74
+ return PlanItem(step.name, "SKIP", _out(step), "disabled by --no-rg")
75
+ if not ctx.have("rg"):
76
+ return PlanItem(step.name, "SKIP", _out(step), "missing tool: rg")
77
+ return PlanItem(step.name, "RUN", _out(step), "")
78
+
79
+
80
+ REGISTRY: list[tuple[type[Any], Callable[[Any, Any], PlanItem]]] = [
81
+ (ShellStep, eval_shell),
82
+ (RuffCheckStep, eval_ruff),
83
+ (RuffFormatCheckStep, eval_ruff),
84
+ (MypyStep, eval_mypy),
85
+ (PytestStep, eval_pytest),
86
+ (RipgrepScanStep, eval_rg),
87
+ ]
88
+
89
+
90
+ def plan_for_profile(ctx: Any, profile: Any) -> list[PlanItem]:
91
+ items: list[PlanItem] = []
92
+ for step in profile.steps:
93
+ item: PlanItem | None = None
94
+
95
+ for cls, fn in REGISTRY:
96
+ if isinstance(step, cls):
97
+ item = fn(ctx, step)
98
+ break
99
+ if item is None:
100
+ item = PlanItem(step.name, "RUN", _out(step), "")
101
+ items.append(item)
102
+ return items
103
+
104
+
105
+ def print_tool_info(ctx: Any) -> None:
106
+ """Print tool availability with security validation status."""
107
+
108
+ tools_data = [
109
+ ("git", ctx.tools.git),
110
+ ("python", ctx.tools.python),
111
+ ("pip", ctx.tools.pip),
112
+ ("zip", ctx.tools.zip),
113
+ ("tar", ctx.tools.tar),
114
+ ("uname", ctx.tools.uname),
115
+ ("ruff", ctx.tools.ruff),
116
+ ("mypy", ctx.tools.mypy),
117
+ ("pytest", ctx.tools.pytest),
118
+ ("rg", ctx.tools.rg),
119
+ ("tree", ctx.tools.tree),
120
+ ("npm", ctx.tools.npm),
121
+ # Code quality tools (v1.3.0)
122
+ ("vulture", ctx.tools.vulture),
123
+ ("radon", ctx.tools.radon),
124
+ ("interrogate", ctx.tools.interrogate),
125
+ ("pylint", ctx.tools.pylint),
126
+ # Dependency analysis tools (v1.3.1)
127
+ ("pipdeptree", ctx.tools.pipdeptree),
128
+ ("pip-licenses", ctx.tools.pip_licenses),
129
+ # Performance profiling tools (v1.4.0)
130
+ ("line_profiler", ctx.tools.line_profiler),
131
+ ]
132
+
133
+ print("\n🔧 Tool Detection:")
134
+ print("=" * 70)
135
+
136
+ for name, path in tools_data:
137
+ if path:
138
+ trusted = is_path_trusted(path)
139
+ trust_marker = "✅" if trusted else "⚠️ "
140
+ print(f" {name:10} {trust_marker} {path}")
141
+ else:
142
+ print(f" {name:10} ❌ <missing>")
143
+
144
+ if ctx.options.strict_paths:
145
+ print("\n⚠️ STRICT-PATHS MODE ENABLED")
146
+ print(" Only tools in trusted directories are available.")
147
+
148
+ print("=" * 70)