redeploy 0.2.34__tar.gz → 0.2.36__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 (170) hide show
  1. {redeploy-0.2.34 → redeploy-0.2.36}/PKG-INFO +6 -6
  2. {redeploy-0.2.34 → redeploy-0.2.36}/README.md +5 -5
  3. {redeploy-0.2.34 → redeploy-0.2.36}/pyproject.toml +1 -1
  4. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/__init__.py +1 -1
  5. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/apply/handlers.py +8 -90
  6. redeploy-0.2.36/redeploy/apply/utils/__init__.py +6 -0
  7. redeploy-0.2.36/redeploy/apply/utils/run_container_build.py +69 -0
  8. redeploy-0.2.36/redeploy/cli/commands/hardware.py +272 -0
  9. redeploy-0.2.36/redeploy/cli/commands/version/__init__.py +6 -0
  10. redeploy-0.2.36/redeploy/cli/commands/version/commands.py +594 -0
  11. redeploy-0.2.36/redeploy/cli/commands/version/helpers.py +349 -0
  12. redeploy-0.2.36/redeploy/cli/commands/version/monorepo.py +139 -0
  13. redeploy-0.2.36/redeploy/cli/commands/version/release.py +153 -0
  14. redeploy-0.2.36/redeploy/cli/commands/version/scanner.py +262 -0
  15. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/cli/display.py +114 -74
  16. redeploy-0.2.36/redeploy/detect/builtin/__init__.py +2 -0
  17. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/detect/hardware.py +3 -166
  18. redeploy-0.2.36/redeploy/detect/hardware_rules.py +262 -0
  19. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/detect/templates.py +84 -170
  20. redeploy-0.2.36/redeploy/hardware/__init__.py +2 -0
  21. redeploy-0.2.36/redeploy/hardware/config_txt.py +88 -0
  22. redeploy-0.2.36/redeploy/hardware/data/__init__.py +2 -0
  23. redeploy-0.2.36/redeploy/hardware/data/waveshare.py +48 -0
  24. redeploy-0.2.36/redeploy/hardware/panels.py +51 -0
  25. redeploy-0.2.36/redeploy/hardware/raspi_config.py +32 -0
  26. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/models.py +26 -32
  27. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/version/sources/base.py +5 -0
  28. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/version/sources/json_.py +0 -4
  29. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/version/sources/plain.py +0 -4
  30. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/version/sources/regex.py +0 -4
  31. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/version/sources/toml_.py +0 -4
  32. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/version/sources/yaml_.py +0 -4
  33. redeploy-0.2.36/redeploy/version.py +23 -0
  34. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy.egg-info/PKG-INFO +6 -6
  35. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy.egg-info/SOURCES.txt +17 -1
  36. redeploy-0.2.36/tests/test_hardware_rules.py +189 -0
  37. redeploy-0.2.34/redeploy/cli/commands/hardware.py +0 -230
  38. redeploy-0.2.34/redeploy/cli/commands/version.py +0 -1405
  39. redeploy-0.2.34/redeploy/version.py +0 -96
  40. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/apply/__init__.py +0 -0
  41. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/apply/exceptions.py +0 -0
  42. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/apply/executor.py +0 -0
  43. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/apply/progress.py +0 -0
  44. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/apply/rollback.py +0 -0
  45. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/apply/state.py +0 -0
  46. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/audit.py +0 -0
  47. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/blueprint/__init__.py +0 -0
  48. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/blueprint/extractor.py +0 -0
  49. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/blueprint/generators/__init__.py +0 -0
  50. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/blueprint/generators/docker_compose.py +0 -0
  51. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/blueprint/generators/migration.py +0 -0
  52. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/cli/__init__.py +0 -0
  53. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/cli/commands/__init__.py +0 -0
  54. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/cli/commands/audit.py +0 -0
  55. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/cli/commands/blueprint.py +0 -0
  56. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/cli/commands/detect.py +0 -0
  57. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/cli/commands/device_map.py +0 -0
  58. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/cli/commands/devices.py +0 -0
  59. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/cli/commands/diagnose.py +0 -0
  60. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/cli/commands/diff.py +0 -0
  61. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/cli/commands/exec_.py +0 -0
  62. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/cli/commands/export.py +0 -0
  63. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/cli/commands/import_.py +0 -0
  64. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/cli/commands/init.py +0 -0
  65. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/cli/commands/inspect.py +0 -0
  66. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/cli/commands/patterns.py +0 -0
  67. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/cli/commands/plan_apply.py +0 -0
  68. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/cli/commands/plugin.py +0 -0
  69. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/cli/commands/probe.py +0 -0
  70. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/cli/commands/state.py +0 -0
  71. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/cli/commands/status.py +0 -0
  72. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/cli/commands/target.py +0 -0
  73. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/cli/commands/workflow.py +0 -0
  74. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/cli/core.py +0 -0
  75. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/cli.py +0 -0
  76. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/data_sync.py +0 -0
  77. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/detect/__init__.py +0 -0
  78. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/detect/detector.py +0 -0
  79. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/detect/probes.py +0 -0
  80. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/detect/remote.py +0 -0
  81. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/detect/workflow.py +0 -0
  82. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/discovery.py +0 -0
  83. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/dsl/__init__.py +0 -0
  84. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/dsl/loader.py +0 -0
  85. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/dsl/parser.py +0 -0
  86. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/dsl_python/__init__.py +0 -0
  87. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/dsl_python/context.py +0 -0
  88. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/dsl_python/decorators.py +0 -0
  89. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/dsl_python/docker_steps.py +0 -0
  90. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/dsl_python/exceptions.py +0 -0
  91. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/dsl_python/runner.py +0 -0
  92. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/dsl_python/steps.py +0 -0
  93. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/fleet.py +0 -0
  94. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/iac/__init__.py +0 -0
  95. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/iac/base.py +0 -0
  96. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/iac/docker_compose.py +0 -0
  97. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/iac/parsers/__init__.py +0 -0
  98. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/iac/parsers/compose.py +0 -0
  99. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/iac/registry.py +0 -0
  100. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/markpact/__init__.py +0 -0
  101. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/markpact/compiler.py +0 -0
  102. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/markpact/models.py +0 -0
  103. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/markpact/parser.py +0 -0
  104. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/observe.py +0 -0
  105. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/parse.py +0 -0
  106. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/patterns.py +0 -0
  107. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/plan/__init__.py +0 -0
  108. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/plan/planner.py +0 -0
  109. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/plugins/__init__.py +0 -0
  110. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/plugins/builtin/__init__.py +0 -0
  111. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/plugins/builtin/browser_reload.py +0 -0
  112. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/plugins/builtin/hardware_diagnostic.py +0 -0
  113. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/plugins/builtin/notify.py +0 -0
  114. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/plugins/builtin/process_control.py +0 -0
  115. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/plugins/builtin/systemd_reload.py +0 -0
  116. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/spec_loader.py +0 -0
  117. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/ssh.py +0 -0
  118. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/steps.py +0 -0
  119. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/tests/__init__.py +0 -0
  120. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/tests/test_audit.py +0 -0
  121. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/tests/test_cli.py +0 -0
  122. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/tests/test_data_sync.py +0 -0
  123. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/tests/test_discovery.py +0 -0
  124. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/tests/test_dsl.py +0 -0
  125. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/tests/test_examples.py +0 -0
  126. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/tests/test_executor.py +0 -0
  127. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/tests/test_fleet.py +0 -0
  128. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/tests/test_git_integration.py +0 -0
  129. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/tests/test_iac.py +0 -0
  130. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/tests/test_manifest.py +0 -0
  131. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/tests/test_markpact_compiler.py +0 -0
  132. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/tests/test_markpact_parser.py +0 -0
  133. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/tests/test_models.py +0 -0
  134. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/tests/test_observe.py +0 -0
  135. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/tests/test_parse.py +0 -0
  136. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/tests/test_patterns.py +0 -0
  137. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/tests/test_planner_edge.py +0 -0
  138. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/tests/test_plugins.py +0 -0
  139. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/tests/test_probes.py +0 -0
  140. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/tests/test_public_api.py +0 -0
  141. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/tests/test_resume.py +0 -0
  142. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/tests/test_spec_loader.py +0 -0
  143. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/tests/test_ssh.py +0 -0
  144. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/tests/test_templates.py +0 -0
  145. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/tests/test_verify.py +0 -0
  146. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/tests/test_version.py +0 -0
  147. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/tests/test_version_changelog.py +0 -0
  148. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/tests/test_version_cli.py +0 -0
  149. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/tests/test_version_manifest.py +0 -0
  150. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/tests/test_version_sources.py +0 -0
  151. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/tests/test_workflow.py +0 -0
  152. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/verify.py +0 -0
  153. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/version/__init__.py +0 -0
  154. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/version/bump.py +0 -0
  155. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/version/changelog.py +0 -0
  156. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/version/commits.py +0 -0
  157. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/version/diff.py +0 -0
  158. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/version/git_integration.py +0 -0
  159. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/version/git_transaction.py +0 -0
  160. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/version/manifest.py +0 -0
  161. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/version/sources/__init__.py +0 -0
  162. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy/version/transaction.py +0 -0
  163. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy.egg-info/dependency_links.txt +0 -0
  164. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy.egg-info/entry_points.txt +0 -0
  165. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy.egg-info/requires.txt +0 -0
  166. {redeploy-0.2.34 → redeploy-0.2.36}/redeploy.egg-info/top_level.txt +0 -0
  167. {redeploy-0.2.34 → redeploy-0.2.36}/setup.cfg +0 -0
  168. {redeploy-0.2.34 → redeploy-0.2.36}/tests/test_iac.py +0 -0
  169. {redeploy-0.2.34 → redeploy-0.2.36}/tests/test_parse.py +0 -0
  170. {redeploy-0.2.34 → redeploy-0.2.36}/tests/test_redeploy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redeploy
3
- Version: 0.2.34
3
+ Version: 0.2.36
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.34-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.7h-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.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)
32
32
 
33
- - 🤖 **LLM usage:** $7.5000 (54 commits)
34
- - 👤 **Human dev:** ~$1769 (17.7h @ $100/h, 30min dedup)
33
+ - 🤖 **LLM usage:** $7.5000 (57 commits)
34
+ - 👤 **Human dev:** ~$1815 (18.1h @ $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.34-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.36-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.34-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.7h-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.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)
8
8
 
9
- - 🤖 **LLM usage:** $7.5000 (54 commits)
10
- - 👤 **Human dev:** ~$1769 (17.7h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $7.5000 (57 commits)
10
+ - 👤 **Human dev:** ~$1815 (18.1h @ $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.34-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.36-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.34"
7
+ version = "0.2.36"
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.34"
10
+ __version__ = "0.2.36"
11
11
 
12
12
  # ── Core models ───────────────────────────────────────────────────────────────
13
13
  from .models import ( # noqa: F401
@@ -36,6 +36,10 @@ def run_scp(step: MigrationStep, probe: RemoteProbe, plan: MigrationPlan) -> Non
36
36
  """Copy file via SCP."""
37
37
  if not step.src or not step.dst:
38
38
  raise StepError(step, "scp requires src and dst")
39
+ if probe.is_local and Path(step.src).resolve() == Path(step.dst).resolve():
40
+ step.status = StepStatus.DONE
41
+ step.result = "skipped (same file)"
42
+ return
39
43
  if probe.is_local:
40
44
  cmd = ["cp", step.src, step.dst]
41
45
  else:
@@ -73,51 +77,8 @@ def run_docker_build(
73
77
  emitter: ProgressEmitter | None,
74
78
  ) -> None:
75
79
  """Run docker compose build on remote with periodic progress polling."""
76
- cmd = step.command
77
- if not cmd:
78
- raise StepError(step, "No command specified")
79
-
80
- timeout = step.timeout or 1800 # 30 min default for first ARM64 build
81
- poll_interval = 15 # seconds between progress snapshots
82
- done_event = threading.Event()
83
- result_holder: list = []
84
-
85
- def _ssh_build() -> None:
86
- r = probe.run(cmd, timeout=timeout)
87
- result_holder.append(r)
88
- done_event.set()
89
-
90
- thread = threading.Thread(target=_ssh_build, daemon=True)
91
- thread.start()
92
-
93
- elapsed = 0
94
- while not done_event.wait(timeout=poll_interval):
95
- elapsed += poll_interval
96
- snap = probe.run(
97
- "docker system df 2>/dev/null | grep -E 'Image|Cache' | "
98
- "awk '{print $1, $3, $4}'",
99
- timeout=10,
100
- )
101
- if snap.ok and snap.out.strip():
102
- lines = " | ".join(snap.out.strip().splitlines())
103
- msg = f"[{elapsed}s] build cache: {lines}"
104
- logger.debug(f" {msg}")
105
- if emitter:
106
- emitter.progress(step.id, msg)
107
- else:
108
- msg = f"[{elapsed}s] build in progress..."
109
- logger.debug(f" {msg}")
110
- if emitter:
111
- emitter.progress(step.id, msg)
112
-
113
- if not result_holder:
114
- raise StepError(step, "Build thread did not return a result")
115
- r = result_holder[0]
116
- step.result = r.out[:500]
117
- if not r.ok:
118
- raise StepError(step, f"exit={r.exit_code}: {r.stderr[:300]}")
119
- logger.debug(f" build completed in {elapsed + poll_interval}s")
120
- step.status = StepStatus.DONE
80
+ from .utils import run_container_build
81
+ run_container_build(step, probe, emitter, engine="docker")
121
82
 
122
83
 
123
84
  def run_podman_build(
@@ -126,51 +87,8 @@ def run_podman_build(
126
87
  emitter: ProgressEmitter | None,
127
88
  ) -> None:
128
89
  """Run podman build on remote with periodic progress polling."""
129
- cmd = step.command
130
- if not cmd:
131
- raise StepError(step, "No command specified")
132
-
133
- timeout = step.timeout or 1800 # 30 min default for ARM64 builds
134
- poll_interval = 15 # seconds between progress snapshots
135
- done_event = threading.Event()
136
- result_holder: list = []
137
-
138
- def _ssh_build() -> None:
139
- r = probe.run(cmd, timeout=timeout)
140
- result_holder.append(r)
141
- done_event.set()
142
-
143
- thread = threading.Thread(target=_ssh_build, daemon=True)
144
- thread.start()
145
-
146
- elapsed = 0
147
- while not done_event.wait(timeout=poll_interval):
148
- elapsed += poll_interval
149
- snap = probe.run(
150
- "podman system df 2>/dev/null | grep -E 'Image|Cache' | "
151
- "awk '{print $1, $3, $4}'",
152
- timeout=10,
153
- )
154
- if snap.ok and snap.out.strip():
155
- lines = " | ".join(snap.out.strip().splitlines())
156
- msg = f"[{elapsed}s] podman cache: {lines}"
157
- logger.debug(f" {msg}")
158
- if emitter:
159
- emitter.progress(step.id, msg)
160
- else:
161
- msg = f"[{elapsed}s] podman build in progress..."
162
- logger.debug(f" {msg}")
163
- if emitter:
164
- emitter.progress(step.id, msg)
165
-
166
- if not result_holder:
167
- raise StepError(step, "Podman build thread did not return a result")
168
- r = result_holder[0]
169
- step.result = r.out[:500]
170
- if not r.ok:
171
- raise StepError(step, f"exit={r.exit_code}: {r.stderr[:300]}")
172
- logger.debug(f" podman build completed in {elapsed + poll_interval}s")
173
- step.status = StepStatus.DONE
90
+ from .utils import run_container_build
91
+ run_container_build(step, probe, emitter, engine="podman")
174
92
 
175
93
 
176
94
  def run_docker_health_wait(
@@ -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,272 @@
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):
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:
184
+ console.print(f"[yellow]No fix available for component: {apply_fix_component}[/yellow]")
185
+ sys.exit(1)
186
+
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]")
202
+
203
+
204
+ @click.command()
205
+ @click.argument("host")
206
+ @click.option(
207
+ "--format", "output_fmt",
208
+ default="rich",
209
+ type=click.Choice(["rich", "yaml", "json"]),
210
+ help="Output format",
211
+ )
212
+ @click.option(
213
+ "--fix", "show_fix",
214
+ is_flag=True,
215
+ help="Print fix commands for all issues found",
216
+ )
217
+ @click.option(
218
+ "--apply-fix", "apply_fix_component",
219
+ default=None,
220
+ metavar="COMPONENT",
221
+ help=(
222
+ "Run the fix for a specific component via SSH "
223
+ "(e.g. 'backlight', 'overlay'). Requires sudo on target."
224
+ ),
225
+ )
226
+ @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):
228
+ """Probe and diagnose hardware on a remote host.
229
+
230
+ Checks DSI display, DRM connectors, backlight controller, I2C buses,
231
+ config.txt overlays and Wayland compositor — then reports issues with
232
+ actionable fix suggestions.
233
+
234
+ \b
235
+ Examples:
236
+ redeploy hardware pi@192.168.188.109
237
+ redeploy hardware pi@192.168.188.109 --fix
238
+ redeploy hardware pi@192.168.188.109 --format yaml
239
+ """
240
+ console = Console()
241
+ hw, p = _probe_hardware(host, ssh_key, console)
242
+
243
+ if _format_output(hw, output_fmt):
244
+ return
245
+
246
+ # ── rich output ─────────────────────────────────────────────────────────
247
+
248
+ console.print()
249
+ console.print(Panel(
250
+ f"[bold]hardware probe[/bold] [cyan]{host}[/cyan]\n"
251
+ f"Board: [white]{hw.board or 'unknown'}[/white] "
252
+ f"Kernel: [white]{hw.kernel or 'unknown'}[/white]",
253
+ box=box.ROUNDED,
254
+ expand=False,
255
+ ))
256
+
257
+ _print_drm_connectors(console, hw)
258
+ _print_backlights(console, hw)
259
+ _print_dsi_overlays(console, hw)
260
+ _print_i2c_buses(console, hw)
261
+
262
+ console.print()
263
+
264
+ _print_diagnostics(console, hw, show_fix)
265
+
266
+ if apply_fix_component:
267
+ _apply_fix(console, p, hw, apply_fix_component)
268
+
269
+ # Exit code based on errors
270
+ errors = hw.errors
271
+ if errors:
272
+ 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"]