redeploy 0.2.45__tar.gz → 0.2.46__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 (185) hide show
  1. {redeploy-0.2.45 → redeploy-0.2.46}/PKG-INFO +6 -6
  2. {redeploy-0.2.45 → redeploy-0.2.46}/README.md +5 -5
  3. {redeploy-0.2.45 → redeploy-0.2.46}/pyproject.toml +1 -1
  4. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/__init__.py +1 -1
  5. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/apply/__init__.py +10 -0
  6. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/apply/handlers.py +109 -27
  7. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/blueprint.py +3 -63
  8. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/device_map.py +6 -250
  9. redeploy-0.2.46/redeploy/cli/commands/device_map_renderers.py +170 -0
  10. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/hardware.py +41 -2
  11. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/models.py +9 -0
  12. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy.egg-info/PKG-INFO +6 -6
  13. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy.egg-info/SOURCES.txt +1 -0
  14. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/apply/exceptions.py +0 -0
  15. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/apply/executor.py +0 -0
  16. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/apply/progress.py +0 -0
  17. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/apply/rollback.py +0 -0
  18. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/apply/state.py +0 -0
  19. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/apply/state_apply.py +0 -0
  20. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/apply/utils/__init__.py +0 -0
  21. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/apply/utils/run_container_build.py +0 -0
  22. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/audit.py +0 -0
  23. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/blueprint/__init__.py +0 -0
  24. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/blueprint/extractor.py +0 -0
  25. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/blueprint/generators/__init__.py +0 -0
  26. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/blueprint/generators/docker_compose.py +0 -0
  27. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/blueprint/generators/migration.py +0 -0
  28. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/__init__.py +0 -0
  29. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/__init__.py +0 -0
  30. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/audit.py +0 -0
  31. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/detect.py +0 -0
  32. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/devices.py +0 -0
  33. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/diagnose.py +0 -0
  34. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/diff.py +0 -0
  35. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/exec_.py +0 -0
  36. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/export.py +0 -0
  37. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/import_.py +0 -0
  38. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/init.py +0 -0
  39. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/inspect.py +0 -0
  40. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/patterns.py +0 -0
  41. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/plan_apply.py +0 -0
  42. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/plugin.py +0 -0
  43. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/probe.py +0 -0
  44. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/push.py +0 -0
  45. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/state.py +0 -0
  46. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/status.py +0 -0
  47. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/target.py +0 -0
  48. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/version/__init__.py +0 -0
  49. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/version/commands.py +0 -0
  50. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/version/helpers.py +0 -0
  51. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/version/monorepo.py +0 -0
  52. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/version/release.py +0 -0
  53. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/version/scanner.py +0 -0
  54. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/workflow.py +0 -0
  55. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/core.py +0 -0
  56. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/display.py +0 -0
  57. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli.py +0 -0
  58. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/config_apply/__init__.py +0 -0
  59. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/config_apply/applier.py +0 -0
  60. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/config_apply/loader.py +0 -0
  61. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/data_sync.py +0 -0
  62. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/detect/__init__.py +0 -0
  63. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/detect/builtin/__init__.py +0 -0
  64. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/detect/detector.py +0 -0
  65. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/detect/hardware.py +0 -0
  66. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/detect/hardware_rules.py +0 -0
  67. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/detect/probes.py +0 -0
  68. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/detect/remote.py +0 -0
  69. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/detect/templates.py +0 -0
  70. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/detect/workflow.py +0 -0
  71. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/discovery.py +0 -0
  72. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/dsl/__init__.py +0 -0
  73. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/dsl/loader.py +0 -0
  74. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/dsl/parser.py +0 -0
  75. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/dsl_python/__init__.py +0 -0
  76. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/dsl_python/context.py +0 -0
  77. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/dsl_python/decorators.py +0 -0
  78. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/dsl_python/docker_steps.py +0 -0
  79. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/dsl_python/exceptions.py +0 -0
  80. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/dsl_python/runner.py +0 -0
  81. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/dsl_python/steps.py +0 -0
  82. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/fleet.py +0 -0
  83. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/hardware/__init__.py +0 -0
  84. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/hardware/config_txt.py +0 -0
  85. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/hardware/data/__init__.py +0 -0
  86. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/hardware/data/hyperpixel.py +0 -0
  87. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/hardware/data/official.py +0 -0
  88. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/hardware/data/waveshare.py +0 -0
  89. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/hardware/fixes.py +0 -0
  90. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/hardware/kiosk/__init__.py +0 -0
  91. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/hardware/kiosk/autostart.py +0 -0
  92. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/hardware/kiosk/browsers.py +0 -0
  93. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/hardware/kiosk/compositors.py +0 -0
  94. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/hardware/kiosk/output_profiles.py +0 -0
  95. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/hardware/panels.py +0 -0
  96. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/hardware/raspi_config.py +0 -0
  97. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/iac/__init__.py +0 -0
  98. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/iac/base.py +0 -0
  99. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/iac/docker_compose.py +0 -0
  100. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/iac/parsers/__init__.py +0 -0
  101. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/iac/parsers/compose.py +0 -0
  102. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/iac/registry.py +0 -0
  103. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/integrations/__init__.py +0 -0
  104. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/integrations/op3_bridge.py +0 -0
  105. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/markpact/__init__.py +0 -0
  106. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/markpact/compiler.py +0 -0
  107. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/markpact/models.py +0 -0
  108. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/markpact/parser.py +0 -0
  109. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/observe.py +0 -0
  110. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/parse.py +0 -0
  111. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/patterns.py +0 -0
  112. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/plan/__init__.py +0 -0
  113. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/plan/planner.py +0 -0
  114. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/plugins/__init__.py +0 -0
  115. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/plugins/builtin/__init__.py +0 -0
  116. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/plugins/builtin/browser_reload.py +0 -0
  117. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/plugins/builtin/hardware_diagnostic.py +0 -0
  118. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/plugins/builtin/notify.py +0 -0
  119. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/plugins/builtin/process_control.py +0 -0
  120. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/plugins/builtin/systemd_reload.py +0 -0
  121. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/spec_loader.py +0 -0
  122. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/ssh.py +0 -0
  123. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/steps.py +0 -0
  124. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/__init__.py +0 -0
  125. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_audit.py +0 -0
  126. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_cli.py +0 -0
  127. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_data_sync.py +0 -0
  128. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_discovery.py +0 -0
  129. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_dsl.py +0 -0
  130. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_examples.py +0 -0
  131. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_executor.py +0 -0
  132. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_fleet.py +0 -0
  133. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_git_integration.py +0 -0
  134. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_iac.py +0 -0
  135. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_manifest.py +0 -0
  136. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_markpact_compiler.py +0 -0
  137. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_markpact_parser.py +0 -0
  138. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_models.py +0 -0
  139. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_observe.py +0 -0
  140. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_parse.py +0 -0
  141. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_patterns.py +0 -0
  142. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_planner_edge.py +0 -0
  143. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_plugins.py +0 -0
  144. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_probes.py +0 -0
  145. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_public_api.py +0 -0
  146. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_resume.py +0 -0
  147. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_spec_loader.py +0 -0
  148. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_ssh.py +0 -0
  149. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_templates.py +0 -0
  150. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_verify.py +0 -0
  151. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_version.py +0 -0
  152. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_version_changelog.py +0 -0
  153. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_version_cli.py +0 -0
  154. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_version_manifest.py +0 -0
  155. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_version_sources.py +0 -0
  156. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_workflow.py +0 -0
  157. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/verify.py +0 -0
  158. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/version/__init__.py +0 -0
  159. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/version/bump.py +0 -0
  160. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/version/changelog.py +0 -0
  161. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/version/commits.py +0 -0
  162. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/version/diff.py +0 -0
  163. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/version/git_integration.py +0 -0
  164. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/version/git_transaction.py +0 -0
  165. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/version/manifest.py +0 -0
  166. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/version/sources/__init__.py +0 -0
  167. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/version/sources/base.py +0 -0
  168. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/version/sources/json_.py +0 -0
  169. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/version/sources/plain.py +0 -0
  170. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/version/sources/regex.py +0 -0
  171. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/version/sources/toml_.py +0 -0
  172. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/version/sources/yaml_.py +0 -0
  173. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/version/transaction.py +0 -0
  174. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/version.py +0 -0
  175. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy.egg-info/dependency_links.txt +0 -0
  176. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy.egg-info/entry_points.txt +0 -0
  177. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy.egg-info/requires.txt +0 -0
  178. {redeploy-0.2.45 → redeploy-0.2.46}/redeploy.egg-info/top_level.txt +0 -0
  179. {redeploy-0.2.45 → redeploy-0.2.46}/setup.cfg +0 -0
  180. {redeploy-0.2.45 → redeploy-0.2.46}/tests/test_hardware_config.py +0 -0
  181. {redeploy-0.2.45 → redeploy-0.2.46}/tests/test_hardware_kiosk.py +0 -0
  182. {redeploy-0.2.45 → redeploy-0.2.46}/tests/test_hardware_rules.py +0 -0
  183. {redeploy-0.2.45 → redeploy-0.2.46}/tests/test_iac.py +0 -0
  184. {redeploy-0.2.45 → redeploy-0.2.46}/tests/test_parse.py +0 -0
  185. {redeploy-0.2.45 → redeploy-0.2.46}/tests/test_redeploy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redeploy
3
- Version: 0.2.45
3
+ Version: 0.2.46
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.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)
33
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.46-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.6h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
35
35
 
36
- - 🤖 **LLM usage:** $7.5000 (66 commits)
37
- - 👤 **Human dev:** ~$2318 (23.2h @ $100/h, 30min dedup)
36
+ - 🤖 **LLM usage:** $7.5000 (67 commits)
37
+ - 👤 **Human dev:** ~$2360 (23.6h @ $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.45-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.46-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.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)
6
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.46-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.6h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $7.5000 (66 commits)
10
- - 👤 **Human dev:** ~$2318 (23.2h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $7.5000 (67 commits)
10
+ - 👤 **Human dev:** ~$2360 (23.6h @ $100/h, 30min dedup)
11
11
 
12
12
  Generated on 2026-04-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
13
13
 
14
14
  ---
15
15
 
16
- ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.45-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.46-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.45"
7
+ version = "0.2.46"
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.45"
10
+ __version__ = "0.2.46"
11
11
 
12
12
  # ── Core models ───────────────────────────────────────────────────────────────
13
13
  from .models import ( # noqa: F401
@@ -5,10 +5,15 @@ from .handlers import (
5
5
  run_container_log_tail,
6
6
  run_docker_build,
7
7
  run_docker_health_wait,
8
+ run_ensure_autostart_entry,
9
+ run_ensure_browser_kiosk_script,
10
+ run_ensure_config_line,
11
+ run_ensure_kanshi_profile,
8
12
  run_http_check,
9
13
  run_inline_script,
10
14
  run_plugin,
11
15
  run_podman_build,
16
+ run_raspi_config,
12
17
  run_rsync,
13
18
  run_scp,
14
19
  run_ssh,
@@ -37,10 +42,15 @@ __all__ = [
37
42
  "run_container_log_tail",
38
43
  "run_docker_build",
39
44
  "run_docker_health_wait",
45
+ "run_ensure_autostart_entry",
46
+ "run_ensure_browser_kiosk_script",
47
+ "run_ensure_config_line",
48
+ "run_ensure_kanshi_profile",
40
49
  "run_http_check",
41
50
  "run_inline_script",
42
51
  "run_plugin",
43
52
  "run_podman_build",
53
+ "run_raspi_config",
44
54
  "run_rsync",
45
55
  "run_scp",
46
56
  "run_ssh",
@@ -405,14 +405,36 @@ def run_raspi_config(step: MigrationStep, probe: "RemoteProbe") -> None:
405
405
  def run_ensure_kanshi_profile(step: MigrationStep, probe: "RemoteProbe") -> None:
406
406
  """Idempotently write or replace a named kanshi output profile.
407
407
 
408
- step.command — the profile block to write (rendered kanshi syntax)
409
- step.config_file — kanshi config path (default: ~/.config/kanshi/config)
408
+ Declarative mode (preferred)::
409
+
410
+ profile_name: waveshare-only
411
+ outputs_on: [DSI-2]
412
+ outputs_off: [HDMI-A-2]
413
+
414
+ Legacy mode (pre-rendered block)::
415
+
416
+ step.command — the profile block to write (rendered kanshi syntax)
417
+ step.config_file — kanshi config path (default: ~/.config/kanshi/config)
410
418
  """
411
419
  from ..hardware.kiosk.output_profiles import OutputProfile
412
420
 
413
- profile_block = step.command
421
+ # Declarative mode: build profile from semantic fields
422
+ if step.profile_name:
423
+ profile = OutputProfile(
424
+ name=step.profile_name,
425
+ enabled=list(step.outputs_on),
426
+ disabled=list(step.outputs_off),
427
+ )
428
+ profile_block = profile.to_kanshi_config()
429
+ else:
430
+ profile_block = step.command
431
+
414
432
  if not profile_block:
415
- raise StepError(step, "ensure_kanshi_profile requires step.command with profile block")
433
+ raise StepError(
434
+ step,
435
+ "ensure_kanshi_profile requires profile_name+outputs_on/outputs_off "
436
+ "or step.command with pre-rendered profile block",
437
+ )
416
438
 
417
439
  config_path = step.config_file or "~/.config/kanshi/config"
418
440
  mkdir_cmd = f"mkdir -p $(dirname {config_path})"
@@ -462,56 +484,116 @@ def run_ensure_kanshi_profile(step: MigrationStep, probe: "RemoteProbe") -> None
462
484
 
463
485
 
464
486
  def run_ensure_autostart_entry(step: MigrationStep, probe: "RemoteProbe") -> None:
465
- """Idempotently add or replace a keyed entry in a compositor autostart file.
487
+ """Idempotently add or replace keyed entries in a compositor autostart file.
466
488
 
467
- step.config_file — path to autostart file
468
- step.config_line — the line to write (the entry body)
469
- step.config_section — used as the entry key for idempotent marker
470
- """
471
- from ..hardware.kiosk.autostart import AutostartEntry, ensure_autostart_entry
489
+ Declarative mode (preferred)::
472
490
 
473
- if not step.config_file or not step.config_line:
474
- raise StepError(step, "ensure_autostart_entry requires config_file and config_line")
491
+ compositor: labwc
492
+ entries:
493
+ - "kanshid &"
494
+ - "sleep 3"
495
+ - "bash /home/pi/c2004/scripts/kiosk-launch.sh &"
475
496
 
476
- key = step.config_section or "redeploy"
477
- entry = AutostartEntry(key=key, line=step.config_line)
497
+ Legacy mode (single entry)::
478
498
 
479
- r = probe.run(f"cat {step.config_file} 2>/dev/null || echo ''", timeout=10)
499
+ step.config_file — path to autostart file
500
+ step.config_line — the line to write (the entry body)
501
+ step.config_section — used as the entry key for idempotent marker
502
+ """
503
+ from ..hardware.kiosk.autostart import AutostartEntry, ensure_autostart_entry
504
+ from ..hardware.kiosk.compositors import COMPOSITORS
505
+
506
+ # Determine autostart path
507
+ config_file = step.config_file
508
+ if not config_file and step.compositor:
509
+ comp = COMPOSITORS.get(step.compositor)
510
+ if not comp:
511
+ raise StepError(step, f"Unknown compositor '{step.compositor}'")
512
+ config_file = comp.autostart_abs()
513
+ if not config_file:
514
+ raise StepError(step, "ensure_autostart_entry requires config_file or compositor")
515
+
516
+ # Collect entries to apply
517
+ entries: list[AutostartEntry] = []
518
+ if step.entries:
519
+ for i, line in enumerate(step.entries):
520
+ key = f"redeploy-{i}-{line.split()[0] if line else 'entry'}"
521
+ entries.append(AutostartEntry(key=key, line=line))
522
+ elif step.config_line:
523
+ key = step.config_section or "redeploy"
524
+ entries.append(AutostartEntry(key=key, line=step.config_line))
525
+
526
+ if not entries:
527
+ raise StepError(step, "ensure_autostart_entry requires entries or config_line")
528
+
529
+ r = probe.run(f"cat {config_file} 2>/dev/null || echo ''", timeout=10)
480
530
  existing = r.out if r.ok else ""
481
531
 
482
- new_content, changed = ensure_autostart_entry(existing, entry)
483
- if not changed:
532
+ changed_any = False
533
+ new_content = existing
534
+ for entry in entries:
535
+ new_content, changed = ensure_autostart_entry(new_content, entry)
536
+ if changed:
537
+ changed_any = True
538
+
539
+ if not changed_any:
484
540
  step.status = StepStatus.DONE
485
- step.result = f"no-op: entry [{key}] already correct"
541
+ step.result = f"no-op: {len(entries)} autostart entry(ies) already correct"
486
542
  return
487
543
 
488
- mkdir_cmd = f"mkdir -p $(dirname {step.config_file})"
544
+ mkdir_cmd = f"mkdir -p $(dirname {config_file})"
489
545
  probe.run(mkdir_cmd, timeout=10)
490
546
 
491
547
  encoded = base64.b64encode(new_content.encode()).decode()
492
548
  tmp = f"/tmp/redeploy-autostart-{step.id}.txt"
493
549
  write_r = probe.run(
494
- f"echo '{encoded}' | base64 -d | tee {tmp} > /dev/null && mv {tmp} {step.config_file}",
550
+ f"echo '{encoded}' | base64 -d | tee {tmp} > /dev/null && mv {tmp} {config_file}",
495
551
  timeout=15,
496
552
  )
497
553
  if not write_r.ok:
498
- raise StepError(step, f"Cannot write {step.config_file}: {write_r.stderr[:200]}")
554
+ raise StepError(step, f"Cannot write {config_file}: {write_r.stderr[:200]}")
499
555
 
500
556
  step.status = StepStatus.DONE
501
- step.result = f"entry [{key}] written to {step.config_file}"
557
+ step.result = f"{len(entries)} autostart entry(ies) written to {config_file}"
502
558
 
503
559
 
504
560
  def run_ensure_browser_kiosk_script(step: MigrationStep, probe: "RemoteProbe") -> None:
505
561
  """Write a kiosk-launch.sh script to the remote device.
506
562
 
507
- step.command — the full script content to write
508
- step.dst — destination path (default: ~/kiosk-launch.sh)
563
+ Declarative mode (preferred)::
564
+
565
+ browser_profile: chromium_wayland_kiosk
566
+ url: "http://localhost:8100/connect-id?font=xlarge&theme=dark"
567
+ kiosk_script_path: /home/pi/c2004/scripts/kiosk-launch.sh
568
+
569
+ Legacy mode (pre-rendered script)::
570
+
571
+ step.command — the full script content to write
572
+ step.dst — destination path (default: ~/kiosk-launch.sh)
509
573
  """
510
- script_content = step.command
574
+ from ..hardware.kiosk.browsers import CHROMIUM_WAYLAND_KIOSK
575
+
576
+ # Declarative mode: build script from profile + URL
577
+ if step.browser_profile:
578
+ # Registry lookup (extend when more profiles exist)
579
+ if step.browser_profile == "chromium_wayland_kiosk":
580
+ profile = CHROMIUM_WAYLAND_KIOSK
581
+ else:
582
+ raise StepError(step, f"Unknown browser_profile '{step.browser_profile}'")
583
+ if not step.url:
584
+ raise StepError(step, "browser_profile mode requires 'url' field")
585
+ script_content = profile.build_launch_cmd(step.url)
586
+ else:
587
+ script_content = step.command
588
+
511
589
  if not script_content:
512
- raise StepError(step, "ensure_browser_kiosk_script requires step.command with script body")
590
+ raise StepError(
591
+ step,
592
+ "ensure_browser_kiosk_script requires browser_profile+url "
593
+ "or step.command with pre-rendered script body",
594
+ )
513
595
 
514
- dst = step.dst or "~/kiosk-launch.sh"
596
+ dst = step.kiosk_script_path or step.dst or "~/kiosk-launch.sh"
515
597
 
516
598
  r = probe.run(f"cat {dst} 2>/dev/null || echo ''", timeout=10)
517
599
  existing = r.out if r.ok else ""
@@ -95,68 +95,6 @@ def capture(host, name, compose_files, migration_file, device_map_file,
95
95
  console.print(f"\n[green]✓ saved:[/green] {saved}")
96
96
 
97
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
-
160
98
  def _execute_query_blueprint(bp, query_expr, output_fmt):
161
99
  """Execute JMESPath query on DeviceBlueprint model and output result."""
162
100
  import jmespath
@@ -299,7 +237,9 @@ def show(blueprint_file, fmt, apply_config, query_expr):
299
237
  click.echo(bp.to_yaml())
300
238
 
301
239
  if apply_config:
302
- _apply_blueprint_config(bp, apply_config)
240
+ from ...config_apply import apply_config_file
241
+ from rich.console import Console
242
+ apply_config_file(apply_config, host=bp.host, console=Console())
303
243
 
304
244
 
305
245
  # ── list ──────────────────────────────────────────────────────────────────────
@@ -29,7 +29,6 @@ from pathlib import Path
29
29
  import click
30
30
  from rich import box
31
31
  from rich.console import Console
32
- from rich.panel import Panel
33
32
  from rich.table import Table
34
33
 
35
34
 
@@ -196,143 +195,6 @@ def device_map_cmd(
196
195
  sys.exit(1)
197
196
 
198
197
 
199
- def _apply_config_from_file(console, config_path, ssh_key):
200
- """Apply hardware/infra settings from YAML/JSON config file to the remote host.
201
-
202
- Reads DeviceMap YAML/JSON and applies:
203
- - Display transforms via wlr-randr + kanshi
204
- - Backlight settings
205
- - Other configurable hardware parameters
206
- """
207
- import yaml
208
- import json as _json
209
-
210
- from ...detect.remote import RemoteProbe
211
-
212
- with open(config_path) as f:
213
- raw = f.read()
214
-
215
- try:
216
- cfg = yaml.safe_load(raw)
217
- except Exception:
218
- cfg = _json.loads(raw)
219
-
220
- # Extract host from config or prompt
221
- host = cfg.get("host")
222
- if not host:
223
- console.print("[red]✗ No 'host' field in config file[/red]")
224
- sys.exit(1)
225
-
226
- console.print(f"[cyan]→ Applying config from {config_path} to {host}[/cyan]")
227
-
228
- # Connect to remote
229
- p = RemoteProbe(host, ssh_key=ssh_key)
230
-
231
- applied = 0
232
-
233
- # ── display transforms ────────────────────────────────────────────────────
234
- hardware = cfg.get("hardware", {})
235
- for output in hardware.get("drm_outputs", []):
236
- connector = output.get("connector", "")
237
- transform = output.get("transform", "normal")
238
- if not connector or not ("DSI" in connector or "HDMI" in connector):
239
- continue
240
-
241
- if transform == "normal":
242
- console.print(f"[dim] skip {connector}: transform=normal (default)[/dim]")
243
- continue
244
-
245
- console.print(f"[cyan]→ {connector}: transform={transform}[/cyan]")
246
-
247
- # Apply via wlr-randr
248
- wlr_cmd = (
249
- f"WAYLAND_DISPLAY=wayland-0 XDG_RUNTIME_DIR=/run/user/$(id -u) "
250
- f"wlr-randr --output {connector} --transform {transform} 2>&1"
251
- )
252
- r = p.run(wlr_cmd)
253
- if r.ok:
254
- console.print(f"[green] ✓ wlr-randr applied[/green]")
255
- applied += 1
256
- else:
257
- console.print(f"[yellow] ⚠ wlr-randr: {r.out.strip() or 'no output'}[/yellow]")
258
-
259
- # ── persist DSI transforms in kanshi ─────────────────────────────────────────
260
- dsi_outputs = [
261
- o for o in hardware.get("drm_outputs", [])
262
- if "DSI" in o.get("connector", "")
263
- ]
264
- if dsi_outputs:
265
- _update_kanshi_from_cfg(console, p, dsi_outputs)
266
-
267
- # ── backlight ─────────────────────────────────────────────────────────────
268
- for bl in hardware.get("backlights", []):
269
- name = bl.get("name", "")
270
- brightness = bl.get("brightness")
271
- bl_power = bl.get("bl_power")
272
- if not name:
273
- continue
274
-
275
- if brightness is not None:
276
- r = p.run(f"echo {brightness} | sudo tee /sys/class/backlight/{name}/brightness > /dev/null")
277
- if r.ok:
278
- console.print(f"[green] ✓ backlight {name}: brightness={brightness}[/green]")
279
- applied += 1
280
-
281
- if bl_power is not None:
282
- r = p.run(f"echo {bl_power} | sudo tee /sys/class/backlight/{name}/bl_power > /dev/null")
283
- if r.ok:
284
- console.print(f"[green] ✓ backlight {name}: bl_power={bl_power}[/green]")
285
- applied += 1
286
-
287
- if applied == 0:
288
- console.print("[yellow]Nothing to apply — no relevant settings found in config[/yellow]")
289
- else:
290
- console.print(f"\n[bold green]✓ Config applied from {config_path}[/bold green]")
291
-
292
-
293
- def _update_kanshi_from_cfg(console, p, dsi_outputs: list):
294
- """Rebuild kanshi profile from drm_outputs list and write to ~/.config/kanshi/config."""
295
- import re as _re
296
-
297
- kanshi_cfg_path = "~/.config/kanshi/config"
298
- read_r = p.run(f"cat {kanshi_cfg_path} 2>/dev/null")
299
- current = read_r.out if read_r.ok else ""
300
-
301
- for output in dsi_outputs:
302
- connector = output.get("connector", "")
303
- transform = output.get("transform", "normal")
304
-
305
- output_line_pat = _re.compile(
306
- rf'(\s*output\s+{_re.escape(connector)}\s+enable)(\s+transform\s+\S+)?'
307
- )
308
- if _re.search(output_line_pat, current):
309
- if transform == "normal":
310
- current = _re.sub(output_line_pat, r'\1', current)
311
- else:
312
- current = _re.sub(output_line_pat, rf'\1 transform {transform}', current)
313
- elif current.strip():
314
- current = _re.sub(
315
- rf'(\s*output\s+{_re.escape(connector)}\b)',
316
- rf'\1 transform {transform}' if transform != "normal" else r'\1',
317
- current,
318
- )
319
- else:
320
- current = (
321
- f"profile waveshare-only {{\n"
322
- f" output {connector} enable"
323
- + (f" transform {transform}" if transform != "normal" else "")
324
- + "\n"
325
- + " output HDMI-A-2 disable\n"
326
- + "}\n"
327
- )
328
-
329
- escaped = current.replace("'", "'\\''")
330
- write_r = p.run(f"mkdir -p ~/.config/kanshi && printf '%s' '{escaped}' > {kanshi_cfg_path}")
331
- if write_r.ok:
332
- console.print(f"[green] ✓ kanshi config updated ({kanshi_cfg_path})[/green]")
333
- p.run("pkill -SIGUSR1 kanshi 2>/dev/null || true")
334
-
335
-
336
198
  def _execute_query_device_map(console, dm, query_expr, output_fmt):
337
199
  """Execute JMESPath query on DeviceMap model and output result."""
338
200
  import jmespath
@@ -360,122 +222,16 @@ def _execute_query_device_map(console, dm, query_expr, output_fmt):
360
222
  # ── rendering helpers ─────────────────────────────────────────────────────────
361
223
 
362
224
 
363
- _SEV_COLOR = {"info": "green", "warning": "yellow", "error": "red", "critical": "bold red"}
364
- _SEV_ICON = {"info": "✓", "warning": "⚠", "error": "✗", "critical": "✗✗"}
365
-
366
-
367
225
  def _render(console: Console, dm: "DeviceMap", fmt: str) -> None:
368
- from ...models import DeviceMap
226
+ """Dispatch to the appropriate renderer for *fmt*."""
227
+ from .device_map_renderers import render_json, render_rich, render_yaml
369
228
 
370
229
  if fmt == "yaml":
371
- click.echo(dm.to_yaml())
372
- return
373
- if fmt == "json":
374
- import json as _json
375
- click.echo(_json.dumps(dm.model_dump(mode="json"), indent=2))
376
- return
377
-
378
- # Rich
379
- console.print()
380
- tags_str = " ".join(f"[dim cyan]{t}[/dim cyan]" for t in dm.tags) or "[dim]-[/dim]"
381
- console.print(Panel(
382
- f"[bold]device-map[/bold] [cyan]{dm.host}[/cyan]\n"
383
- f"Name: [white]{dm.name or '—'}[/white] "
384
- f"Tags: {tags_str} "
385
- f"Scanned: [dim]{dm.scanned_at.strftime('%Y-%m-%d %H:%M UTC')}[/dim]",
386
- box=box.ROUNDED,
387
- expand=False,
388
- ))
389
-
390
- # Hardware section
391
- hw = dm.hardware
392
- if hw:
393
- console.print(f"\n[bold]Board[/bold] {hw.board or '?'} Kernel: {hw.kernel or '?'}")
394
-
395
- if hw.drm_outputs:
396
- t = Table(title="Display (DRM)", show_header=True, box=box.SIMPLE, padding=(0, 1))
397
- t.add_column("Connector")
398
- t.add_column("Status")
399
- t.add_column("Mode")
400
- t.add_column("Transform")
401
- t.add_column("Backlight")
402
- for o in hw.drm_outputs:
403
- sc = "green" if o.status == "connected" else "dim"
404
- bl = next((b for b in hw.backlights if b.display_name == o.connector), None)
405
- bl_str = f"{bl.brightness}/{bl.max_brightness}" if bl else "—"
406
- t.add_row(
407
- o.connector,
408
- f"[{sc}]{o.status}[/{sc}]",
409
- o.modes[0] if o.modes else "—",
410
- o.transform,
411
- bl_str,
412
- )
413
- console.print(t)
414
-
415
- if hw.dsi_overlays:
416
- console.print(f"[dim]Overlays:[/dim] {', '.join(hw.dsi_overlays)}")
417
- if hw.i2c_buses:
418
- buses = ", ".join(
419
- f"i2c-{b.bus}" + (f"[{','.join(b.devices)}]" if b.devices else "")
420
- for b in hw.i2c_buses
421
- )
422
- console.print(f"[dim]I2C:[/dim] {buses}")
423
-
424
- # Infra section
425
- infra = dm.infra
426
- if infra:
427
- rt = infra.runtime
428
- runtimes = [
429
- f"podman {rt.podman}" if rt.podman else None,
430
- f"docker {rt.docker}" if rt.docker else None,
431
- f"k3s {rt.k3s}" if rt.k3s else None,
432
- ]
433
- rt_str = " ".join(r for r in runtimes if r) or "—"
434
- console.print(f"\n[bold]Infra[/bold] strategy=[cyan]{infra.detected_strategy.value}[/cyan]"
435
- f" version={infra.current_version or '?'} {rt_str}")
436
-
437
- # Running services
438
- podman_svcs = infra.services.get("podman", [])
439
- systemd_svcs = infra.services.get("systemd", [])
440
- all_svcs = podman_svcs + systemd_svcs
441
- if all_svcs:
442
- t = Table(title="Services", show_header=True, box=box.SIMPLE, padding=(0, 1))
443
- t.add_column("Name")
444
- t.add_column("Status")
445
- t.add_column("Type")
446
- for s in all_svcs:
447
- sc = "green" if "running" in s.status.lower() or "active" in s.status.lower() else "red"
448
- kind = "podman" if s in podman_svcs else "systemd"
449
- t.add_row(s.name, f"[{sc}]{s.status}[/{sc}]", kind)
450
- console.print(t)
451
-
452
- # Issues
453
- console.print()
454
- if not dm.issues:
455
- console.print("[green]✓ No issues detected[/green]")
230
+ render_yaml(dm)
231
+ elif fmt == "json":
232
+ render_json(dm)
456
233
  else:
457
- t = Table(title="Issues", show_header=True, box=box.SIMPLE, padding=(0, 1))
458
- t.add_column("", width=3)
459
- t.add_column("Source", style="dim")
460
- t.add_column("Component")
461
- t.add_column("Message")
462
-
463
- for issue in dm.issues:
464
- sev = issue.get("severity", "info")
465
- color = _SEV_COLOR.get(sev, "white")
466
- icon = _SEV_ICON.get(sev, "?")
467
- t.add_row(
468
- f"[{color}]{icon}[/{color}]",
469
- issue.get("source", ""),
470
- f"[{color}]{issue.get('component', '')}[/{color}]",
471
- issue.get("message", ""),
472
- )
473
- console.print(t)
474
-
475
- fixes = [i for i in dm.issues if i.get("fix")]
476
- if fixes:
477
- console.print(f"[dim] {len(fixes)} fix suggestion(s) — rerun with:[/dim] "
478
- f"[cyan]redeploy hardware {dm.host} --fix[/cyan]")
234
+ render_rich(console, dm)
479
235
 
480
236
 
481
237
  def _print_list(console: Console) -> None: