redeploy 0.2.35__tar.gz → 0.2.37__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 (174) hide show
  1. {redeploy-0.2.35 → redeploy-0.2.37}/PKG-INFO +6 -6
  2. {redeploy-0.2.35 → redeploy-0.2.37}/README.md +5 -5
  3. {redeploy-0.2.35 → redeploy-0.2.37}/pyproject.toml +1 -1
  4. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/__init__.py +1 -1
  5. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/apply/executor.py +3 -0
  6. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/apply/handlers.py +64 -90
  7. redeploy-0.2.37/redeploy/apply/utils/__init__.py +6 -0
  8. redeploy-0.2.37/redeploy/apply/utils/run_container_build.py +69 -0
  9. redeploy-0.2.37/redeploy/cli/commands/hardware.py +314 -0
  10. redeploy-0.2.37/redeploy/cli/commands/version/__init__.py +6 -0
  11. redeploy-0.2.37/redeploy/cli/commands/version/commands.py +594 -0
  12. redeploy-0.2.37/redeploy/cli/commands/version/helpers.py +349 -0
  13. redeploy-0.2.37/redeploy/cli/commands/version/monorepo.py +139 -0
  14. redeploy-0.2.37/redeploy/cli/commands/version/release.py +153 -0
  15. redeploy-0.2.37/redeploy/cli/commands/version/scanner.py +262 -0
  16. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/display.py +114 -74
  17. redeploy-0.2.37/redeploy/detect/builtin/__init__.py +2 -0
  18. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/detect/hardware.py +12 -171
  19. redeploy-0.2.37/redeploy/detect/hardware_rules.py +262 -0
  20. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/detect/templates.py +84 -170
  21. redeploy-0.2.37/redeploy/hardware/__init__.py +8 -0
  22. redeploy-0.2.37/redeploy/hardware/config_txt.py +88 -0
  23. redeploy-0.2.37/redeploy/hardware/data/__init__.py +4 -0
  24. redeploy-0.2.37/redeploy/hardware/data/hyperpixel.py +41 -0
  25. redeploy-0.2.37/redeploy/hardware/data/official.py +18 -0
  26. redeploy-0.2.37/redeploy/hardware/data/waveshare.py +48 -0
  27. redeploy-0.2.37/redeploy/hardware/fixes.py +203 -0
  28. redeploy-0.2.37/redeploy/hardware/panels.py +51 -0
  29. redeploy-0.2.37/redeploy/hardware/raspi_config.py +32 -0
  30. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/models.py +47 -32
  31. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/version/sources/base.py +5 -0
  32. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/version/sources/json_.py +0 -4
  33. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/version/sources/plain.py +0 -4
  34. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/version/sources/regex.py +0 -4
  35. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/version/sources/toml_.py +0 -4
  36. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/version/sources/yaml_.py +0 -4
  37. redeploy-0.2.37/redeploy/version.py +23 -0
  38. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy.egg-info/PKG-INFO +6 -6
  39. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy.egg-info/SOURCES.txt +21 -1
  40. redeploy-0.2.37/tests/test_hardware_config.py +162 -0
  41. redeploy-0.2.37/tests/test_hardware_rules.py +189 -0
  42. redeploy-0.2.35/redeploy/cli/commands/hardware.py +0 -230
  43. redeploy-0.2.35/redeploy/cli/commands/version.py +0 -1405
  44. redeploy-0.2.35/redeploy/version.py +0 -96
  45. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/apply/__init__.py +0 -0
  46. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/apply/exceptions.py +0 -0
  47. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/apply/progress.py +0 -0
  48. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/apply/rollback.py +0 -0
  49. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/apply/state.py +0 -0
  50. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/audit.py +0 -0
  51. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/blueprint/__init__.py +0 -0
  52. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/blueprint/extractor.py +0 -0
  53. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/blueprint/generators/__init__.py +0 -0
  54. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/blueprint/generators/docker_compose.py +0 -0
  55. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/blueprint/generators/migration.py +0 -0
  56. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/__init__.py +0 -0
  57. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/__init__.py +0 -0
  58. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/audit.py +0 -0
  59. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/blueprint.py +0 -0
  60. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/detect.py +0 -0
  61. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/device_map.py +0 -0
  62. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/devices.py +0 -0
  63. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/diagnose.py +0 -0
  64. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/diff.py +0 -0
  65. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/exec_.py +0 -0
  66. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/export.py +0 -0
  67. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/import_.py +0 -0
  68. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/init.py +0 -0
  69. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/inspect.py +0 -0
  70. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/patterns.py +0 -0
  71. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/plan_apply.py +0 -0
  72. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/plugin.py +0 -0
  73. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/probe.py +0 -0
  74. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/state.py +0 -0
  75. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/status.py +0 -0
  76. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/target.py +0 -0
  77. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/workflow.py +0 -0
  78. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/core.py +0 -0
  79. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli.py +0 -0
  80. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/data_sync.py +0 -0
  81. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/detect/__init__.py +0 -0
  82. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/detect/detector.py +0 -0
  83. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/detect/probes.py +0 -0
  84. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/detect/remote.py +0 -0
  85. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/detect/workflow.py +0 -0
  86. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/discovery.py +0 -0
  87. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/dsl/__init__.py +0 -0
  88. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/dsl/loader.py +0 -0
  89. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/dsl/parser.py +0 -0
  90. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/dsl_python/__init__.py +0 -0
  91. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/dsl_python/context.py +0 -0
  92. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/dsl_python/decorators.py +0 -0
  93. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/dsl_python/docker_steps.py +0 -0
  94. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/dsl_python/exceptions.py +0 -0
  95. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/dsl_python/runner.py +0 -0
  96. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/dsl_python/steps.py +0 -0
  97. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/fleet.py +0 -0
  98. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/iac/__init__.py +0 -0
  99. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/iac/base.py +0 -0
  100. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/iac/docker_compose.py +0 -0
  101. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/iac/parsers/__init__.py +0 -0
  102. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/iac/parsers/compose.py +0 -0
  103. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/iac/registry.py +0 -0
  104. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/markpact/__init__.py +0 -0
  105. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/markpact/compiler.py +0 -0
  106. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/markpact/models.py +0 -0
  107. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/markpact/parser.py +0 -0
  108. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/observe.py +0 -0
  109. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/parse.py +0 -0
  110. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/patterns.py +0 -0
  111. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/plan/__init__.py +0 -0
  112. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/plan/planner.py +0 -0
  113. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/plugins/__init__.py +0 -0
  114. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/plugins/builtin/__init__.py +0 -0
  115. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/plugins/builtin/browser_reload.py +0 -0
  116. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/plugins/builtin/hardware_diagnostic.py +0 -0
  117. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/plugins/builtin/notify.py +0 -0
  118. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/plugins/builtin/process_control.py +0 -0
  119. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/plugins/builtin/systemd_reload.py +0 -0
  120. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/spec_loader.py +0 -0
  121. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/ssh.py +0 -0
  122. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/steps.py +0 -0
  123. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/__init__.py +0 -0
  124. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_audit.py +0 -0
  125. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_cli.py +0 -0
  126. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_data_sync.py +0 -0
  127. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_discovery.py +0 -0
  128. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_dsl.py +0 -0
  129. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_examples.py +0 -0
  130. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_executor.py +0 -0
  131. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_fleet.py +0 -0
  132. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_git_integration.py +0 -0
  133. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_iac.py +0 -0
  134. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_manifest.py +0 -0
  135. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_markpact_compiler.py +0 -0
  136. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_markpact_parser.py +0 -0
  137. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_models.py +0 -0
  138. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_observe.py +0 -0
  139. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_parse.py +0 -0
  140. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_patterns.py +0 -0
  141. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_planner_edge.py +0 -0
  142. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_plugins.py +0 -0
  143. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_probes.py +0 -0
  144. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_public_api.py +0 -0
  145. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_resume.py +0 -0
  146. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_spec_loader.py +0 -0
  147. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_ssh.py +0 -0
  148. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_templates.py +0 -0
  149. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_verify.py +0 -0
  150. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_version.py +0 -0
  151. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_version_changelog.py +0 -0
  152. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_version_cli.py +0 -0
  153. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_version_manifest.py +0 -0
  154. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_version_sources.py +0 -0
  155. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_workflow.py +0 -0
  156. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/verify.py +0 -0
  157. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/version/__init__.py +0 -0
  158. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/version/bump.py +0 -0
  159. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/version/changelog.py +0 -0
  160. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/version/commits.py +0 -0
  161. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/version/diff.py +0 -0
  162. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/version/git_integration.py +0 -0
  163. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/version/git_transaction.py +0 -0
  164. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/version/manifest.py +0 -0
  165. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/version/sources/__init__.py +0 -0
  166. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/version/transaction.py +0 -0
  167. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy.egg-info/dependency_links.txt +0 -0
  168. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy.egg-info/entry_points.txt +0 -0
  169. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy.egg-info/requires.txt +0 -0
  170. {redeploy-0.2.35 → redeploy-0.2.37}/redeploy.egg-info/top_level.txt +0 -0
  171. {redeploy-0.2.35 → redeploy-0.2.37}/setup.cfg +0 -0
  172. {redeploy-0.2.35 → redeploy-0.2.37}/tests/test_iac.py +0 -0
  173. {redeploy-0.2.35 → redeploy-0.2.37}/tests/test_parse.py +0 -0
  174. {redeploy-0.2.35 → redeploy-0.2.37}/tests/test_redeploy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redeploy
3
- Version: 0.2.35
3
+ Version: 0.2.37
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.35-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.0h-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.37-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.6h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
32
32
 
33
- - 🤖 **LLM usage:** $7.5000 (56 commits)
34
- - 👤 **Human dev:** ~$1796 (18.0h @ $100/h, 30min dedup)
33
+ - 🤖 **LLM usage:** $7.5000 (58 commits)
34
+ - 👤 **Human dev:** ~$1859 (18.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.35-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.37-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.35-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.0h-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.37-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.6h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $7.5000 (56 commits)
10
- - 👤 **Human dev:** ~$1796 (18.0h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $7.5000 (58 commits)
10
+ - 👤 **Human dev:** ~$1859 (18.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.35-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.37-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.35"
7
+ version = "0.2.37"
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.35"
10
+ __version__ = "0.2.37"
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)
@@ -77,51 +77,8 @@ def run_docker_build(
77
77
  emitter: ProgressEmitter | None,
78
78
  ) -> None:
79
79
  """Run docker compose build on remote with periodic progress polling."""
80
- cmd = step.command
81
- if not cmd:
82
- raise StepError(step, "No command specified")
83
-
84
- timeout = step.timeout or 1800 # 30 min default for first ARM64 build
85
- poll_interval = 15 # seconds between progress snapshots
86
- done_event = threading.Event()
87
- result_holder: list = []
88
-
89
- def _ssh_build() -> None:
90
- r = probe.run(cmd, timeout=timeout)
91
- result_holder.append(r)
92
- done_event.set()
93
-
94
- thread = threading.Thread(target=_ssh_build, daemon=True)
95
- thread.start()
96
-
97
- elapsed = 0
98
- while not done_event.wait(timeout=poll_interval):
99
- elapsed += poll_interval
100
- snap = probe.run(
101
- "docker system df 2>/dev/null | grep -E 'Image|Cache' | "
102
- "awk '{print $1, $3, $4}'",
103
- timeout=10,
104
- )
105
- if snap.ok and snap.out.strip():
106
- lines = " | ".join(snap.out.strip().splitlines())
107
- msg = f"[{elapsed}s] build cache: {lines}"
108
- logger.debug(f" {msg}")
109
- if emitter:
110
- emitter.progress(step.id, msg)
111
- else:
112
- msg = f"[{elapsed}s] build in progress..."
113
- logger.debug(f" {msg}")
114
- if emitter:
115
- emitter.progress(step.id, msg)
116
-
117
- if not result_holder:
118
- raise StepError(step, "Build thread did not return a result")
119
- r = result_holder[0]
120
- step.result = r.out[:500]
121
- if not r.ok:
122
- raise StepError(step, f"exit={r.exit_code}: {r.stderr[:300]}")
123
- logger.debug(f" build completed in {elapsed + poll_interval}s")
124
- step.status = StepStatus.DONE
80
+ from .utils import run_container_build
81
+ run_container_build(step, probe, emitter, engine="docker")
125
82
 
126
83
 
127
84
  def run_podman_build(
@@ -130,51 +87,8 @@ def run_podman_build(
130
87
  emitter: ProgressEmitter | None,
131
88
  ) -> None:
132
89
  """Run podman build on remote with periodic progress polling."""
133
- cmd = step.command
134
- if not cmd:
135
- raise StepError(step, "No command specified")
136
-
137
- timeout = step.timeout or 1800 # 30 min default for ARM64 builds
138
- poll_interval = 15 # seconds between progress snapshots
139
- done_event = threading.Event()
140
- result_holder: list = []
141
-
142
- def _ssh_build() -> None:
143
- r = probe.run(cmd, timeout=timeout)
144
- result_holder.append(r)
145
- done_event.set()
146
-
147
- thread = threading.Thread(target=_ssh_build, daemon=True)
148
- thread.start()
149
-
150
- elapsed = 0
151
- while not done_event.wait(timeout=poll_interval):
152
- elapsed += poll_interval
153
- snap = probe.run(
154
- "podman system df 2>/dev/null | grep -E 'Image|Cache' | "
155
- "awk '{print $1, $3, $4}'",
156
- timeout=10,
157
- )
158
- if snap.ok and snap.out.strip():
159
- lines = " | ".join(snap.out.strip().splitlines())
160
- msg = f"[{elapsed}s] podman cache: {lines}"
161
- logger.debug(f" {msg}")
162
- if emitter:
163
- emitter.progress(step.id, msg)
164
- else:
165
- msg = f"[{elapsed}s] podman build in progress..."
166
- logger.debug(f" {msg}")
167
- if emitter:
168
- emitter.progress(step.id, msg)
169
-
170
- if not result_holder:
171
- raise StepError(step, "Podman build thread did not return a result")
172
- r = result_holder[0]
173
- step.result = r.out[:500]
174
- if not r.ok:
175
- raise StepError(step, f"exit={r.exit_code}: {r.stderr[:300]}")
176
- logger.debug(f" podman build completed in {elapsed + poll_interval}s")
177
- step.status = StepStatus.DONE
90
+ from .utils import run_container_build
91
+ run_container_build(step, probe, emitter, engine="podman")
178
92
 
179
93
 
180
94
  def run_docker_health_wait(
@@ -424,3 +338,63 @@ def _resolve_command_ref(command_ref: str, step: MigrationStep, plan: MigrationP
424
338
  raise StepError(step, f"Could not find bash script for ref '{section_id}' in {file_path} (tried markpact:ref and section heading)")
425
339
 
426
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}"
@@ -0,0 +1,6 @@
1
+ """Apply utilities package."""
2
+ from __future__ import annotations
3
+
4
+ from .run_container_build import run_container_build
5
+
6
+ __all__ = ["run_container_build"]
@@ -0,0 +1,69 @@
1
+ """Container build handler with progress polling (docker/podman)."""
2
+ from __future__ import annotations
3
+
4
+ import threading
5
+ import time
6
+ from typing import TYPE_CHECKING, Literal
7
+
8
+ from loguru import logger
9
+
10
+ from ...models import StepStatus
11
+
12
+ if TYPE_CHECKING:
13
+ from ..models import MigrationStep
14
+ from ..progress import ProgressEmitter
15
+ from ...detect.remote import RemoteProbe
16
+
17
+
18
+ def run_container_build(
19
+ step: "MigrationStep",
20
+ probe: "RemoteProbe",
21
+ emitter: "ProgressEmitter | None",
22
+ engine: Literal["docker", "podman"],
23
+ ) -> None:
24
+ """Run container build on remote with periodic progress polling."""
25
+ cmd = step.command
26
+ if not cmd:
27
+ from ..exceptions import StepError
28
+ raise StepError(step, "No command specified")
29
+
30
+ timeout = step.timeout or 1800 # 30 min default for ARM64 builds
31
+ poll_interval = 15
32
+ done_event = threading.Event()
33
+ result_holder: list = []
34
+
35
+ def _ssh_build() -> None:
36
+ r = probe.run(cmd, timeout=timeout)
37
+ result_holder.append(r)
38
+ done_event.set()
39
+
40
+ thread = threading.Thread(target=_ssh_build, daemon=True)
41
+ thread.start()
42
+
43
+ elapsed = 0
44
+ while not done_event.wait(timeout=poll_interval):
45
+ elapsed += poll_interval
46
+ cache_cmd = f"{engine} system df 2>/dev/null | grep -E 'Image|Cache' | awk '{{print $1, $3, $4}}'"
47
+ snap = probe.run(cache_cmd, timeout=10)
48
+ if snap.ok and snap.out.strip():
49
+ lines = " | ".join(snap.out.strip().splitlines())
50
+ msg = f"[{elapsed}s] {engine} cache: {lines}"
51
+ logger.debug(f" {msg}")
52
+ if emitter:
53
+ emitter.progress(step.id, msg)
54
+ else:
55
+ msg = f"[{elapsed}s] {engine} build in progress..."
56
+ logger.debug(f" {msg}")
57
+ if emitter:
58
+ emitter.progress(step.id, msg)
59
+
60
+ if not result_holder:
61
+ from ..exceptions import StepError
62
+ raise StepError(step, f"{engine.capitalize()} build thread did not return a result")
63
+ r = result_holder[0]
64
+ step.result = r.out[:500]
65
+ if not r.ok:
66
+ from ..exceptions import StepError
67
+ raise StepError(step, f"exit={r.exit_code}: {r.stderr[:300]}")
68
+ logger.debug(f" {engine} build completed in {elapsed + poll_interval}s")
69
+ step.status = StepStatus.DONE
@@ -0,0 +1,314 @@
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
+ def _probe_hardware(host, ssh_key, console):
40
+ """Probe hardware on remote host."""
41
+ from ...detect.hardware import probe_hardware
42
+ from ...detect.remote import RemoteProbe
43
+
44
+ p = RemoteProbe(host, ssh_key=ssh_key) if ssh_key else RemoteProbe(host)
45
+
46
+ with console.status(f"[cyan]Probing hardware on {host}…[/cyan]"):
47
+ try:
48
+ hw = probe_hardware(p)
49
+ except ConnectionError as exc:
50
+ console.print(f"[red]✗ {exc}[/red]")
51
+ sys.exit(2)
52
+ except Exception as exc:
53
+ console.print(f"[red]✗ hardware probe failed: {exc}[/red]")
54
+ raise
55
+
56
+ return hw, p
57
+
58
+
59
+ def _format_output(hw, output_fmt):
60
+ """Format output as yaml/json, return True if formatted."""
61
+ if output_fmt == "yaml":
62
+ import yaml
63
+ click.echo(yaml.safe_dump(hw.model_dump(mode="json"), sort_keys=False))
64
+ return True
65
+
66
+ if output_fmt == "json":
67
+ import json as _json
68
+ click.echo(_json.dumps(hw.model_dump(mode="json"), indent=2))
69
+ return True
70
+
71
+ return False
72
+
73
+
74
+ def _print_drm_connectors(console, hw):
75
+ """Print DRM connectors table."""
76
+ if not hw.drm_outputs:
77
+ return
78
+
79
+ t = Table(title="DRM Connectors", show_header=True, box=box.SIMPLE, padding=(0, 1))
80
+ t.add_column("Connector")
81
+ t.add_column("Status")
82
+ t.add_column("Enabled")
83
+ t.add_column("Mode")
84
+ t.add_column("Transform")
85
+ for o in hw.drm_outputs:
86
+ status_color = "green" if o.status == "connected" else "red"
87
+ mode = o.modes[0] if o.modes else "—"
88
+ t.add_row(
89
+ o.connector,
90
+ f"[{status_color}]{o.status}[/{status_color}]",
91
+ o.enabled,
92
+ mode,
93
+ o.transform,
94
+ )
95
+ console.print(t)
96
+
97
+
98
+ def _print_backlights(console, hw):
99
+ """Print backlights table."""
100
+ if not hw.backlights:
101
+ return
102
+
103
+ t = Table(title="Backlight", show_header=True, box=box.SIMPLE, padding=(0, 1))
104
+ t.add_column("Device")
105
+ t.add_column("Brightness")
106
+ t.add_column("Max")
107
+ t.add_column("Power")
108
+ t.add_column("Display")
109
+ for b in hw.backlights:
110
+ pw_color = "green" if b.bl_power == 0 else "red"
111
+ br_color = "green" if b.brightness > 0 else "red"
112
+ t.add_row(
113
+ b.name,
114
+ f"[{br_color}]{b.brightness}[/{br_color}]",
115
+ str(b.max_brightness),
116
+ f"[{pw_color}]{'ON' if b.bl_power == 0 else 'OFF'}[/{pw_color}]",
117
+ b.display_name or "—",
118
+ )
119
+ console.print(t)
120
+
121
+
122
+ def _print_dsi_overlays(console, hw):
123
+ """Print DSI overlays."""
124
+ if hw.dsi_overlays:
125
+ console.print(f"[dim]DSI overlays:[/dim] {', '.join(hw.dsi_overlays)}")
126
+ else:
127
+ console.print("[red]DSI overlays: none found[/red]")
128
+
129
+
130
+ def _print_i2c_buses(console, hw):
131
+ """Print I2C buses summary."""
132
+ if not hw.i2c_buses:
133
+ return
134
+
135
+ bus_summary = ", ".join(
136
+ f"i2c-{b.bus}" + (f" [{','.join(b.devices)}]" if b.devices else "")
137
+ for b in hw.i2c_buses
138
+ )
139
+ console.print(f"[dim]I2C buses:[/dim] {bus_summary}")
140
+
141
+
142
+ def _print_diagnostics(console, hw, show_fix):
143
+ """Print diagnostics table and fix suggestions."""
144
+ if not hw.diagnostics:
145
+ console.print("[green]✓ No hardware issues detected[/green]")
146
+ return
147
+
148
+ t = Table(title="Diagnostics", show_header=True, box=box.SIMPLE, padding=(0, 1))
149
+ t.add_column("", width=3)
150
+ t.add_column("Component", style="bold")
151
+ t.add_column("Message")
152
+
153
+ for d in hw.diagnostics:
154
+ color = _SEVERITY_COLOR.get(d.severity, "white")
155
+ icon = _SEVERITY_ICON.get(d.severity, "?")
156
+ t.add_row(
157
+ f"[{color}]{icon}[/{color}]",
158
+ f"[{color}]{d.component}[/{color}]",
159
+ d.message,
160
+ )
161
+ console.print(t)
162
+
163
+ # Fix hints
164
+ fixable = [d for d in hw.diagnostics if d.fix]
165
+ if fixable and show_fix:
166
+ console.print()
167
+ console.print("[bold]Fix suggestions:[/bold]")
168
+ for d in fixable:
169
+ color = _SEVERITY_COLOR.get(d.severity, "white")
170
+ console.print(
171
+ f"\n [{color}][{d.component}][/{color}] {d.message}\n"
172
+ + "\n".join(f" [dim]{line}[/dim]" for line in (d.fix or "").splitlines())
173
+ )
174
+ elif fixable and not show_fix:
175
+ console.print(
176
+ f"\n[dim] {len(fixable)} fix suggestion(s) available — rerun with --fix[/dim]"
177
+ )
178
+
179
+
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:
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]")
194
+ sys.exit(1)
195
+
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)
212
+
213
+
214
+ @click.command()
215
+ @click.argument("host", required=False, default=None)
216
+ @click.option(
217
+ "--format", "output_fmt",
218
+ default="rich",
219
+ type=click.Choice(["rich", "yaml", "json"]),
220
+ help="Output format",
221
+ )
222
+ @click.option(
223
+ "--fix", "show_fix",
224
+ is_flag=True,
225
+ help="Print fix commands for all issues found",
226
+ )
227
+ @click.option(
228
+ "--apply-fix", "apply_fix_component",
229
+ default=None,
230
+ metavar="COMPONENT",
231
+ help=(
232
+ "Run the fix for a specific component via SSH "
233
+ "(e.g. 'backlight', 'overlay'). Requires sudo on target."
234
+ ),
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
+ )
247
+ @click.option("--ssh-key", default=None, type=click.Path(), help="SSH private key path")
248
+ def hardware(host, output_fmt, show_fix, apply_fix_component, panel_id, list_panels, ssh_key):
249
+ """Probe and diagnose hardware on a remote host.
250
+
251
+ Checks DSI display, DRM connectors, backlight controller, I2C buses,
252
+ config.txt overlays and Wayland compositor — then reports issues with
253
+ actionable fix suggestions.
254
+
255
+ \b
256
+ Examples:
257
+ redeploy hardware pi@192.168.188.109
258
+ redeploy hardware pi@192.168.188.109 --fix
259
+ redeploy hardware pi@192.168.188.109 --format yaml
260
+ redeploy hardware --list-panels
261
+ """
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
+
283
+ hw, p = _probe_hardware(host, ssh_key, console)
284
+
285
+ if _format_output(hw, output_fmt):
286
+ return
287
+
288
+ # ── rich output ─────────────────────────────────────────────────────────
289
+
290
+ console.print()
291
+ console.print(Panel(
292
+ f"[bold]hardware probe[/bold] [cyan]{host}[/cyan]\n"
293
+ f"Board: [white]{hw.board or 'unknown'}[/white] "
294
+ f"Kernel: [white]{hw.kernel or 'unknown'}[/white]",
295
+ box=box.ROUNDED,
296
+ expand=False,
297
+ ))
298
+
299
+ _print_drm_connectors(console, hw)
300
+ _print_backlights(console, hw)
301
+ _print_dsi_overlays(console, hw)
302
+ _print_i2c_buses(console, hw)
303
+
304
+ console.print()
305
+
306
+ _print_diagnostics(console, hw, show_fix)
307
+
308
+ if apply_fix_component:
309
+ _apply_fix(console, p, hw, apply_fix_component, panel_id)
310
+
311
+ # Exit code based on errors
312
+ errors = hw.errors
313
+ if errors:
314
+ sys.exit(1)
@@ -0,0 +1,6 @@
1
+ """version command group — Declarative version management."""
2
+ from __future__ import annotations
3
+
4
+ from .commands import version_cmd
5
+
6
+ __all__ = ["version_cmd"]