redeploy 0.2.32__tar.gz → 0.2.33__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. {redeploy-0.2.32 → redeploy-0.2.33}/PKG-INFO +6 -6
  2. {redeploy-0.2.32 → redeploy-0.2.33}/README.md +5 -5
  3. {redeploy-0.2.32 → redeploy-0.2.33}/pyproject.toml +1 -1
  4. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/__init__.py +1 -1
  5. redeploy-0.2.33/redeploy/cli/commands/hardware.py +230 -0
  6. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/detect/__init__.py +2 -1
  7. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/detect/detector.py +9 -1
  8. redeploy-0.2.33/redeploy/detect/hardware.py +442 -0
  9. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy.egg-info/PKG-INFO +6 -6
  10. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy.egg-info/SOURCES.txt +2 -0
  11. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/apply/__init__.py +0 -0
  12. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/apply/exceptions.py +0 -0
  13. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/apply/executor.py +0 -0
  14. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/apply/handlers.py +0 -0
  15. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/apply/progress.py +0 -0
  16. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/apply/rollback.py +0 -0
  17. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/apply/state.py +0 -0
  18. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/audit.py +0 -0
  19. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/cli/__init__.py +0 -0
  20. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/cli/commands/__init__.py +0 -0
  21. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/cli/commands/audit.py +0 -0
  22. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/cli/commands/detect.py +0 -0
  23. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/cli/commands/devices.py +0 -0
  24. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/cli/commands/diagnose.py +0 -0
  25. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/cli/commands/diff.py +0 -0
  26. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/cli/commands/exec_.py +0 -0
  27. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/cli/commands/export.py +0 -0
  28. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/cli/commands/import_.py +0 -0
  29. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/cli/commands/init.py +0 -0
  30. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/cli/commands/inspect.py +0 -0
  31. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/cli/commands/patterns.py +0 -0
  32. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/cli/commands/plan_apply.py +0 -0
  33. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/cli/commands/plugin.py +0 -0
  34. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/cli/commands/probe.py +0 -0
  35. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/cli/commands/state.py +0 -0
  36. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/cli/commands/status.py +0 -0
  37. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/cli/commands/target.py +0 -0
  38. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/cli/commands/version.py +0 -0
  39. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/cli/commands/workflow.py +0 -0
  40. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/cli/core.py +0 -0
  41. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/cli/display.py +0 -0
  42. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/cli.py +0 -0
  43. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/data_sync.py +0 -0
  44. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/detect/probes.py +0 -0
  45. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/detect/remote.py +0 -0
  46. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/detect/templates.py +0 -0
  47. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/detect/workflow.py +0 -0
  48. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/discovery.py +0 -0
  49. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/dsl/__init__.py +0 -0
  50. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/dsl/loader.py +0 -0
  51. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/dsl/parser.py +0 -0
  52. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/dsl_python/__init__.py +0 -0
  53. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/dsl_python/context.py +0 -0
  54. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/dsl_python/decorators.py +0 -0
  55. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/dsl_python/docker_steps.py +0 -0
  56. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/dsl_python/exceptions.py +0 -0
  57. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/dsl_python/runner.py +0 -0
  58. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/dsl_python/steps.py +0 -0
  59. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/fleet.py +0 -0
  60. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/iac/__init__.py +0 -0
  61. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/iac/base.py +0 -0
  62. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/iac/docker_compose.py +0 -0
  63. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/iac/parsers/__init__.py +0 -0
  64. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/iac/parsers/compose.py +0 -0
  65. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/iac/registry.py +0 -0
  66. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/markpact/__init__.py +0 -0
  67. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/markpact/compiler.py +0 -0
  68. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/markpact/models.py +0 -0
  69. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/markpact/parser.py +0 -0
  70. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/models.py +0 -0
  71. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/observe.py +0 -0
  72. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/parse.py +0 -0
  73. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/patterns.py +0 -0
  74. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/plan/__init__.py +0 -0
  75. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/plan/planner.py +0 -0
  76. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/plugins/__init__.py +0 -0
  77. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/plugins/builtin/__init__.py +0 -0
  78. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/plugins/builtin/browser_reload.py +0 -0
  79. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/plugins/builtin/hardware_diagnostic.py +0 -0
  80. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/plugins/builtin/notify.py +0 -0
  81. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/plugins/builtin/process_control.py +0 -0
  82. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/plugins/builtin/systemd_reload.py +0 -0
  83. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/spec_loader.py +0 -0
  84. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/ssh.py +0 -0
  85. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/steps.py +0 -0
  86. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/tests/__init__.py +0 -0
  87. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/tests/test_audit.py +0 -0
  88. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/tests/test_cli.py +0 -0
  89. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/tests/test_data_sync.py +0 -0
  90. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/tests/test_discovery.py +0 -0
  91. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/tests/test_dsl.py +0 -0
  92. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/tests/test_examples.py +0 -0
  93. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/tests/test_executor.py +0 -0
  94. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/tests/test_fleet.py +0 -0
  95. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/tests/test_git_integration.py +0 -0
  96. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/tests/test_iac.py +0 -0
  97. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/tests/test_manifest.py +0 -0
  98. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/tests/test_markpact_compiler.py +0 -0
  99. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/tests/test_markpact_parser.py +0 -0
  100. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/tests/test_models.py +0 -0
  101. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/tests/test_observe.py +0 -0
  102. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/tests/test_parse.py +0 -0
  103. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/tests/test_patterns.py +0 -0
  104. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/tests/test_planner_edge.py +0 -0
  105. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/tests/test_plugins.py +0 -0
  106. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/tests/test_probes.py +0 -0
  107. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/tests/test_public_api.py +0 -0
  108. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/tests/test_resume.py +0 -0
  109. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/tests/test_spec_loader.py +0 -0
  110. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/tests/test_ssh.py +0 -0
  111. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/tests/test_templates.py +0 -0
  112. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/tests/test_verify.py +0 -0
  113. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/tests/test_version.py +0 -0
  114. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/tests/test_version_changelog.py +0 -0
  115. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/tests/test_version_cli.py +0 -0
  116. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/tests/test_version_manifest.py +0 -0
  117. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/tests/test_version_sources.py +0 -0
  118. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/tests/test_workflow.py +0 -0
  119. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/verify.py +0 -0
  120. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/version/__init__.py +0 -0
  121. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/version/bump.py +0 -0
  122. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/version/changelog.py +0 -0
  123. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/version/commits.py +0 -0
  124. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/version/diff.py +0 -0
  125. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/version/git_integration.py +0 -0
  126. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/version/git_transaction.py +0 -0
  127. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/version/manifest.py +0 -0
  128. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/version/sources/__init__.py +0 -0
  129. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/version/sources/base.py +0 -0
  130. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/version/sources/json_.py +0 -0
  131. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/version/sources/plain.py +0 -0
  132. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/version/sources/regex.py +0 -0
  133. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/version/sources/toml_.py +0 -0
  134. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/version/sources/yaml_.py +0 -0
  135. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/version/transaction.py +0 -0
  136. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy/version.py +0 -0
  137. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy.egg-info/dependency_links.txt +0 -0
  138. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy.egg-info/entry_points.txt +0 -0
  139. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy.egg-info/requires.txt +0 -0
  140. {redeploy-0.2.32 → redeploy-0.2.33}/redeploy.egg-info/top_level.txt +0 -0
  141. {redeploy-0.2.32 → redeploy-0.2.33}/setup.cfg +0 -0
  142. {redeploy-0.2.32 → redeploy-0.2.33}/tests/test_iac.py +0 -0
  143. {redeploy-0.2.32 → redeploy-0.2.33}/tests/test_parse.py +0 -0
  144. {redeploy-0.2.32 → redeploy-0.2.33}/tests/test_redeploy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redeploy
3
- Version: 0.2.32
3
+ Version: 0.2.33
4
4
  Summary: Infrastructure migration toolkit: detect → plan → apply
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -27,17 +27,17 @@ Requires-Dist: pfix>=0.1.60; extra == "dev"
27
27
 
28
28
  ## AI Cost Tracking
29
29
 
30
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.32-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
31
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-17.5h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
30
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.33-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
31
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-17.6h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
32
32
 
33
- - 🤖 **LLM usage:** $7.5000 (52 commits)
34
- - 👤 **Human dev:** ~$1755 (17.5h @ $100/h, 30min dedup)
33
+ - 🤖 **LLM usage:** $7.5000 (53 commits)
34
+ - 👤 **Human dev:** ~$1758 (17.6h @ $100/h, 30min dedup)
35
35
 
36
36
  Generated on 2026-04-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
37
37
 
38
38
  ---
39
39
 
40
- ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.32-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
40
+ ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.33-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
41
41
 
42
42
  Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
43
43
 
@@ -3,17 +3,17 @@
3
3
 
4
4
  ## AI Cost Tracking
5
5
 
6
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.32-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-17.5h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
6
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.33-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-17.6h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $7.5000 (52 commits)
10
- - 👤 **Human dev:** ~$1755 (17.5h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $7.5000 (53 commits)
10
+ - 👤 **Human dev:** ~$1758 (17.6h @ $100/h, 30min dedup)
11
11
 
12
12
  Generated on 2026-04-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
13
13
 
14
14
  ---
15
15
 
16
- ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.32-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
16
+ ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.33-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
17
17
 
18
18
  Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
19
19
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "redeploy"
7
- version = "0.2.32"
7
+ version = "0.2.33"
8
8
  description = "Infrastructure migration toolkit: detect → plan → apply"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -7,7 +7,7 @@ Public API (stable from 0.2.0, semver guaranteed)::
7
7
 
8
8
  Everything not listed in ``__all__`` is internal and may change without notice.
9
9
  """
10
- __version__ = "0.2.32"
10
+ __version__ = "0.2.33"
11
11
 
12
12
  # ── Core models ───────────────────────────────────────────────────────────────
13
13
  from .models import ( # noqa: F401
@@ -0,0 +1,230 @@
1
+ """hardware command — Probe and diagnose hardware on a remote host.
2
+
3
+ Detects DSI displays, DRM connectors, backlight, I2C buses,
4
+ GPIO, config.txt overlays and reports problems with fix suggestions.
5
+
6
+ Examples::
7
+
8
+ redeploy hardware pi@192.168.188.109
9
+ redeploy hardware pi@192.168.188.109 --format json
10
+ redeploy hardware pi@192.168.188.109 --fix # print all fix commands
11
+ redeploy hardware pi@192.168.188.109 --apply-fix dsi # run fix for component
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import sys
16
+
17
+ import click
18
+ from rich.console import Console
19
+ from rich.panel import Panel
20
+ from rich.table import Table
21
+ from rich import box
22
+
23
+
24
+ _SEVERITY_COLOR = {
25
+ "info": "green",
26
+ "warning": "yellow",
27
+ "error": "red",
28
+ "critical": "bold red",
29
+ }
30
+
31
+ _SEVERITY_ICON = {
32
+ "info": "✓",
33
+ "warning": "⚠",
34
+ "error": "✗",
35
+ "critical": "✗✗",
36
+ }
37
+
38
+
39
+ @click.command()
40
+ @click.argument("host")
41
+ @click.option(
42
+ "--format", "output_fmt",
43
+ default="rich",
44
+ type=click.Choice(["rich", "yaml", "json"]),
45
+ help="Output format",
46
+ )
47
+ @click.option(
48
+ "--fix", "show_fix",
49
+ is_flag=True,
50
+ help="Print fix commands for all issues found",
51
+ )
52
+ @click.option(
53
+ "--apply-fix", "apply_fix_component",
54
+ default=None,
55
+ metavar="COMPONENT",
56
+ help=(
57
+ "Run the fix for a specific component via SSH "
58
+ "(e.g. 'backlight', 'overlay'). Requires sudo on target."
59
+ ),
60
+ )
61
+ @click.option("--ssh-key", default=None, type=click.Path(), help="SSH private key path")
62
+ def hardware(host, output_fmt, show_fix, apply_fix_component, ssh_key):
63
+ """Probe and diagnose hardware on a remote host.
64
+
65
+ Checks DSI display, DRM connectors, backlight controller, I2C buses,
66
+ config.txt overlays and Wayland compositor — then reports issues with
67
+ actionable fix suggestions.
68
+
69
+ \b
70
+ Examples:
71
+ redeploy hardware pi@192.168.188.109
72
+ redeploy hardware pi@192.168.188.109 --fix
73
+ redeploy hardware pi@192.168.188.109 --format yaml
74
+ """
75
+ from ...detect.hardware import probe_hardware
76
+ from ...detect.remote import RemoteProbe
77
+
78
+ console = Console()
79
+
80
+ p = RemoteProbe(host, ssh_key=ssh_key) if ssh_key else RemoteProbe(host)
81
+
82
+ with console.status(f"[cyan]Probing hardware on {host}…[/cyan]"):
83
+ try:
84
+ hw = probe_hardware(p)
85
+ except ConnectionError as exc:
86
+ console.print(f"[red]✗ {exc}[/red]")
87
+ sys.exit(2)
88
+ except Exception as exc:
89
+ console.print(f"[red]✗ hardware probe failed: {exc}[/red]")
90
+ raise
91
+
92
+ if output_fmt == "yaml":
93
+ import yaml
94
+ click.echo(yaml.safe_dump(hw.model_dump(mode="json"), sort_keys=False))
95
+ return
96
+
97
+ if output_fmt == "json":
98
+ import json as _json
99
+ click.echo(_json.dumps(hw.model_dump(mode="json"), indent=2))
100
+ return
101
+
102
+ # ── rich output ─────────────────────────────────────────────────────────
103
+
104
+ console.print()
105
+ console.print(Panel(
106
+ f"[bold]hardware probe[/bold] [cyan]{host}[/cyan]\n"
107
+ f"Board: [white]{hw.board or 'unknown'}[/white] "
108
+ f"Kernel: [white]{hw.kernel or 'unknown'}[/white]",
109
+ box=box.ROUNDED,
110
+ expand=False,
111
+ ))
112
+
113
+ # DRM Connectors
114
+ if hw.drm_outputs:
115
+ t = Table(title="DRM Connectors", show_header=True, box=box.SIMPLE, padding=(0, 1))
116
+ t.add_column("Connector")
117
+ t.add_column("Status")
118
+ t.add_column("Enabled")
119
+ t.add_column("Mode")
120
+ t.add_column("Transform")
121
+ for o in hw.drm_outputs:
122
+ status_color = "green" if o.status == "connected" else "red"
123
+ mode = o.modes[0] if o.modes else "—"
124
+ t.add_row(
125
+ o.connector,
126
+ f"[{status_color}]{o.status}[/{status_color}]",
127
+ o.enabled,
128
+ mode,
129
+ o.transform,
130
+ )
131
+ console.print(t)
132
+
133
+ # Backlights
134
+ if hw.backlights:
135
+ t = Table(title="Backlight", show_header=True, box=box.SIMPLE, padding=(0, 1))
136
+ t.add_column("Device")
137
+ t.add_column("Brightness")
138
+ t.add_column("Max")
139
+ t.add_column("Power")
140
+ t.add_column("Display")
141
+ for b in hw.backlights:
142
+ pw_color = "green" if b.bl_power == 0 else "red"
143
+ br_color = "green" if b.brightness > 0 else "red"
144
+ t.add_row(
145
+ b.name,
146
+ f"[{br_color}]{b.brightness}[/{br_color}]",
147
+ str(b.max_brightness),
148
+ f"[{pw_color}]{'ON' if b.bl_power == 0 else 'OFF'}[/{pw_color}]",
149
+ b.display_name or "—",
150
+ )
151
+ console.print(t)
152
+
153
+ # DSI config.txt overlays
154
+ if hw.dsi_overlays:
155
+ console.print(f"[dim]DSI overlays:[/dim] {', '.join(hw.dsi_overlays)}")
156
+ else:
157
+ console.print("[red]DSI overlays: none found[/red]")
158
+
159
+ # I2C buses
160
+ if hw.i2c_buses:
161
+ bus_summary = ", ".join(
162
+ f"i2c-{b.bus}" + (f" [{','.join(b.devices)}]" if b.devices else "")
163
+ for b in hw.i2c_buses
164
+ )
165
+ console.print(f"[dim]I2C buses:[/dim] {bus_summary}")
166
+
167
+ console.print()
168
+
169
+ # Diagnostics
170
+ if not hw.diagnostics:
171
+ console.print("[green]✓ No hardware issues detected[/green]")
172
+ else:
173
+ t = Table(title="Diagnostics", show_header=True, box=box.SIMPLE, padding=(0, 1))
174
+ t.add_column("", width=3)
175
+ t.add_column("Component", style="bold")
176
+ t.add_column("Message")
177
+
178
+ for d in hw.diagnostics:
179
+ color = _SEVERITY_COLOR.get(d.severity, "white")
180
+ icon = _SEVERITY_ICON.get(d.severity, "?")
181
+ t.add_row(
182
+ f"[{color}]{icon}[/{color}]",
183
+ f"[{color}]{d.component}[/{color}]",
184
+ d.message,
185
+ )
186
+ console.print(t)
187
+
188
+ # Fix hints
189
+ fixable = [d for d in hw.diagnostics if d.fix]
190
+ if fixable and show_fix:
191
+ console.print()
192
+ console.print("[bold]Fix suggestions:[/bold]")
193
+ for d in fixable:
194
+ color = _SEVERITY_COLOR.get(d.severity, "white")
195
+ console.print(
196
+ f"\n [{color}][{d.component}][/{color}] {d.message}\n"
197
+ + "\n".join(f" [dim]{line}[/dim]" for line in (d.fix or "").splitlines())
198
+ )
199
+ elif fixable and not show_fix:
200
+ console.print(
201
+ f"\n[dim] {len(fixable)} fix suggestion(s) available — rerun with --fix[/dim]"
202
+ )
203
+
204
+ # Apply fix
205
+ if apply_fix_component:
206
+ matching = [d for d in hw.diagnostics if d.component == apply_fix_component and d.fix]
207
+ if not matching:
208
+ console.print(f"[yellow]No fix available for component: {apply_fix_component}[/yellow]")
209
+ sys.exit(1)
210
+
211
+ for d in matching:
212
+ # Extract just the command lines (skip explanatory text)
213
+ fix_lines = [
214
+ line.strip()
215
+ for line in (d.fix or "").splitlines()
216
+ if line.strip() and not line.strip().startswith("#")
217
+ and ":" not in line[:20] # skip lines like "Check: ..."
218
+ ]
219
+ for cmd in fix_lines:
220
+ console.print(f"[cyan]→ {cmd}[/cyan]")
221
+ result = p.run(cmd)
222
+ if result.ok:
223
+ console.print(f"[green] ✓ done[/green]")
224
+ else:
225
+ console.print(f"[red] ✗ failed: {result.err}[/red]")
226
+
227
+ # Exit code based on errors
228
+ errors = hw.errors
229
+ if errors:
230
+ sys.exit(1)
@@ -1,4 +1,5 @@
1
1
  """detect — Probe infrastructure and produce InfraState."""
2
2
  from .detector import Detector
3
+ from .hardware import probe_hardware
3
4
 
4
- __all__ = ["Detector"]
5
+ __all__ = ["Detector", "probe_hardware"]
@@ -7,7 +7,8 @@ from typing import Optional
7
7
  import yaml
8
8
  from loguru import logger
9
9
 
10
- from ..models import InfraState
10
+ from ..models import HardwareInfo, InfraState
11
+ from .hardware import probe_hardware
11
12
  from .probes import (
12
13
  detect_conflicts, detect_strategy, probe_docker_services,
13
14
  probe_health, probe_iptables_dnat, probe_k3s_services,
@@ -94,3 +95,10 @@ class Detector:
94
95
  data = state.model_dump(mode="json")
95
96
  output.write_text(yaml.dump(data, default_flow_style=False, allow_unicode=True))
96
97
  logger.info(f"InfraState saved to {output}")
98
+
99
+ def probe_hardware(self) -> HardwareInfo:
100
+ """Probe hardware state (display, DSI, backlight, I2C, overlays)."""
101
+ if not self.probe.is_reachable():
102
+ raise ConnectionError(f"Host {self.host} is not reachable via SSH")
103
+ logger.info(f"Probing hardware on {self.host}")
104
+ return probe_hardware(self.probe)