redeploy 0.2.44__tar.gz → 0.2.45__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 (184) hide show
  1. {redeploy-0.2.44 → redeploy-0.2.45}/PKG-INFO +6 -6
  2. {redeploy-0.2.44 → redeploy-0.2.45}/README.md +5 -5
  3. {redeploy-0.2.44 → redeploy-0.2.45}/pyproject.toml +1 -1
  4. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/__init__.py +1 -1
  5. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/cli/commands/device_map.py +2 -1
  6. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/cli/commands/hardware.py +2 -126
  7. redeploy-0.2.45/redeploy/config_apply/__init__.py +15 -0
  8. redeploy-0.2.45/redeploy/config_apply/applier.py +72 -0
  9. redeploy-0.2.45/redeploy/config_apply/loader.py +17 -0
  10. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/integrations/op3_bridge.py +45 -3
  11. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy.egg-info/PKG-INFO +6 -6
  12. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy.egg-info/SOURCES.txt +3 -0
  13. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/apply/__init__.py +0 -0
  14. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/apply/exceptions.py +0 -0
  15. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/apply/executor.py +0 -0
  16. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/apply/handlers.py +0 -0
  17. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/apply/progress.py +0 -0
  18. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/apply/rollback.py +0 -0
  19. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/apply/state.py +0 -0
  20. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/apply/state_apply.py +0 -0
  21. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/apply/utils/__init__.py +0 -0
  22. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/apply/utils/run_container_build.py +0 -0
  23. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/audit.py +0 -0
  24. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/blueprint/__init__.py +0 -0
  25. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/blueprint/extractor.py +0 -0
  26. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/blueprint/generators/__init__.py +0 -0
  27. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/blueprint/generators/docker_compose.py +0 -0
  28. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/blueprint/generators/migration.py +0 -0
  29. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/cli/__init__.py +0 -0
  30. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/cli/commands/__init__.py +0 -0
  31. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/cli/commands/audit.py +0 -0
  32. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/cli/commands/blueprint.py +0 -0
  33. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/cli/commands/detect.py +0 -0
  34. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/cli/commands/devices.py +0 -0
  35. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/cli/commands/diagnose.py +0 -0
  36. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/cli/commands/diff.py +0 -0
  37. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/cli/commands/exec_.py +0 -0
  38. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/cli/commands/export.py +0 -0
  39. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/cli/commands/import_.py +0 -0
  40. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/cli/commands/init.py +0 -0
  41. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/cli/commands/inspect.py +0 -0
  42. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/cli/commands/patterns.py +0 -0
  43. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/cli/commands/plan_apply.py +0 -0
  44. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/cli/commands/plugin.py +0 -0
  45. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/cli/commands/probe.py +0 -0
  46. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/cli/commands/push.py +0 -0
  47. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/cli/commands/state.py +0 -0
  48. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/cli/commands/status.py +0 -0
  49. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/cli/commands/target.py +0 -0
  50. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/cli/commands/version/__init__.py +0 -0
  51. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/cli/commands/version/commands.py +0 -0
  52. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/cli/commands/version/helpers.py +0 -0
  53. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/cli/commands/version/monorepo.py +0 -0
  54. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/cli/commands/version/release.py +0 -0
  55. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/cli/commands/version/scanner.py +0 -0
  56. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/cli/commands/workflow.py +0 -0
  57. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/cli/core.py +0 -0
  58. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/cli/display.py +0 -0
  59. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/cli.py +0 -0
  60. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/data_sync.py +0 -0
  61. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/detect/__init__.py +0 -0
  62. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/detect/builtin/__init__.py +0 -0
  63. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/detect/detector.py +0 -0
  64. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/detect/hardware.py +0 -0
  65. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/detect/hardware_rules.py +0 -0
  66. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/detect/probes.py +0 -0
  67. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/detect/remote.py +0 -0
  68. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/detect/templates.py +0 -0
  69. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/detect/workflow.py +0 -0
  70. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/discovery.py +0 -0
  71. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/dsl/__init__.py +0 -0
  72. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/dsl/loader.py +0 -0
  73. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/dsl/parser.py +0 -0
  74. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/dsl_python/__init__.py +0 -0
  75. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/dsl_python/context.py +0 -0
  76. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/dsl_python/decorators.py +0 -0
  77. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/dsl_python/docker_steps.py +0 -0
  78. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/dsl_python/exceptions.py +0 -0
  79. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/dsl_python/runner.py +0 -0
  80. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/dsl_python/steps.py +0 -0
  81. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/fleet.py +0 -0
  82. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/hardware/__init__.py +0 -0
  83. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/hardware/config_txt.py +0 -0
  84. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/hardware/data/__init__.py +0 -0
  85. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/hardware/data/hyperpixel.py +0 -0
  86. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/hardware/data/official.py +0 -0
  87. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/hardware/data/waveshare.py +0 -0
  88. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/hardware/fixes.py +0 -0
  89. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/hardware/kiosk/__init__.py +0 -0
  90. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/hardware/kiosk/autostart.py +0 -0
  91. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/hardware/kiosk/browsers.py +0 -0
  92. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/hardware/kiosk/compositors.py +0 -0
  93. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/hardware/kiosk/output_profiles.py +0 -0
  94. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/hardware/panels.py +0 -0
  95. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/hardware/raspi_config.py +0 -0
  96. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/iac/__init__.py +0 -0
  97. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/iac/base.py +0 -0
  98. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/iac/docker_compose.py +0 -0
  99. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/iac/parsers/__init__.py +0 -0
  100. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/iac/parsers/compose.py +0 -0
  101. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/iac/registry.py +0 -0
  102. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/integrations/__init__.py +0 -0
  103. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/markpact/__init__.py +0 -0
  104. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/markpact/compiler.py +0 -0
  105. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/markpact/models.py +0 -0
  106. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/markpact/parser.py +0 -0
  107. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/models.py +0 -0
  108. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/observe.py +0 -0
  109. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/parse.py +0 -0
  110. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/patterns.py +0 -0
  111. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/plan/__init__.py +0 -0
  112. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/plan/planner.py +0 -0
  113. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/plugins/__init__.py +0 -0
  114. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/plugins/builtin/__init__.py +0 -0
  115. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/plugins/builtin/browser_reload.py +0 -0
  116. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/plugins/builtin/hardware_diagnostic.py +0 -0
  117. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/plugins/builtin/notify.py +0 -0
  118. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/plugins/builtin/process_control.py +0 -0
  119. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/plugins/builtin/systemd_reload.py +0 -0
  120. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/spec_loader.py +0 -0
  121. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/ssh.py +0 -0
  122. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/steps.py +0 -0
  123. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/tests/__init__.py +0 -0
  124. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/tests/test_audit.py +0 -0
  125. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/tests/test_cli.py +0 -0
  126. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/tests/test_data_sync.py +0 -0
  127. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/tests/test_discovery.py +0 -0
  128. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/tests/test_dsl.py +0 -0
  129. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/tests/test_examples.py +0 -0
  130. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/tests/test_executor.py +0 -0
  131. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/tests/test_fleet.py +0 -0
  132. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/tests/test_git_integration.py +0 -0
  133. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/tests/test_iac.py +0 -0
  134. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/tests/test_manifest.py +0 -0
  135. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/tests/test_markpact_compiler.py +0 -0
  136. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/tests/test_markpact_parser.py +0 -0
  137. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/tests/test_models.py +0 -0
  138. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/tests/test_observe.py +0 -0
  139. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/tests/test_parse.py +0 -0
  140. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/tests/test_patterns.py +0 -0
  141. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/tests/test_planner_edge.py +0 -0
  142. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/tests/test_plugins.py +0 -0
  143. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/tests/test_probes.py +0 -0
  144. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/tests/test_public_api.py +0 -0
  145. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/tests/test_resume.py +0 -0
  146. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/tests/test_spec_loader.py +0 -0
  147. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/tests/test_ssh.py +0 -0
  148. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/tests/test_templates.py +0 -0
  149. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/tests/test_verify.py +0 -0
  150. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/tests/test_version.py +0 -0
  151. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/tests/test_version_changelog.py +0 -0
  152. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/tests/test_version_cli.py +0 -0
  153. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/tests/test_version_manifest.py +0 -0
  154. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/tests/test_version_sources.py +0 -0
  155. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/tests/test_workflow.py +0 -0
  156. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/verify.py +0 -0
  157. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/version/__init__.py +0 -0
  158. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/version/bump.py +0 -0
  159. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/version/changelog.py +0 -0
  160. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/version/commits.py +0 -0
  161. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/version/diff.py +0 -0
  162. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/version/git_integration.py +0 -0
  163. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/version/git_transaction.py +0 -0
  164. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/version/manifest.py +0 -0
  165. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/version/sources/__init__.py +0 -0
  166. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/version/sources/base.py +0 -0
  167. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/version/sources/json_.py +0 -0
  168. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/version/sources/plain.py +0 -0
  169. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/version/sources/regex.py +0 -0
  170. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/version/sources/toml_.py +0 -0
  171. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/version/sources/yaml_.py +0 -0
  172. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/version/transaction.py +0 -0
  173. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy/version.py +0 -0
  174. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy.egg-info/dependency_links.txt +0 -0
  175. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy.egg-info/entry_points.txt +0 -0
  176. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy.egg-info/requires.txt +0 -0
  177. {redeploy-0.2.44 → redeploy-0.2.45}/redeploy.egg-info/top_level.txt +0 -0
  178. {redeploy-0.2.44 → redeploy-0.2.45}/setup.cfg +0 -0
  179. {redeploy-0.2.44 → redeploy-0.2.45}/tests/test_hardware_config.py +0 -0
  180. {redeploy-0.2.44 → redeploy-0.2.45}/tests/test_hardware_kiosk.py +0 -0
  181. {redeploy-0.2.44 → redeploy-0.2.45}/tests/test_hardware_rules.py +0 -0
  182. {redeploy-0.2.44 → redeploy-0.2.45}/tests/test_iac.py +0 -0
  183. {redeploy-0.2.44 → redeploy-0.2.45}/tests/test_parse.py +0 -0
  184. {redeploy-0.2.44 → redeploy-0.2.45}/tests/test_redeploy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redeploy
3
- Version: 0.2.44
3
+ Version: 0.2.45
4
4
  Summary: Infrastructure migration toolkit: detect → plan → apply
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -30,17 +30,17 @@ Requires-Dist: op3>=0.1.4; extra == "op3"
30
30
 
31
31
  ## AI Cost Tracking
32
32
 
33
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.44-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
34
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-21.4h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
33
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.45-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
34
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-23.2h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
35
35
 
36
- - 🤖 **LLM usage:** $7.5000 (65 commits)
37
- - 👤 **Human dev:** ~$2136 (21.4h @ $100/h, 30min dedup)
36
+ - 🤖 **LLM usage:** $7.5000 (66 commits)
37
+ - 👤 **Human dev:** ~$2318 (23.2h @ $100/h, 30min dedup)
38
38
 
39
39
  Generated on 2026-04-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
40
40
 
41
41
  ---
42
42
 
43
- ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.44-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
43
+ ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.45-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
44
44
 
45
45
  Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
46
46
 
@@ -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.44-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-21.4h-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.45-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-23.2h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $7.5000 (65 commits)
10
- - 👤 **Human dev:** ~$2136 (21.4h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $7.5000 (66 commits)
10
+ - 👤 **Human dev:** ~$2318 (23.2h @ $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.44-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.45-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.44"
7
+ version = "0.2.45"
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.44"
10
+ __version__ = "0.2.45"
11
11
 
12
12
  # ── Core models ───────────────────────────────────────────────────────────────
13
13
  from .models import ( # noqa: F401
@@ -113,7 +113,8 @@ def device_map_cmd(
113
113
 
114
114
  # ── --apply-config ─────────────────────────────────────────────────────────
115
115
  if apply_config:
116
- _apply_config_from_file(console, apply_config, ssh_key)
116
+ from ...config_apply import apply_config_file
117
+ apply_config_file(apply_config, ssh_key=ssh_key, console=console)
117
118
  return
118
119
 
119
120
  # ── probe ─────────────────────────────────────────────────────────────────
@@ -254,131 +254,6 @@ def _apply_transform(console, p, hw, transform):
254
254
  )
255
255
 
256
256
 
257
- def _apply_config(console, p, config_path):
258
- """Apply hardware settings from a YAML/JSON config file to the remote host.
259
-
260
- Reads drm_outputs[].transform entries and applies them via wlr-randr + kanshi.
261
- Also reads backlights[].brightness / bl_power for backlight settings.
262
- """
263
- import yaml
264
- import json as _json
265
-
266
- with open(config_path) as f:
267
- raw = f.read()
268
-
269
- try:
270
- cfg = yaml.safe_load(raw)
271
- except Exception:
272
- cfg = _json.loads(raw)
273
-
274
- applied = 0
275
-
276
- # ── display transforms ────────────────────────────────────────────────────
277
- for output in cfg.get("drm_outputs", []):
278
- connector = output.get("connector", "")
279
- transform = output.get("transform", "normal")
280
- if not connector:
281
- continue
282
- if "DSI" not in connector and "HDMI" not in connector:
283
- continue
284
-
285
- # skip defaults we don't need to touch
286
- if transform == "normal" and output.get("enabled") in (None, "enabled"):
287
- console.print(f"[dim] skip {connector}: transform=normal (default)[/dim]")
288
- continue
289
-
290
- console.print(f"[cyan]→ {connector}: transform={transform}[/cyan]")
291
-
292
- # Apply via wlr-randr
293
- wlr_cmd = (
294
- f"WAYLAND_DISPLAY=wayland-0 XDG_RUNTIME_DIR=/run/user/$(id -u) "
295
- f"wlr-randr --output {connector} --transform {transform} 2>&1"
296
- )
297
- r = p.run(wlr_cmd)
298
- if r.ok:
299
- console.print(f"[green] ✓ wlr-randr applied[/green]")
300
- else:
301
- console.print(f"[yellow] ⚠ wlr-randr: {r.out.strip() or 'no output'}[/yellow]")
302
-
303
- applied += 1
304
-
305
- # ── persist all DSI transforms in kanshi ─────────────────────────────────
306
- dsi_outputs = [
307
- o for o in cfg.get("drm_outputs", [])
308
- if "DSI" in o.get("connector", "")
309
- ]
310
- if dsi_outputs:
311
- _update_kanshi_from_cfg(console, p, dsi_outputs)
312
-
313
- # ── backlight ─────────────────────────────────────────────────────────────
314
- for bl in cfg.get("backlights", []):
315
- name = bl.get("name", "")
316
- brightness = bl.get("brightness")
317
- bl_power = bl.get("bl_power")
318
- if not name:
319
- continue
320
- if brightness is not None:
321
- r = p.run(f"echo {brightness} | sudo tee /sys/class/backlight/{name}/brightness > /dev/null")
322
- if r.ok:
323
- console.print(f"[green] ✓ backlight {name}: brightness={brightness}[/green]")
324
- if bl_power is not None:
325
- r = p.run(f"echo {bl_power} | sudo tee /sys/class/backlight/{name}/bl_power > /dev/null")
326
- if r.ok:
327
- console.print(f"[green] ✓ backlight {name}: bl_power={bl_power}[/green]")
328
- applied += 1
329
-
330
- if applied == 0:
331
- console.print("[yellow]Nothing to apply — no relevant settings found in config[/yellow]")
332
- else:
333
- console.print(f"\n[bold green]✓ Config applied from {config_path}[/bold green]")
334
-
335
-
336
- def _update_kanshi_from_cfg(console, p, dsi_outputs: list):
337
- """Rebuild kanshi profile from drm_outputs list and write to ~/.config/kanshi/config."""
338
- import re as _re
339
-
340
- kanshi_cfg_path = "~/.config/kanshi/config"
341
- read_r = p.run(f"cat {kanshi_cfg_path} 2>/dev/null")
342
- current = read_r.out if read_r.ok else ""
343
-
344
- for output in dsi_outputs:
345
- connector = output.get("connector", "")
346
- transform = output.get("transform", "normal")
347
-
348
- output_line_pat = _re.compile(
349
- rf'(\s*output\s+{_re.escape(connector)}\s+enable)(\s+transform\s+\S+)?'
350
- )
351
- if _re.search(output_line_pat, current):
352
- if transform == "normal":
353
- # Remove transform clause
354
- current = _re.sub(output_line_pat, r'\1', current)
355
- else:
356
- current = _re.sub(output_line_pat, rf'\1 transform {transform}', current)
357
- elif current.strip():
358
- # output line exists but without "enable"
359
- current = _re.sub(
360
- rf'(\s*output\s+{_re.escape(connector)}\b)',
361
- rf'\1 transform {transform}' if transform != "normal" else r'\1',
362
- current,
363
- )
364
- else:
365
- # No config at all — create minimal profile
366
- current = (
367
- f"profile waveshare-only {{\n"
368
- f" output {connector} enable"
369
- + (f" transform {transform}" if transform != "normal" else "")
370
- + "\n"
371
- + " output HDMI-A-2 disable\n"
372
- + "}\n"
373
- )
374
-
375
- escaped = current.replace("'", "'\\''")
376
- write_r = p.run(f"mkdir -p ~/.config/kanshi && printf '%s' '{escaped}' > {kanshi_cfg_path}")
377
- if write_r.ok:
378
- console.print(f"[green] ✓ kanshi config updated ({kanshi_cfg_path})[/green]")
379
- p.run("pkill -SIGUSR1 kanshi 2>/dev/null || true")
380
-
381
-
382
257
  def _execute_query(hw, query_expr, output_fmt):
383
258
  """Execute JMESPath query on hardware model and output result."""
384
259
  import jmespath
@@ -551,7 +426,8 @@ def hardware(host, output_fmt, show_fix, apply_fix_component, panel_id, list_pan
551
426
 
552
427
  # --apply-config: load YAML/JSON file and push settings to device
553
428
  if apply_config is not None:
554
- _apply_config(console, p, apply_config)
429
+ from ...config_apply import apply_config_file
430
+ apply_config_file(apply_config, host=host, ssh_key=ssh_key, console=console)
555
431
  return
556
432
 
557
433
  # --set-transform: apply rotation immediately and update kanshi config
@@ -0,0 +1,15 @@
1
+ """Shared config-apply layer for ``--apply-config`` across CLI commands.
2
+
3
+ Unifies the duplicated load-host-connect-apply logic found in
4
+ ``hardware``, ``device-map``, and ``blueprint`` commands.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from .applier import apply_config_dict, apply_config_file
9
+ from .loader import load_config_file
10
+
11
+ __all__ = [
12
+ "apply_config_dict",
13
+ "apply_config_file",
14
+ "load_config_file",
15
+ ]
@@ -0,0 +1,72 @@
1
+ """Apply a declarative config dict or file to a remote host."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+
6
+ from rich.console import Console
7
+
8
+ from ..apply.state_apply import apply_state, ApplyResult
9
+ from ..detect.remote import RemoteProbe
10
+ from .loader import load_config_file
11
+
12
+
13
+ def _normalize_hardware(data: dict) -> dict:
14
+ """Flatten nested hardware configs for state_apply handlers.
15
+
16
+ DeviceMap and DeviceBlueprint nest hardware under a ``hardware`` key,
17
+ while ``redeploy hardware`` produces a flat file. This helper makes
18
+ both shapes palatable for :func:`apply_state`.
19
+ """
20
+ if "drm_outputs" in data or "backlights" in data:
21
+ return data
22
+ hw = data.get("hardware")
23
+ if isinstance(hw, dict):
24
+ return hw
25
+ return data
26
+
27
+
28
+ def apply_config_dict(data: dict, probe: RemoteProbe, console: Console) -> ApplyResult:
29
+ """Apply *data* to the host behind *probe*.
30
+
31
+ Delegates to :func:`redeploy.apply.state_apply.apply_state` after
32
+ normalising nested ``hardware`` sections.
33
+ """
34
+ normalized = _normalize_hardware(data)
35
+ return apply_state(normalized, probe, console)
36
+
37
+
38
+ def apply_config_file(
39
+ path: str | Path,
40
+ *,
41
+ host: str | None = None,
42
+ ssh_key: str | None = None,
43
+ console: Console | None = None,
44
+ ) -> ApplyResult:
45
+ """Load *path* and apply its hardware/infra settings to the remote host.
46
+
47
+ Parameters
48
+ ----------
49
+ path:
50
+ Local YAML/JSON file (DeviceMap, DeviceBlueprint, or flat hardware dump).
51
+ host:
52
+ Optional SSH target. Falls back to ``data["host"]``.
53
+ ssh_key:
54
+ Private-key path for the SSH connection.
55
+ console:
56
+ Rich console for progress output. Created automatically when ``None``.
57
+
58
+ Raises
59
+ ------
60
+ ValueError
61
+ If no host can be determined (neither *host* nor ``data["host"]``).
62
+ """
63
+ data = load_config_file(path)
64
+ _host = host or data.get("host")
65
+ if not _host:
66
+ raise ValueError("No host specified and no 'host' field in config file")
67
+
68
+ if console is None:
69
+ console = Console()
70
+
71
+ probe = RemoteProbe(_host, ssh_key=ssh_key)
72
+ return apply_config_dict(data, probe, console)
@@ -0,0 +1,17 @@
1
+ """Load a declarative config file (YAML or JSON)."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from pathlib import Path
6
+
7
+ import yaml
8
+
9
+
10
+ def load_config_file(path: str | Path) -> dict:
11
+ """Read *path* and return a dict (YAML or JSON auto-detected)."""
12
+ path = Path(path)
13
+ raw = path.read_text(encoding="utf-8")
14
+ try:
15
+ return yaml.safe_load(raw) or {}
16
+ except Exception:
17
+ return json.loads(raw)
@@ -65,10 +65,52 @@ def snapshot_to_infra_state(snapshot: "Snapshot") -> "InfraState":
65
65
 
66
66
  def snapshot_to_hardware_info(snapshot: "Snapshot") -> "HardwareInfo":
67
67
  """Convert opstree.Snapshot -> redeploy.HardwareInfo."""
68
- from redeploy.models import HardwareInfo
68
+ from redeploy.models import HardwareInfo, DrmOutput, BacklightInfo
69
69
 
70
70
  physical = snapshot.layer("physical.display")
71
- # TODO: expand mapping as op3 layer schema grows
71
+ os_kernel = snapshot.layer("os.kernel")
72
+ os_config = snapshot.layer("os.config")
73
+
74
+ if physical is None:
75
+ return HardwareInfo()
76
+
77
+ # Map DRM outputs (op3 field names differ slightly from redeploy)
78
+ drm_raw = physical.data.get("drm_outputs", [])
79
+ drm_outputs = []
80
+ for d in drm_raw:
81
+ drm_outputs.append(DrmOutput(
82
+ name=d.get("name", ""),
83
+ connector=d.get("connector", ""),
84
+ status=d.get("status", "unknown"),
85
+ enabled=d.get("enabled", "unknown"),
86
+ modes=d.get("modes", []),
87
+ transform=d.get("transform", "normal"),
88
+ position=d.get("position", "0,0"),
89
+ scale=d.get("scale", "1.0"),
90
+ edid_bytes=d.get("edid_bytes", 0),
91
+ power_state=d.get("dpms"),
92
+ sysfs_path=f"/sys/class/drm/{d.get('name', '')}",
93
+ ))
94
+
95
+ # Map backlights
96
+ bl_raw = physical.data.get("backlights", [])
97
+ backlights = []
98
+ for b in bl_raw:
99
+ backlights.append(BacklightInfo(
100
+ name=b.get("name", ""),
101
+ brightness=b.get("brightness", 0),
102
+ max_brightness=b.get("max_brightness", 255),
103
+ bl_power=b.get("bl_power", 0),
104
+ display_name=b.get("display_name"),
105
+ sysfs_path=f"/sys/class/backlight/{b.get('name', '')}",
106
+ ))
107
+
72
108
  return HardwareInfo(
73
- device_model=physical.data.get("model") if physical else None,
109
+ board=physical.data.get("board_model"),
110
+ kernel=os_kernel.data.get("version") if os_kernel else None,
111
+ config_txt=os_config.data.get("config_txt", "") if os_config else "",
112
+ config_txt_path=os_config.data.get("config_txt_path", "/boot/firmware/config.txt") if os_config else "/boot/firmware/config.txt",
113
+ drm_outputs=drm_outputs,
114
+ backlights=backlights,
115
+ framebuffers=physical.data.get("framebuffers", []),
74
116
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redeploy
3
- Version: 0.2.44
3
+ Version: 0.2.45
4
4
  Summary: Infrastructure migration toolkit: detect → plan → apply
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -30,17 +30,17 @@ Requires-Dist: op3>=0.1.4; extra == "op3"
30
30
 
31
31
  ## AI Cost Tracking
32
32
 
33
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.44-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
34
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-21.4h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
33
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.45-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
34
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-23.2h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
35
35
 
36
- - 🤖 **LLM usage:** $7.5000 (65 commits)
37
- - 👤 **Human dev:** ~$2136 (21.4h @ $100/h, 30min dedup)
36
+ - 🤖 **LLM usage:** $7.5000 (66 commits)
37
+ - 👤 **Human dev:** ~$2318 (23.2h @ $100/h, 30min dedup)
38
38
 
39
39
  Generated on 2026-04-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
40
40
 
41
41
  ---
42
42
 
43
- ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.44-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
43
+ ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.45-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
44
44
 
45
45
  Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
46
46
 
@@ -68,6 +68,9 @@ redeploy/cli/commands/version/helpers.py
68
68
  redeploy/cli/commands/version/monorepo.py
69
69
  redeploy/cli/commands/version/release.py
70
70
  redeploy/cli/commands/version/scanner.py
71
+ redeploy/config_apply/__init__.py
72
+ redeploy/config_apply/applier.py
73
+ redeploy/config_apply/loader.py
71
74
  redeploy/detect/__init__.py
72
75
  redeploy/detect/detector.py
73
76
  redeploy/detect/hardware.py
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes