redeploy 0.2.40__tar.gz → 0.2.41__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 (172) hide show
  1. {redeploy-0.2.40 → redeploy-0.2.41}/PKG-INFO +6 -6
  2. {redeploy-0.2.40 → redeploy-0.2.41}/README.md +5 -5
  3. {redeploy-0.2.40 → redeploy-0.2.41}/pyproject.toml +1 -1
  4. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/__init__.py +1 -1
  5. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/blueprint.py +89 -6
  6. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/device_map.py +154 -16
  7. redeploy-0.2.41/redeploy/cli/commands/diagnose.py +47 -0
  8. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/diff.py +1 -5
  9. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/export.py +10 -10
  10. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/hardware.py +241 -8
  11. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/detect/hardware.py +71 -1
  12. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/detect/hardware_rules.py +227 -0
  13. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/models.py +20 -0
  14. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy.egg-info/PKG-INFO +6 -6
  15. {redeploy-0.2.40 → redeploy-0.2.41}/tests/test_hardware_rules.py +3 -3
  16. redeploy-0.2.40/redeploy/cli/commands/diagnose.py +0 -88
  17. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/apply/__init__.py +0 -0
  18. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/apply/exceptions.py +0 -0
  19. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/apply/executor.py +0 -0
  20. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/apply/handlers.py +0 -0
  21. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/apply/progress.py +0 -0
  22. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/apply/rollback.py +0 -0
  23. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/apply/state.py +0 -0
  24. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/apply/utils/__init__.py +0 -0
  25. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/apply/utils/run_container_build.py +0 -0
  26. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/audit.py +0 -0
  27. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/blueprint/__init__.py +0 -0
  28. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/blueprint/extractor.py +0 -0
  29. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/blueprint/generators/__init__.py +0 -0
  30. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/blueprint/generators/docker_compose.py +0 -0
  31. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/blueprint/generators/migration.py +0 -0
  32. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/__init__.py +0 -0
  33. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/__init__.py +0 -0
  34. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/audit.py +0 -0
  35. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/detect.py +0 -0
  36. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/devices.py +0 -0
  37. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/exec_.py +0 -0
  38. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/import_.py +0 -0
  39. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/init.py +0 -0
  40. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/inspect.py +0 -0
  41. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/patterns.py +0 -0
  42. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/plan_apply.py +0 -0
  43. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/plugin.py +0 -0
  44. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/probe.py +0 -0
  45. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/state.py +0 -0
  46. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/status.py +0 -0
  47. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/target.py +0 -0
  48. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/version/__init__.py +0 -0
  49. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/version/commands.py +0 -0
  50. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/version/helpers.py +0 -0
  51. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/version/monorepo.py +0 -0
  52. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/version/release.py +0 -0
  53. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/version/scanner.py +0 -0
  54. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/workflow.py +0 -0
  55. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/core.py +0 -0
  56. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/display.py +0 -0
  57. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli.py +0 -0
  58. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/data_sync.py +0 -0
  59. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/detect/__init__.py +0 -0
  60. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/detect/builtin/__init__.py +0 -0
  61. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/detect/detector.py +0 -0
  62. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/detect/probes.py +0 -0
  63. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/detect/remote.py +0 -0
  64. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/detect/templates.py +0 -0
  65. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/detect/workflow.py +0 -0
  66. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/discovery.py +0 -0
  67. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/dsl/__init__.py +0 -0
  68. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/dsl/loader.py +0 -0
  69. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/dsl/parser.py +0 -0
  70. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/dsl_python/__init__.py +0 -0
  71. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/dsl_python/context.py +0 -0
  72. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/dsl_python/decorators.py +0 -0
  73. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/dsl_python/docker_steps.py +0 -0
  74. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/dsl_python/exceptions.py +0 -0
  75. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/dsl_python/runner.py +0 -0
  76. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/dsl_python/steps.py +0 -0
  77. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/fleet.py +0 -0
  78. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/hardware/__init__.py +0 -0
  79. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/hardware/config_txt.py +0 -0
  80. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/hardware/data/__init__.py +0 -0
  81. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/hardware/data/hyperpixel.py +0 -0
  82. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/hardware/data/official.py +0 -0
  83. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/hardware/data/waveshare.py +0 -0
  84. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/hardware/fixes.py +0 -0
  85. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/hardware/panels.py +0 -0
  86. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/hardware/raspi_config.py +0 -0
  87. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/iac/__init__.py +0 -0
  88. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/iac/base.py +0 -0
  89. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/iac/docker_compose.py +0 -0
  90. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/iac/parsers/__init__.py +0 -0
  91. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/iac/parsers/compose.py +0 -0
  92. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/iac/registry.py +0 -0
  93. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/markpact/__init__.py +0 -0
  94. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/markpact/compiler.py +0 -0
  95. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/markpact/models.py +0 -0
  96. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/markpact/parser.py +0 -0
  97. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/observe.py +0 -0
  98. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/parse.py +0 -0
  99. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/patterns.py +0 -0
  100. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/plan/__init__.py +0 -0
  101. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/plan/planner.py +0 -0
  102. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/plugins/__init__.py +0 -0
  103. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/plugins/builtin/__init__.py +0 -0
  104. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/plugins/builtin/browser_reload.py +0 -0
  105. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/plugins/builtin/hardware_diagnostic.py +0 -0
  106. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/plugins/builtin/notify.py +0 -0
  107. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/plugins/builtin/process_control.py +0 -0
  108. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/plugins/builtin/systemd_reload.py +0 -0
  109. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/spec_loader.py +0 -0
  110. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/ssh.py +0 -0
  111. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/steps.py +0 -0
  112. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/__init__.py +0 -0
  113. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_audit.py +0 -0
  114. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_cli.py +0 -0
  115. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_data_sync.py +0 -0
  116. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_discovery.py +0 -0
  117. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_dsl.py +0 -0
  118. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_examples.py +0 -0
  119. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_executor.py +0 -0
  120. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_fleet.py +0 -0
  121. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_git_integration.py +0 -0
  122. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_iac.py +0 -0
  123. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_manifest.py +0 -0
  124. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_markpact_compiler.py +0 -0
  125. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_markpact_parser.py +0 -0
  126. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_models.py +0 -0
  127. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_observe.py +0 -0
  128. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_parse.py +0 -0
  129. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_patterns.py +0 -0
  130. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_planner_edge.py +0 -0
  131. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_plugins.py +0 -0
  132. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_probes.py +0 -0
  133. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_public_api.py +0 -0
  134. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_resume.py +0 -0
  135. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_spec_loader.py +0 -0
  136. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_ssh.py +0 -0
  137. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_templates.py +0 -0
  138. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_verify.py +0 -0
  139. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_version.py +0 -0
  140. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_version_changelog.py +0 -0
  141. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_version_cli.py +0 -0
  142. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_version_manifest.py +0 -0
  143. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_version_sources.py +0 -0
  144. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_workflow.py +0 -0
  145. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/verify.py +0 -0
  146. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/version/__init__.py +0 -0
  147. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/version/bump.py +0 -0
  148. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/version/changelog.py +0 -0
  149. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/version/commits.py +0 -0
  150. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/version/diff.py +0 -0
  151. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/version/git_integration.py +0 -0
  152. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/version/git_transaction.py +0 -0
  153. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/version/manifest.py +0 -0
  154. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/version/sources/__init__.py +0 -0
  155. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/version/sources/base.py +0 -0
  156. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/version/sources/json_.py +0 -0
  157. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/version/sources/plain.py +0 -0
  158. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/version/sources/regex.py +0 -0
  159. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/version/sources/toml_.py +0 -0
  160. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/version/sources/yaml_.py +0 -0
  161. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/version/transaction.py +0 -0
  162. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/version.py +0 -0
  163. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy.egg-info/SOURCES.txt +0 -0
  164. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy.egg-info/dependency_links.txt +0 -0
  165. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy.egg-info/entry_points.txt +0 -0
  166. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy.egg-info/requires.txt +0 -0
  167. {redeploy-0.2.40 → redeploy-0.2.41}/redeploy.egg-info/top_level.txt +0 -0
  168. {redeploy-0.2.40 → redeploy-0.2.41}/setup.cfg +0 -0
  169. {redeploy-0.2.40 → redeploy-0.2.41}/tests/test_hardware_config.py +0 -0
  170. {redeploy-0.2.40 → redeploy-0.2.41}/tests/test_iac.py +0 -0
  171. {redeploy-0.2.40 → redeploy-0.2.41}/tests/test_parse.py +0 -0
  172. {redeploy-0.2.40 → redeploy-0.2.41}/tests/test_redeploy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redeploy
3
- Version: 0.2.40
3
+ Version: 0.2.41
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.40-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
31
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-19.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.41-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-20.4h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
32
32
 
33
- - 🤖 **LLM usage:** $7.5000 (61 commits)
34
- - 👤 **Human dev:** ~$1974 (19.7h @ $100/h, 30min dedup)
33
+ - 🤖 **LLM usage:** $7.5000 (62 commits)
34
+ - 👤 **Human dev:** ~$2044 (20.4h @ $100/h, 30min dedup)
35
35
 
36
36
  Generated on 2026-04-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
37
37
 
38
38
  ---
39
39
 
40
- ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.40-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.41-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.40-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-19.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.41-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-20.4h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $7.5000 (61 commits)
10
- - 👤 **Human dev:** ~$1974 (19.7h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $7.5000 (62 commits)
10
+ - 👤 **Human dev:** ~$2044 (20.4h @ $100/h, 30min dedup)
11
11
 
12
12
  Generated on 2026-04-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
13
13
 
14
14
  ---
15
15
 
16
- ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.40-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.41-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.40"
7
+ version = "0.2.41"
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.40"
10
+ __version__ = "0.2.41"
11
11
 
12
12
  # ── Core models ───────────────────────────────────────────────────────────────
13
13
  from .models import ( # noqa: F401
@@ -53,7 +53,7 @@ def blueprint_cmd():
53
53
  @click.option("--save", is_flag=True, help="Save to ~/.config/redeploy/blueprints/")
54
54
  @click.option("--out", "out_path", default=None, type=click.Path(),
55
55
  help="Save to specific file instead")
56
- @click.option("--format", "fmt", default="yaml", type=click.Choice(["rich", "yaml", "json"]))
56
+ @click.option("--format", "fmt", default="yaml", type=click.Choice(["yaml", "json"]))
57
57
  @click.option("--tag", "tags", multiple=True)
58
58
  def capture(host, name, compose_files, migration_file, device_map_file,
59
59
  live, save, out_path, fmt, tags):
@@ -83,7 +83,11 @@ def capture(host, name, compose_files, migration_file, device_map_file,
83
83
  host=host,
84
84
  )
85
85
 
86
- _print_blueprint(console, bp, fmt)
86
+ if fmt == "json":
87
+ import json as _json
88
+ click.echo(_json.dumps(bp.model_dump(mode="json"), indent=2))
89
+ else:
90
+ click.echo(bp.to_yaml())
87
91
 
88
92
  if save or out_path:
89
93
  path = Path(out_path) if out_path else None
@@ -91,6 +95,68 @@ def capture(host, name, compose_files, migration_file, device_map_file,
91
95
  console.print(f"\n[green]✓ saved:[/green] {saved}")
92
96
 
93
97
 
98
+ def _apply_blueprint_config(bp, config_path):
99
+ """Apply blueprint settings from YAML config file to the remote host.
100
+
101
+ Reads blueprint YAML/JSON and applies hardware settings (transforms, backlight, etc.).
102
+ """
103
+ import yaml
104
+ import json as _json
105
+
106
+ from ...remote import Remote
107
+
108
+ with open(config_path) as f:
109
+ raw = f.read()
110
+
111
+ try:
112
+ cfg = yaml.safe_load(raw)
113
+ except Exception:
114
+ cfg = _json.loads(raw)
115
+
116
+ host = cfg.get("host") or getattr(bp, "host", None)
117
+ if not host:
118
+ print("[red]✗ No 'host' field in config file[/red]")
119
+ return
120
+
121
+ print(f"[cyan]→ Applying blueprint config from {config_path} to {host}[/cyan]")
122
+
123
+ p = Remote(host)
124
+
125
+ # Apply hardware settings (reuse logic from device-map)
126
+ hardware = cfg.get("hardware", {})
127
+ applied = 0
128
+
129
+ for output in hardware.get("drm_outputs", []):
130
+ connector = output.get("connector", "")
131
+ transform = output.get("transform", "normal")
132
+ if not connector or not ("DSI" in connector or "HDMI" in connector):
133
+ continue
134
+
135
+ if transform != "normal":
136
+ wlr_cmd = (
137
+ f"WAYLAND_DISPLAY=wayland-0 XDG_RUNTIME_DIR=/run/user/$(id -u) "
138
+ f"wlr-randr --output {connector} --transform {transform} 2>&1"
139
+ )
140
+ r = p.run(wlr_cmd)
141
+ if r.ok:
142
+ print(f"[green] ✓ {connector}: transform={transform}[/green]")
143
+ applied += 1
144
+
145
+ for bl in hardware.get("backlights", []):
146
+ name = bl.get("name", "")
147
+ brightness = bl.get("brightness")
148
+ if name and brightness is not None:
149
+ r = p.run(f"echo {brightness} | sudo tee /sys/class/backlight/{name}/brightness > /dev/null")
150
+ if r.ok:
151
+ print(f"[green] ✓ backlight {name}: brightness={brightness}[/green]")
152
+ applied += 1
153
+
154
+ if applied > 0:
155
+ print(f"[bold green]✓ Blueprint config applied from {config_path}[/bold green]")
156
+ else:
157
+ print("[yellow]Nothing to apply[/yellow]")
158
+
159
+
94
160
  # ── twin ──────────────────────────────────────────────────────────────────────
95
161
 
96
162
  @blueprint_cmd.command("twin")
@@ -180,13 +246,30 @@ def deploy(blueprint_file, target_host, out_path, remote_dir, no_transfer,
180
246
 
181
247
  @blueprint_cmd.command("show")
182
248
  @click.argument("blueprint_file", type=click.Path(exists=True))
183
- @click.option("--format", "fmt", default="yaml", type=click.Choice(["rich", "yaml", "json"]))
184
- def show(blueprint_file, fmt):
185
- """Display a saved DeviceBlueprint."""
249
+ @click.option("--format", "fmt", default="yaml", type=click.Choice(["yaml", "json"]))
250
+ @click.option("--apply-config", "apply_config", default=None,
251
+ type=click.Path(exists=True, dir_okay=False),
252
+ help="Apply blueprint settings from YAML config file to the remote host")
253
+ def show(blueprint_file, fmt, apply_config):
254
+ """Display a saved DeviceBlueprint.
255
+
256
+ \b
257
+ Config-file workflow (scan → edit → apply):
258
+ redeploy blueprint capture pi@192.168.188.109 > blueprint.yaml
259
+ # edit blueprint.yaml: set hardware.drm_outputs[0].transform: '270'
260
+ redeploy blueprint show blueprint.yaml --apply-config blueprint.yaml
261
+ """
186
262
  from ...models import DeviceBlueprint
187
263
 
188
264
  bp = DeviceBlueprint.load(Path(blueprint_file))
189
- _print_blueprint(Console(), bp, fmt)
265
+ if fmt == "json":
266
+ import json as _json
267
+ click.echo(_json.dumps(bp.model_dump(mode="json"), indent=2))
268
+ else:
269
+ click.echo(bp.to_yaml())
270
+
271
+ if apply_config:
272
+ _apply_blueprint_config(bp, apply_config)
190
273
 
191
274
 
192
275
  # ── list ──────────────────────────────────────────────────────────────────────
@@ -39,22 +39,21 @@ from rich.table import Table
39
39
  @click.option("--tag", "tags", multiple=True, help="Tag(s) to attach (repeatable)")
40
40
  @click.option("--save", is_flag=True, help="Persist map to ~/.config/redeploy/device-maps/")
41
41
  @click.option("--out", "out_path", default=None, type=click.Path(), help="Save to specific file")
42
- @click.option(
43
- "--format", "output_fmt",
44
- default="yaml",
45
- type=click.Choice(["rich", "yaml", "json"]),
46
- help="Output format",
47
- )
42
+ @click.option("--format", "output_fmt", default="yaml",
43
+ type=click.Choice(["yaml", "json"]), help="Output format (default: yaml)")
48
44
  @click.option("--no-infra", is_flag=True, help="Skip infra probe (faster — hardware only)")
49
45
  @click.option("--list", "list_saved", is_flag=True, help="List saved device maps")
50
46
  @click.option("--show", "show_file", default=None, type=click.Path(exists=True),
51
47
  help="Load and display a saved device-map file")
52
48
  @click.option("--diff", "diff_files", nargs=2, type=click.Path(exists=True),
53
49
  help="Diff two saved device-map files")
50
+ @click.option("--apply-config", "apply_config", default=None,
51
+ type=click.Path(exists=True, dir_okay=False),
52
+ help="Apply hardware/infra settings from YAML config file to the remote host")
54
53
  @click.option("--ssh-key", default=None, type=click.Path(), help="SSH private key path")
55
54
  def device_map_cmd(
56
55
  host, name, tags, save, out_path, output_fmt,
57
- no_infra, list_saved, show_file, diff_files, ssh_key,
56
+ no_infra, list_saved, show_file, diff_files, ssh_key, apply_config,
58
57
  ):
59
58
  """Generate a full standardized device snapshot (hardware + infra + diagnostics).
60
59
 
@@ -65,9 +64,14 @@ def device_map_cmd(
65
64
  Examples:
66
65
  redeploy device-map pi@192.168.188.109
67
66
  redeploy device-map pi@192.168.188.109 --save --name "kiosk-lab"
68
- redeploy device-map pi@192.168.188.109 --format yaml > rpi5.yaml
69
67
  redeploy device-map --list
70
68
  redeploy device-map --show ~/.config/redeploy/device-maps/pi_at_192.168.188.109.yaml
69
+
70
+ \b
71
+ Config-file workflow (scan → edit → apply):
72
+ redeploy device-map pi@192.168.188.109 > device-map.yaml
73
+ # edit device-map.yaml: set hardware.drm_outputs[0].transform: '270'
74
+ redeploy device-map pi@192.168.188.109 --apply-config device-map.yaml
71
75
  """
72
76
  from ...models import DeviceMap
73
77
 
@@ -81,7 +85,7 @@ def device_map_cmd(
81
85
  # ── --show ────────────────────────────────────────────────────────────────
82
86
  if show_file:
83
87
  dm = DeviceMap.load(Path(show_file))
84
- _render(console, dm, output_fmt)
88
+ click.echo(dm.to_yaml())
85
89
  return
86
90
 
87
91
  # ── --diff ────────────────────────────────────────────────────────────────
@@ -153,14 +157,11 @@ def device_map_cmd(
153
157
  )
154
158
 
155
159
  # ── output ────────────────────────────────────────────────────────────────
156
- if output_fmt in ("yaml", "json"):
157
- if output_fmt == "yaml":
158
- click.echo(dm.to_yaml())
159
- else:
160
- import json as _json
161
- click.echo(_json.dumps(dm.model_dump(mode="json"), indent=2))
160
+ if output_fmt == "json":
161
+ import json as _json
162
+ click.echo(_json.dumps(dm.model_dump(mode="json"), indent=2))
162
163
  else:
163
- _render(console, dm, "rich")
164
+ click.echo(dm.to_yaml())
164
165
 
165
166
  # ── save ──────────────────────────────────────────────────────────────────
166
167
  if save or out_path:
@@ -172,6 +173,143 @@ def device_map_cmd(
172
173
  sys.exit(1)
173
174
 
174
175
 
176
+ def _apply_config_from_file(console, config_path, ssh_key):
177
+ """Apply hardware/infra settings from YAML/JSON config file to the remote host.
178
+
179
+ Reads DeviceMap YAML/JSON and applies:
180
+ - Display transforms via wlr-randr + kanshi
181
+ - Backlight settings
182
+ - Other configurable hardware parameters
183
+ """
184
+ import yaml
185
+ import json as _json
186
+
187
+ from ...remote import Remote
188
+
189
+ with open(config_path) as f:
190
+ raw = f.read()
191
+
192
+ try:
193
+ cfg = yaml.safe_load(raw)
194
+ except Exception:
195
+ cfg = _json.loads(raw)
196
+
197
+ # Extract host from config or prompt
198
+ host = cfg.get("host")
199
+ if not host:
200
+ console.print("[red]✗ No 'host' field in config file[/red]")
201
+ sys.exit(1)
202
+
203
+ console.print(f"[cyan]→ Applying config from {config_path} to {host}[/cyan]")
204
+
205
+ # Connect to remote
206
+ p = Remote(host, ssh_key=ssh_key)
207
+
208
+ applied = 0
209
+
210
+ # ── display transforms ────────────────────────────────────────────────────
211
+ hardware = cfg.get("hardware", {})
212
+ for output in hardware.get("drm_outputs", []):
213
+ connector = output.get("connector", "")
214
+ transform = output.get("transform", "normal")
215
+ if not connector or not ("DSI" in connector or "HDMI" in connector):
216
+ continue
217
+
218
+ if transform == "normal":
219
+ console.print(f"[dim] skip {connector}: transform=normal (default)[/dim]")
220
+ continue
221
+
222
+ console.print(f"[cyan]→ {connector}: transform={transform}[/cyan]")
223
+
224
+ # Apply via wlr-randr
225
+ wlr_cmd = (
226
+ f"WAYLAND_DISPLAY=wayland-0 XDG_RUNTIME_DIR=/run/user/$(id -u) "
227
+ f"wlr-randr --output {connector} --transform {transform} 2>&1"
228
+ )
229
+ r = p.run(wlr_cmd)
230
+ if r.ok:
231
+ console.print(f"[green] ✓ wlr-randr applied[/green]")
232
+ applied += 1
233
+ else:
234
+ console.print(f"[yellow] ⚠ wlr-randr: {r.out.strip() or 'no output'}[/yellow]")
235
+
236
+ # ── persist DSI transforms in kanshi ─────────────────────────────────────────
237
+ dsi_outputs = [
238
+ o for o in hardware.get("drm_outputs", [])
239
+ if "DSI" in o.get("connector", "")
240
+ ]
241
+ if dsi_outputs:
242
+ _update_kanshi_from_cfg(console, p, dsi_outputs)
243
+
244
+ # ── backlight ─────────────────────────────────────────────────────────────
245
+ for bl in hardware.get("backlights", []):
246
+ name = bl.get("name", "")
247
+ brightness = bl.get("brightness")
248
+ bl_power = bl.get("bl_power")
249
+ if not name:
250
+ continue
251
+
252
+ if brightness is not None:
253
+ r = p.run(f"echo {brightness} | sudo tee /sys/class/backlight/{name}/brightness > /dev/null")
254
+ if r.ok:
255
+ console.print(f"[green] ✓ backlight {name}: brightness={brightness}[/green]")
256
+ applied += 1
257
+
258
+ if bl_power is not None:
259
+ r = p.run(f"echo {bl_power} | sudo tee /sys/class/backlight/{name}/bl_power > /dev/null")
260
+ if r.ok:
261
+ console.print(f"[green] ✓ backlight {name}: bl_power={bl_power}[/green]")
262
+ applied += 1
263
+
264
+ if applied == 0:
265
+ console.print("[yellow]Nothing to apply — no relevant settings found in config[/yellow]")
266
+ else:
267
+ console.print(f"\n[bold green]✓ Config applied from {config_path}[/bold green]")
268
+
269
+
270
+ def _update_kanshi_from_cfg(console, p, dsi_outputs: list):
271
+ """Rebuild kanshi profile from drm_outputs list and write to ~/.config/kanshi/config."""
272
+ import re as _re
273
+
274
+ kanshi_cfg_path = "~/.config/kanshi/config"
275
+ read_r = p.run(f"cat {kanshi_cfg_path} 2>/dev/null")
276
+ current = read_r.out if read_r.ok else ""
277
+
278
+ for output in dsi_outputs:
279
+ connector = output.get("connector", "")
280
+ transform = output.get("transform", "normal")
281
+
282
+ output_line_pat = _re.compile(
283
+ rf'(\s*output\s+{_re.escape(connector)}\s+enable)(\s+transform\s+\S+)?'
284
+ )
285
+ if _re.search(output_line_pat, current):
286
+ if transform == "normal":
287
+ current = _re.sub(output_line_pat, r'\1', current)
288
+ else:
289
+ current = _re.sub(output_line_pat, rf'\1 transform {transform}', current)
290
+ elif current.strip():
291
+ current = _re.sub(
292
+ rf'(\s*output\s+{_re.escape(connector)}\b)',
293
+ rf'\1 transform {transform}' if transform != "normal" else r'\1',
294
+ current,
295
+ )
296
+ else:
297
+ current = (
298
+ f"profile waveshare-only {{\n"
299
+ f" output {connector} enable"
300
+ + (f" transform {transform}" if transform != "normal" else "")
301
+ + "\n"
302
+ + " output HDMI-A-2 disable\n"
303
+ + "}\n"
304
+ )
305
+
306
+ escaped = current.replace("'", "'\\''")
307
+ write_r = p.run(f"mkdir -p ~/.config/kanshi && printf '%s' '{escaped}' > {kanshi_cfg_path}")
308
+ if write_r.ok:
309
+ console.print(f"[green] ✓ kanshi config updated ({kanshi_cfg_path})[/green]")
310
+ p.run("pkill -SIGUSR1 kanshi 2>/dev/null || true")
311
+
312
+
175
313
  # ── rendering helpers ─────────────────────────────────────────────────────────
176
314
 
177
315
 
@@ -0,0 +1,47 @@
1
+ """diagnose command — Compare a migration spec against the live target host."""
2
+ from __future__ import annotations
3
+
4
+ import sys
5
+
6
+ import click
7
+ import yaml
8
+
9
+
10
+ @click.command()
11
+ @click.argument("spec", type=click.Path(exists=True, dir_okay=False))
12
+ @click.option("--host", default=None, help="Override target host (default: spec.target.host)")
13
+ @click.option("--ssh-key", default=None, type=click.Path(), help="Path to SSH private key")
14
+ @click.option("--format", "output_fmt", default="yaml", type=click.Choice(["yaml", "json"]), help="Output format (default: yaml)")
15
+ @click.option("--exit-on-fail", is_flag=True, help="Exit with non-zero status if any check fails")
16
+ @click.pass_context
17
+ def diagnose(ctx, spec, host, ssh_key, output_fmt, exit_on_fail):
18
+ """Compare a migration spec against the live target host.
19
+
20
+ Walks the spec (YAML or markpact .md), derives all expected facts
21
+ (binaries, directories, ports, container images, systemd units,
22
+ apt packages, env files, free disk) and probes the target read-only
23
+ via SSH. Reports what is missing or out of spec — without applying
24
+ any change.
25
+
26
+ \b
27
+ Examples:
28
+ redeploy diagnose migration.podman-rpi5-resume.md
29
+ redeploy diagnose migration.yaml --host pi@192.168.188.108
30
+ redeploy diagnose migration.yaml --format json
31
+ """
32
+ from ...audit import audit_spec
33
+
34
+ try:
35
+ report = audit_spec(spec, host=host, ssh_key=ssh_key)
36
+ except Exception as exc:
37
+ click.echo(f"audit failed: {exc}", err=True)
38
+ sys.exit(2)
39
+
40
+ if output_fmt == "json":
41
+ import json as _json
42
+ click.echo(_json.dumps(report.to_dict(), indent=2))
43
+ else:
44
+ click.echo(yaml.safe_dump(report.to_dict(), sort_keys=False))
45
+
46
+ if exit_on_fail and not report.ok:
47
+ sys.exit(1)
@@ -19,11 +19,7 @@ from rich.console import Console
19
19
  "--to", "to_src", default=None,
20
20
  help="Right side: IaC file/directory or SSH host"
21
21
  )
22
- @click.option(
23
- "--format", "out_format", default="json",
24
- type=click.Choice(["text", "json"]), show_default=True
25
- )
26
- def diff(ci_file, host, from_src, to_src, out_format):
22
+ def diff(ci_file, host, from_src, to_src):
27
23
  """Compare IaC file vs live host (drift detection). [Phase 3 — coming soon]
28
24
 
29
25
  \b
@@ -9,10 +9,6 @@ from rich.console import Console
9
9
 
10
10
 
11
11
  @click.command("export")
12
- @click.option(
13
- "--format", "fmt", default="yaml", type=click.Choice(["css", "yaml"]),
14
- help="Output format (css or yaml)"
15
- )
16
12
  @click.option(
17
13
  "-o", "--output", default=None, type=click.Path(),
18
14
  help="Output file (default: print to stdout)"
@@ -21,19 +17,23 @@ from rich.console import Console
21
17
  "--file", "src_file", default=None, type=click.Path(),
22
18
  help="Source file to convert (auto-detected if omitted)"
23
19
  )
20
+ @click.option(
21
+ "--format", "fmt", default="yaml", type=click.Choice(["yaml", "css"]),
22
+ help="Output format (default: yaml)"
23
+ )
24
24
  @click.pass_context
25
- def export_cmd(ctx, fmt, output, src_file):
25
+ def export_cmd(ctx, output, src_file, fmt):
26
26
  """Convert between redeploy.css and redeploy.yaml formats.
27
27
 
28
- Reads the nearest redeploy.css or redeploy.yaml and exports to the
29
- requested format. Useful for migration, sharing, and LLM inspection.
28
+ Reads the nearest redeploy.css or redeploy.yaml and exports to YAML.
29
+ Useful for migration, sharing, and LLM inspection.
30
30
 
31
31
  \b
32
32
  Examples:
33
+ redeploy export # auto-detect source → yaml (stdout)
34
+ redeploy export -o redeploy.yaml # auto-detect source → yaml (file)
35
+ redeploy export --file redeploy.css # css → yaml
33
36
  redeploy export --format css # yaml → css (stdout)
34
- redeploy export --format css -o redeploy.css
35
- redeploy export --format yaml -o redeploy.yaml
36
- redeploy export --format yaml --file redeploy.css
37
37
  """
38
38
  console = Console(stderr=True)
39
39