redeploy 0.2.36__tar.gz → 0.2.38__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 (173) hide show
  1. {redeploy-0.2.36 → redeploy-0.2.38}/PKG-INFO +6 -6
  2. {redeploy-0.2.36 → redeploy-0.2.38}/README.md +5 -5
  3. {redeploy-0.2.36 → redeploy-0.2.38}/pyproject.toml +1 -1
  4. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/__init__.py +1 -1
  5. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/apply/executor.py +3 -0
  6. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/apply/handlers.py +60 -0
  7. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/hardware.py +64 -22
  8. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/detect/hardware.py +9 -5
  9. redeploy-0.2.38/redeploy/hardware/__init__.py +8 -0
  10. redeploy-0.2.38/redeploy/hardware/data/__init__.py +4 -0
  11. redeploy-0.2.38/redeploy/hardware/data/hyperpixel.py +41 -0
  12. redeploy-0.2.38/redeploy/hardware/data/official.py +18 -0
  13. redeploy-0.2.38/redeploy/hardware/fixes.py +203 -0
  14. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/models.py +21 -0
  15. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy.egg-info/PKG-INFO +6 -6
  16. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy.egg-info/SOURCES.txt +4 -0
  17. redeploy-0.2.38/tests/test_hardware_config.py +162 -0
  18. redeploy-0.2.36/redeploy/hardware/__init__.py +0 -2
  19. redeploy-0.2.36/redeploy/hardware/data/__init__.py +0 -2
  20. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/apply/__init__.py +0 -0
  21. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/apply/exceptions.py +0 -0
  22. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/apply/progress.py +0 -0
  23. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/apply/rollback.py +0 -0
  24. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/apply/state.py +0 -0
  25. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/apply/utils/__init__.py +0 -0
  26. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/apply/utils/run_container_build.py +0 -0
  27. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/audit.py +0 -0
  28. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/blueprint/__init__.py +0 -0
  29. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/blueprint/extractor.py +0 -0
  30. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/blueprint/generators/__init__.py +0 -0
  31. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/blueprint/generators/docker_compose.py +0 -0
  32. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/blueprint/generators/migration.py +0 -0
  33. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/__init__.py +0 -0
  34. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/__init__.py +0 -0
  35. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/audit.py +0 -0
  36. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/blueprint.py +0 -0
  37. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/detect.py +0 -0
  38. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/device_map.py +0 -0
  39. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/devices.py +0 -0
  40. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/diagnose.py +0 -0
  41. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/diff.py +0 -0
  42. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/exec_.py +0 -0
  43. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/export.py +0 -0
  44. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/import_.py +0 -0
  45. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/init.py +0 -0
  46. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/inspect.py +0 -0
  47. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/patterns.py +0 -0
  48. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/plan_apply.py +0 -0
  49. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/plugin.py +0 -0
  50. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/probe.py +0 -0
  51. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/state.py +0 -0
  52. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/status.py +0 -0
  53. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/target.py +0 -0
  54. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/version/__init__.py +0 -0
  55. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/version/commands.py +0 -0
  56. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/version/helpers.py +0 -0
  57. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/version/monorepo.py +0 -0
  58. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/version/release.py +0 -0
  59. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/version/scanner.py +0 -0
  60. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/workflow.py +0 -0
  61. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/core.py +0 -0
  62. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/display.py +0 -0
  63. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli.py +0 -0
  64. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/data_sync.py +0 -0
  65. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/detect/__init__.py +0 -0
  66. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/detect/builtin/__init__.py +0 -0
  67. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/detect/detector.py +0 -0
  68. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/detect/hardware_rules.py +0 -0
  69. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/detect/probes.py +0 -0
  70. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/detect/remote.py +0 -0
  71. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/detect/templates.py +0 -0
  72. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/detect/workflow.py +0 -0
  73. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/discovery.py +0 -0
  74. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/dsl/__init__.py +0 -0
  75. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/dsl/loader.py +0 -0
  76. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/dsl/parser.py +0 -0
  77. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/dsl_python/__init__.py +0 -0
  78. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/dsl_python/context.py +0 -0
  79. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/dsl_python/decorators.py +0 -0
  80. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/dsl_python/docker_steps.py +0 -0
  81. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/dsl_python/exceptions.py +0 -0
  82. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/dsl_python/runner.py +0 -0
  83. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/dsl_python/steps.py +0 -0
  84. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/fleet.py +0 -0
  85. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/hardware/config_txt.py +0 -0
  86. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/hardware/data/waveshare.py +0 -0
  87. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/hardware/panels.py +0 -0
  88. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/hardware/raspi_config.py +0 -0
  89. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/iac/__init__.py +0 -0
  90. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/iac/base.py +0 -0
  91. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/iac/docker_compose.py +0 -0
  92. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/iac/parsers/__init__.py +0 -0
  93. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/iac/parsers/compose.py +0 -0
  94. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/iac/registry.py +0 -0
  95. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/markpact/__init__.py +0 -0
  96. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/markpact/compiler.py +0 -0
  97. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/markpact/models.py +0 -0
  98. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/markpact/parser.py +0 -0
  99. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/observe.py +0 -0
  100. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/parse.py +0 -0
  101. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/patterns.py +0 -0
  102. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/plan/__init__.py +0 -0
  103. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/plan/planner.py +0 -0
  104. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/plugins/__init__.py +0 -0
  105. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/plugins/builtin/__init__.py +0 -0
  106. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/plugins/builtin/browser_reload.py +0 -0
  107. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/plugins/builtin/hardware_diagnostic.py +0 -0
  108. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/plugins/builtin/notify.py +0 -0
  109. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/plugins/builtin/process_control.py +0 -0
  110. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/plugins/builtin/systemd_reload.py +0 -0
  111. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/spec_loader.py +0 -0
  112. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/ssh.py +0 -0
  113. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/steps.py +0 -0
  114. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/__init__.py +0 -0
  115. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_audit.py +0 -0
  116. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_cli.py +0 -0
  117. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_data_sync.py +0 -0
  118. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_discovery.py +0 -0
  119. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_dsl.py +0 -0
  120. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_examples.py +0 -0
  121. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_executor.py +0 -0
  122. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_fleet.py +0 -0
  123. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_git_integration.py +0 -0
  124. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_iac.py +0 -0
  125. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_manifest.py +0 -0
  126. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_markpact_compiler.py +0 -0
  127. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_markpact_parser.py +0 -0
  128. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_models.py +0 -0
  129. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_observe.py +0 -0
  130. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_parse.py +0 -0
  131. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_patterns.py +0 -0
  132. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_planner_edge.py +0 -0
  133. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_plugins.py +0 -0
  134. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_probes.py +0 -0
  135. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_public_api.py +0 -0
  136. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_resume.py +0 -0
  137. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_spec_loader.py +0 -0
  138. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_ssh.py +0 -0
  139. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_templates.py +0 -0
  140. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_verify.py +0 -0
  141. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_version.py +0 -0
  142. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_version_changelog.py +0 -0
  143. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_version_cli.py +0 -0
  144. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_version_manifest.py +0 -0
  145. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_version_sources.py +0 -0
  146. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_workflow.py +0 -0
  147. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/verify.py +0 -0
  148. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/version/__init__.py +0 -0
  149. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/version/bump.py +0 -0
  150. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/version/changelog.py +0 -0
  151. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/version/commits.py +0 -0
  152. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/version/diff.py +0 -0
  153. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/version/git_integration.py +0 -0
  154. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/version/git_transaction.py +0 -0
  155. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/version/manifest.py +0 -0
  156. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/version/sources/__init__.py +0 -0
  157. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/version/sources/base.py +0 -0
  158. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/version/sources/json_.py +0 -0
  159. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/version/sources/plain.py +0 -0
  160. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/version/sources/regex.py +0 -0
  161. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/version/sources/toml_.py +0 -0
  162. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/version/sources/yaml_.py +0 -0
  163. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/version/transaction.py +0 -0
  164. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/version.py +0 -0
  165. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy.egg-info/dependency_links.txt +0 -0
  166. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy.egg-info/entry_points.txt +0 -0
  167. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy.egg-info/requires.txt +0 -0
  168. {redeploy-0.2.36 → redeploy-0.2.38}/redeploy.egg-info/top_level.txt +0 -0
  169. {redeploy-0.2.36 → redeploy-0.2.38}/setup.cfg +0 -0
  170. {redeploy-0.2.36 → redeploy-0.2.38}/tests/test_hardware_rules.py +0 -0
  171. {redeploy-0.2.36 → redeploy-0.2.38}/tests/test_iac.py +0 -0
  172. {redeploy-0.2.36 → redeploy-0.2.38}/tests/test_parse.py +0 -0
  173. {redeploy-0.2.36 → redeploy-0.2.38}/tests/test_redeploy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redeploy
3
- Version: 0.2.36
3
+ Version: 0.2.38
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.36-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-18.1h-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.38-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-19.4h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
32
32
 
33
- - 🤖 **LLM usage:** $7.5000 (57 commits)
34
- - 👤 **Human dev:** ~$1815 (18.1h @ $100/h, 30min dedup)
33
+ - 🤖 **LLM usage:** $7.5000 (59 commits)
34
+ - 👤 **Human dev:** ~$1944 (19.4h @ $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.36-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.38-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.36-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-18.1h-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.38-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-19.4h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $7.5000 (57 commits)
10
- - 👤 **Human dev:** ~$1815 (18.1h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $7.5000 (59 commits)
10
+ - 👤 **Human dev:** ~$1944 (19.4h @ $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.36-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.38-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.36"
7
+ version = "0.2.38"
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.36"
10
+ __version__ = "0.2.38"
11
11
 
12
12
  # ── Core models ───────────────────────────────────────────────────────────────
13
13
  from .models import ( # noqa: F401
@@ -15,6 +15,7 @@ from .handlers import (
15
15
  run_ssh, run_scp, run_rsync, run_docker_build, run_podman_build,
16
16
  run_docker_health_wait, run_container_log_tail, run_http_check,
17
17
  run_version_check, run_plugin, run_wait, run_inline_script,
18
+ run_ensure_config_line, run_raspi_config,
18
19
  )
19
20
  from .progress import ProgressEmitter
20
21
  from .rollback import rollback_steps
@@ -227,6 +228,8 @@ class Executor:
227
228
  StepAction.WAIT: lambda s: run_wait(s),
228
229
  StepAction.PLUGIN: lambda s: run_plugin(s, self.probe, self.plan, self._emitter, self.dry_run),
229
230
  StepAction.INLINE_SCRIPT: lambda s: run_inline_script(s, self.probe, self.plan),
231
+ StepAction.ENSURE_CONFIG_LINE: lambda s: run_ensure_config_line(s, self.probe),
232
+ StepAction.RASPI_CONFIG: lambda s: run_raspi_config(s, self.probe),
230
233
  }
231
234
 
232
235
  handler = dispatch.get(step.action)
@@ -338,3 +338,63 @@ def _resolve_command_ref(command_ref: str, step: MigrationStep, plan: MigrationP
338
338
  raise StepError(step, f"Could not find bash script for ref '{section_id}' in {file_path} (tried markpact:ref and section heading)")
339
339
 
340
340
  return script
341
+
342
+
343
+ # ── hardware-specific handlers ────────────────────────────────────────────────
344
+
345
+ def run_ensure_config_line(step: MigrationStep, probe: "RemoteProbe") -> None:
346
+ """Idempotent add/replace a line in a remote config.txt."""
347
+ from ..hardware.config_txt import ensure_line
348
+
349
+ if not step.config_file or not step.config_line:
350
+ raise StepError(step, "ensure_config_line requires config_file and config_line")
351
+
352
+ config_path = step.config_file
353
+ r = probe.run(f"sudo cat {config_path}", timeout=10)
354
+ if not r.ok:
355
+ raise StepError(step, f"Cannot read {config_path}: {r.stderr[:200]}")
356
+
357
+ edit = ensure_line(
358
+ r.out,
359
+ step.config_line,
360
+ section=step.config_section or "all",
361
+ replaces_pattern=step.config_replaces_pattern,
362
+ )
363
+
364
+ if not edit.changed:
365
+ step.status = StepStatus.DONE
366
+ step.result = f"no-op: {edit.diff_summary}"
367
+ return
368
+
369
+ # Write atomically: base64-encode to avoid shell quoting issues
370
+ encoded = base64.b64encode(edit.new_content.encode()).decode()
371
+ tmp = f"/tmp/redeploy-cfg-{step.id}.txt"
372
+ write_r = probe.run(
373
+ f"echo '{encoded}' | base64 -d | sudo tee {tmp} > /dev/null && sudo mv {tmp} {config_path}",
374
+ timeout=15,
375
+ )
376
+ if not write_r.ok:
377
+ raise StepError(step, f"Cannot write {config_path}: {write_r.stderr[:200]}")
378
+
379
+ step.status = StepStatus.DONE
380
+ step.result = edit.diff_summary
381
+
382
+
383
+ def run_raspi_config(step: MigrationStep, probe: "RemoteProbe") -> None:
384
+ """Run raspi-config nonint to enable/disable an interface."""
385
+ from ..hardware.raspi_config import build_raspi_config_command
386
+
387
+ if not step.raspi_interface or not step.raspi_state:
388
+ raise StepError(step, "raspi_config requires raspi_interface and raspi_state")
389
+
390
+ try:
391
+ cmd = build_raspi_config_command(step.raspi_interface, step.raspi_state)
392
+ except ValueError as exc:
393
+ raise StepError(step, str(exc))
394
+
395
+ r = probe.run(cmd, timeout=30)
396
+ if not r.ok:
397
+ raise StepError(step, f"raspi-config failed: {r.stderr[:200]}")
398
+
399
+ step.status = StepStatus.DONE
400
+ step.result = f"applied: {cmd}"
@@ -177,32 +177,42 @@ def _print_diagnostics(console, hw, show_fix):
177
177
  )
178
178
 
179
179
 
180
- def _apply_fix(console, p, hw, apply_fix_component):
181
- """Apply fix for a specific component."""
182
- matching = [d for d in hw.diagnostics if d.component == apply_fix_component and d.fix]
183
- if not matching:
180
+ def _apply_fix(console, p, hw, apply_fix_component, panel_id=None):
181
+ """Apply fix for a specific component using fix plan generator."""
182
+ from ...hardware.fixes import generate_fix_plan
183
+ from ...hardware.panels import get
184
+
185
+ # Get panel if specified
186
+ panel = get(panel_id) if panel_id else None
187
+
188
+ # Generate fix plan
189
+ steps = generate_fix_plan(hw, apply_fix_component, panel)
190
+ if not steps:
184
191
  console.print(f"[yellow]No fix available for component: {apply_fix_component}[/yellow]")
192
+ if panel_id:
193
+ console.print(f"[dim]Panel: {panel_id}[/dim]")
185
194
  sys.exit(1)
186
195
 
187
- for d in matching:
188
- # Extract just the command lines (skip explanatory text)
189
- fix_lines = [
190
- line.strip()
191
- for line in (d.fix or "").splitlines()
192
- if line.strip() and not line.strip().startswith("#")
193
- and ":" not in line[:20] # skip lines like "Check: ..."
194
- ]
195
- for cmd in fix_lines:
196
- console.print(f"[cyan]→ {cmd}[/cyan]")
197
- result = p.run(cmd)
198
- if result.ok:
199
- console.print(f"[green] ✓ done[/green]")
200
- else:
201
- console.print(f"[red] ✗ failed: {result.err}[/red]")
196
+ console.print(f"[bold]Fix plan for {apply_fix_component}[/bold]")
197
+ if panel:
198
+ console.print(f" Panel: {panel.name}")
199
+
200
+ # Execute steps via Executor (simplified version)
201
+ from ...apply.executor import Executor
202
+
203
+ executor = Executor(probe=p, console=console, dry_run=False)
204
+ for step in steps:
205
+ console.print(f"[cyan]→ {step.description}[/cyan]")
206
+ try:
207
+ executor._run_single_step(step)
208
+ console.print(f"[green] ✓ done[/green]")
209
+ except Exception as exc:
210
+ console.print(f"[red] ✗ failed: {exc}[/red]")
211
+ sys.exit(1)
202
212
 
203
213
 
204
214
  @click.command()
205
- @click.argument("host")
215
+ @click.argument("host", required=False, default=None)
206
216
  @click.option(
207
217
  "--format", "output_fmt",
208
218
  default="rich",
@@ -223,8 +233,19 @@ def _apply_fix(console, p, hw, apply_fix_component):
223
233
  "(e.g. 'backlight', 'overlay'). Requires sudo on target."
224
234
  ),
225
235
  )
236
+ @click.option(
237
+ "--panel", "panel_id",
238
+ default=None,
239
+ metavar="PANEL_ID",
240
+ help="Specify panel ID explicitly (use --list-panels to see available)",
241
+ )
242
+ @click.option(
243
+ "--list-panels",
244
+ is_flag=True,
245
+ help="List available panel definitions and exit",
246
+ )
226
247
  @click.option("--ssh-key", default=None, type=click.Path(), help="SSH private key path")
227
- def hardware(host, output_fmt, show_fix, apply_fix_component, ssh_key):
248
+ def hardware(host, output_fmt, show_fix, apply_fix_component, panel_id, list_panels, ssh_key):
228
249
  """Probe and diagnose hardware on a remote host.
229
250
 
230
251
  Checks DSI display, DRM connectors, backlight controller, I2C buses,
@@ -236,8 +257,29 @@ def hardware(host, output_fmt, show_fix, apply_fix_component, ssh_key):
236
257
  redeploy hardware pi@192.168.188.109
237
258
  redeploy hardware pi@192.168.188.109 --fix
238
259
  redeploy hardware pi@192.168.188.109 --format yaml
260
+ redeploy hardware --list-panels
239
261
  """
240
262
  console = Console()
263
+
264
+ # Handle --list-panels
265
+ if list_panels:
266
+ from ...hardware.panels import all_panels
267
+ panels = all_panels()
268
+ t = Table(show_header=True, box=box.ROUNDED)
269
+ t.add_column("ID", style="cyan")
270
+ t.add_column("Name", style="bold")
271
+ t.add_column("Vendor", style="dim")
272
+ t.add_column("Resolution", style="dim")
273
+ t.add_column("Overlay", style="dim")
274
+ for p in panels:
275
+ res = f"{p.resolution[0]}×{p.resolution[1]}" if p.resolution else "—"
276
+ t.add_row(p.id, p.name, p.vendor, res, p.overlay)
277
+ console.print(t)
278
+ return
279
+
280
+ if not host:
281
+ raise click.UsageError("HOST argument is required unless --list-panels is used.")
282
+
241
283
  hw, p = _probe_hardware(host, ssh_key, console)
242
284
 
243
285
  if _format_output(hw, output_fmt):
@@ -264,7 +306,7 @@ def hardware(host, output_fmt, show_fix, apply_fix_component, ssh_key):
264
306
  _print_diagnostics(console, hw, show_fix)
265
307
 
266
308
  if apply_fix_component:
267
- _apply_fix(console, p, hw, apply_fix_component)
309
+ _apply_fix(console, p, hw, apply_fix_component, panel_id)
268
310
 
269
311
  # Exit code based on errors
270
312
  errors = hw.errors
@@ -53,13 +53,16 @@ def probe_board(p: RemoteProbe) -> tuple[Optional[str], Optional[str]]:
53
53
  return board, kernel
54
54
 
55
55
 
56
- def probe_config_txt(p: RemoteProbe) -> str:
57
- """Read /boot/firmware/config.txt (RPi5) or /boot/config.txt."""
56
+ def probe_config_txt(p: RemoteProbe) -> tuple[str, str]:
57
+ """Read /boot/firmware/config.txt (RPi5) or /boot/config.txt.
58
+
59
+ Returns (content, path) — path is needed for idempotent edits later.
60
+ """
58
61
  for path in ("/boot/firmware/config.txt", "/boot/config.txt"):
59
62
  r = p.run(f"cat {path} 2>/dev/null")
60
63
  if r.ok and r.out.strip():
61
- return r.out
62
- return ""
64
+ return r.out, path
65
+ return "", "/boot/firmware/config.txt"
63
66
 
64
67
 
65
68
  def probe_drm_outputs(p: RemoteProbe) -> list[DrmOutput]:
@@ -232,7 +235,7 @@ def _analyze(hw: HardwareInfo) -> list[HardwareDiagnostic]:
232
235
  def probe_hardware(p: RemoteProbe) -> HardwareInfo:
233
236
  """Probe hardware state of the remote host and return HardwareInfo with diagnostics."""
234
237
  board, kernel = probe_board(p)
235
- config_txt = probe_config_txt(p)
238
+ config_txt, config_txt_path = probe_config_txt(p)
236
239
 
237
240
  # Parse active DSI overlays from config.txt
238
241
  dsi_overlays = [
@@ -266,6 +269,7 @@ def probe_hardware(p: RemoteProbe) -> HardwareInfo:
266
269
  board=board,
267
270
  kernel=kernel,
268
271
  config_txt=config_txt,
272
+ config_txt_path=config_txt_path,
269
273
  dsi_overlays=dsi_overlays,
270
274
  drm_outputs=drm_outputs,
271
275
  wlr_outputs=wlr_outputs,
@@ -0,0 +1,8 @@
1
+ """Hardware configuration support for Raspberry Pi."""
2
+ from __future__ import annotations
3
+
4
+ from .panels import PanelDefinition, get, all_panels, infer_from_hardware, register
5
+ from . import data as _data # noqa: F401 — populates panel registry
6
+
7
+ __all__ = ["PanelDefinition", "get", "all_panels", "infer_from_hardware", "register"]
8
+
@@ -0,0 +1,4 @@
1
+ """Auto-import all panel data files to populate the registry."""
2
+ from __future__ import annotations
3
+
4
+ from . import waveshare, official, hyperpixel # noqa: F401
@@ -0,0 +1,41 @@
1
+ """HyperPixel panel definitions."""
2
+ from __future__ import annotations
3
+
4
+ from redeploy.hardware.panels import PanelDefinition, register
5
+
6
+ register(PanelDefinition(
7
+ id="hyperpixel4-square",
8
+ name="HyperPixel 4.0 Square 720×720",
9
+ vendor="hyperpixel",
10
+ overlay="vc4-kms-dpi-hyperpixel4sq",
11
+ overlay_params=(),
12
+ resolution=(720, 720),
13
+ requires_spi_touch=True,
14
+ notes=[
15
+ "Dotyk przez SPI — raspi-config → SPI → enable",
16
+ "Nie wymaga I2C",
17
+ ],
18
+ ))
19
+
20
+ register(PanelDefinition(
21
+ id="hyperpixel4",
22
+ name="HyperPixel 4.0 800×480",
23
+ vendor="hyperpixel",
24
+ overlay="vc4-kms-dpi-hyperpixel4",
25
+ overlay_params=(),
26
+ resolution=(800, 480),
27
+ requires_spi_touch=True,
28
+ notes=[
29
+ "Dotyk przez SPI — raspi-config → SPI → enable",
30
+ ],
31
+ ))
32
+
33
+ register(PanelDefinition(
34
+ id="hyperpixel2r",
35
+ name="HyperPixel 2.1 Round 480×480",
36
+ vendor="hyperpixel",
37
+ overlay="vc4-kms-dpi-hyperpixel2r",
38
+ overlay_params=(),
39
+ resolution=(480, 480),
40
+ requires_spi_touch=True,
41
+ ))
@@ -0,0 +1,18 @@
1
+ """Official Raspberry Pi display panel definitions."""
2
+ from __future__ import annotations
3
+
4
+ from redeploy.hardware.panels import PanelDefinition, register
5
+
6
+ register(PanelDefinition(
7
+ id="rpi-dsi-7-inch",
8
+ name='Official Raspberry Pi 7" DSI 800×480',
9
+ vendor="official",
10
+ overlay="vc4-kms-dsi-rpi-touchscreen",
11
+ overlay_params=(),
12
+ resolution=(800, 480),
13
+ requires_i2c_touch=True,
14
+ notes=[
15
+ "Oryginalny ekran RPi — sterownik vc4-kms-dsi-rpi-touchscreen",
16
+ "Podłączenie zasilania przez 4-pin JST na module ekranu",
17
+ ],
18
+ ))
@@ -0,0 +1,203 @@
1
+ """Fix-plan generators: HardwareInfo + PanelDefinition → list[MigrationStep]."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Callable
5
+
6
+ from redeploy.models import ConflictSeverity, MigrationStep, StepAction, HardwareInfo
7
+ from redeploy.hardware.panels import PanelDefinition, infer_from_hardware
8
+
9
+
10
+ def _step(
11
+ id: str,
12
+ action: StepAction,
13
+ description: str,
14
+ risk: ConflictSeverity = ConflictSeverity.LOW,
15
+ **kw,
16
+ ) -> MigrationStep:
17
+ return MigrationStep(id=id, action=action, description=description, risk=risk, **kw)
18
+
19
+
20
+ def _detect_port(hw: HardwareInfo) -> str:
21
+ """Detect which DSI port has a connected display."""
22
+ for output in hw.drm_outputs:
23
+ if "DSI" in output.name and output.status == "connected":
24
+ if "DSI-1" in output.name:
25
+ return "dsi1"
26
+ if "DSI-0" in output.name:
27
+ return "dsi0"
28
+ return "dsi1" # safe default
29
+
30
+
31
+ # ── fix generators ────────────────────────────────────────────────────────────
32
+
33
+ def fix_dsi_not_enabled(
34
+ hw: HardwareInfo,
35
+ panel: PanelDefinition | None = None,
36
+ ) -> list[MigrationStep]:
37
+ """Generate steps to configure DSI panel + reboot + verify."""
38
+ panel = panel or infer_from_hardware(hw)
39
+ if panel is None:
40
+ raise ValueError(
41
+ "Cannot infer panel from hardware — specify --panel <id> explicitly. "
42
+ "Run 'redeploy hardware --list-panels' for available IDs."
43
+ )
44
+
45
+ port = _detect_port(hw)
46
+ config_path = hw.config_txt_path
47
+ steps: list[MigrationStep] = []
48
+
49
+ # 1. Backup
50
+ steps.append(_step(
51
+ id="backup_config_txt",
52
+ action=StepAction.SSH_CMD,
53
+ description="Backup config.txt before editing",
54
+ command=(
55
+ f"sudo cp {config_path} {config_path}.bak.$(date +%Y%m%d_%H%M%S)"
56
+ ),
57
+ risk=ConflictSeverity.LOW,
58
+ ))
59
+
60
+ # 2. KMS overlay (if not already present)
61
+ if panel.requires_kms:
62
+ steps.append(_step(
63
+ id="ensure_kms",
64
+ action=StepAction.ENSURE_CONFIG_LINE,
65
+ description="Enable KMS driver (vc4-kms-v3d)",
66
+ config_file=config_path,
67
+ config_line="dtoverlay=vc4-kms-v3d",
68
+ config_replaces_pattern=r"^#?\s*dtoverlay=vc4-kms-v3d",
69
+ config_section="all",
70
+ risk=ConflictSeverity.MEDIUM,
71
+ ))
72
+
73
+ # 3. Panel overlay
74
+ steps.append(_step(
75
+ id="set_panel_overlay",
76
+ action=StepAction.ENSURE_CONFIG_LINE,
77
+ description=f"Set overlay for {panel.name}",
78
+ config_file=config_path,
79
+ config_line=panel.overlay_line(port=port),
80
+ config_replaces_pattern=r"^dtoverlay=vc4-kms-dsi-.*",
81
+ config_section="all",
82
+ risk=ConflictSeverity.MEDIUM,
83
+ ))
84
+
85
+ # 4. I2C for touch (if needed)
86
+ if panel.requires_i2c_touch:
87
+ steps.append(_step(
88
+ id="enable_i2c",
89
+ action=StepAction.RASPI_CONFIG,
90
+ description="Enable I2C for touch controller",
91
+ raspi_interface="i2c",
92
+ raspi_state="enable",
93
+ risk=ConflictSeverity.LOW,
94
+ ))
95
+
96
+ # 5. SPI for touch (if needed)
97
+ if panel.requires_spi_touch:
98
+ steps.append(_step(
99
+ id="enable_spi",
100
+ action=StepAction.RASPI_CONFIG,
101
+ description="Enable SPI for touch controller",
102
+ raspi_interface="spi",
103
+ raspi_state="enable",
104
+ risk=ConflictSeverity.LOW,
105
+ ))
106
+
107
+ # 6. Reboot
108
+ steps.append(_step(
109
+ id="reboot_rpi",
110
+ action=StepAction.SSH_CMD,
111
+ description="Reboot — required by overlay change",
112
+ command="sudo reboot",
113
+ risk=ConflictSeverity.HIGH,
114
+ ))
115
+
116
+ # 7. Wait
117
+ steps.append(_step(
118
+ id="wait_for_rpi",
119
+ action=StepAction.WAIT,
120
+ description="Wait for RPi to come back online",
121
+ seconds=90,
122
+ risk=ConflictSeverity.MEDIUM,
123
+ ))
124
+
125
+ # 8. Verify
126
+ steps.append(_step(
127
+ id="verify_dsi_initialized",
128
+ action=StepAction.SSH_CMD,
129
+ description="Verify DSI initialized in dmesg",
130
+ command="dmesg | grep -iE 'dsi|vc4|panel' | tail -20",
131
+ risk=ConflictSeverity.LOW,
132
+ ))
133
+
134
+ return steps
135
+
136
+
137
+ def fix_enable_i2c(
138
+ hw: HardwareInfo,
139
+ panel: PanelDefinition | None = None,
140
+ ) -> list[MigrationStep]:
141
+ """Enable I2C interface via raspi-config."""
142
+ return [_step(
143
+ id="enable_i2c",
144
+ action=StepAction.RASPI_CONFIG,
145
+ description="Enable I2C",
146
+ raspi_interface="i2c",
147
+ raspi_state="enable",
148
+ risk=ConflictSeverity.LOW,
149
+ )]
150
+
151
+
152
+ def fix_enable_spi(
153
+ hw: HardwareInfo,
154
+ panel: PanelDefinition | None = None,
155
+ ) -> list[MigrationStep]:
156
+ """Enable SPI interface via raspi-config."""
157
+ return [_step(
158
+ id="enable_spi",
159
+ action=StepAction.RASPI_CONFIG,
160
+ description="Enable SPI",
161
+ raspi_interface="spi",
162
+ raspi_state="enable",
163
+ risk=ConflictSeverity.LOW,
164
+ )]
165
+
166
+
167
+ # ── fix registry ──────────────────────────────────────────────────────────────
168
+
169
+ # Maps hardware_rules rule name / diagnostic component → fix generator
170
+ FixGenerator = Callable[[HardwareInfo, PanelDefinition | None], list[MigrationStep]]
171
+
172
+ FIX_REGISTRY: dict[str, FixGenerator] = {
173
+ "no_dsi_overlay": fix_dsi_not_enabled,
174
+ "dsi_overlay_no_drm_connector": fix_dsi_not_enabled,
175
+ "dsi_connector_not_connected": fix_dsi_not_enabled,
176
+ "dsi_connected_no_backlight": fix_dsi_not_enabled,
177
+ "i2c": fix_enable_i2c,
178
+ "spi": fix_enable_spi,
179
+ }
180
+
181
+
182
+ def generate_fix_plan(
183
+ hw: HardwareInfo,
184
+ component: str,
185
+ panel: PanelDefinition | None = None,
186
+ ) -> list[MigrationStep]:
187
+ """From a component name or rule name, return fix steps."""
188
+ # Direct rule name match first
189
+ if component in FIX_REGISTRY:
190
+ return FIX_REGISTRY[component](hw, panel)
191
+
192
+ # Fall back: find diagnostics for this component and use their rule_name
193
+ for diag in hw.diagnostics:
194
+ if diag.component == component:
195
+ # Try to find a matching rule by message keywords
196
+ for key in FIX_REGISTRY:
197
+ if key in diag.message.lower().replace(" ", "_"):
198
+ return FIX_REGISTRY[key](hw, panel)
199
+ # The DSI-related diagnostics all resolve to dsi_not_enabled
200
+ if component in ("dsi", "overlay", "backlight"):
201
+ return fix_dsi_not_enabled(hw, panel)
202
+
203
+ return []
@@ -1,6 +1,7 @@
1
1
  """Shared data models for redeploy: InfraState, MigrationPlan, Target."""
2
2
  from __future__ import annotations
3
3
 
4
+ import re
4
5
  from datetime import datetime, timezone
5
6
  from enum import Enum
6
7
  from pathlib import Path
@@ -185,6 +186,7 @@ class HardwareInfo(BaseModel):
185
186
  board: Optional[str] = None # e.g. "Raspberry Pi 5 Model B Rev 1.0"
186
187
  kernel: Optional[str] = None # uname -r
187
188
  config_txt: str = "" # full /boot/firmware/config.txt
189
+ config_txt_path: str = "/boot/firmware/config.txt" # probed path, varies by OS
188
190
 
189
191
  # Display
190
192
  drm_outputs: list[DrmOutput] = Field(default_factory=list)
@@ -209,6 +211,15 @@ class HardwareInfo(BaseModel):
209
211
  def has_dsi(self) -> bool:
210
212
  return any("DSI" in o.name for o in self.drm_outputs)
211
213
 
214
+ @property
215
+ def kms_enabled(self) -> bool:
216
+ """True if vc4-kms-v3d overlay is present in config.txt."""
217
+ return any(
218
+ re.match(r'\s*dtoverlay=vc4-kms-v3d', line)
219
+ for line in self.config_txt.splitlines()
220
+ if not line.strip().startswith("#")
221
+ )
222
+
212
223
  @property
213
224
  def dsi_connected(self) -> bool:
214
225
  return any("DSI" in o.name and o.status == "connected" for o in self.drm_outputs)
@@ -316,6 +327,16 @@ class MigrationStep(BaseModel):
316
327
  plugin_type: Optional[str] = None # e.g. "browser_reload"
317
328
  plugin_params: dict = Field(default_factory=dict) # passed to plugin
318
329
 
330
+ # ensure_config_line params
331
+ config_file: Optional[str] = None # remote path, e.g. /boot/firmware/config.txt
332
+ config_line: Optional[str] = None # line to ensure presence of
333
+ config_section: str = "all" # [all], [pi5], etc.
334
+ config_replaces_pattern: Optional[str] = None # regex — replace matching line
335
+
336
+ # raspi_config params
337
+ raspi_interface: Optional[str] = None # i2c, spi, camera, onewire, ssh, vnc, serial
338
+ raspi_state: Optional[str] = None # enable, disable
339
+
319
340
  result: Optional[str] = None # filled after apply
320
341
  error: Optional[str] = None
321
342