redeploy 0.2.76__tar.gz → 0.2.78__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 (297) hide show
  1. {redeploy-0.2.76 → redeploy-0.2.78}/PKG-INFO +5 -5
  2. {redeploy-0.2.76 → redeploy-0.2.78}/README.md +4 -4
  3. {redeploy-0.2.76 → redeploy-0.2.78}/pyproject.toml +1 -1
  4. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/__init__.py +1 -1
  5. redeploy-0.2.78/redeploy/cli/commands/apply_cmd.py +51 -0
  6. redeploy-0.2.78/redeploy/cli/commands/device_map.py +78 -0
  7. redeploy-0.2.78/redeploy/cli/commands/device_map_actions.py +174 -0
  8. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/devices.py +6 -40
  9. redeploy-0.2.78/redeploy/cli/commands/devices_display.py +61 -0
  10. redeploy-0.2.78/redeploy/cli/commands/migrate_cmd.py +75 -0
  11. redeploy-0.2.78/redeploy/cli/commands/plan_apply.py +9 -0
  12. redeploy-0.2.78/redeploy/cli/commands/plan_apply_report.py +296 -0
  13. redeploy-0.2.78/redeploy/cli/commands/plan_apply_run.py +104 -0
  14. redeploy-0.2.78/redeploy/cli/commands/plan_apply_shared.py +235 -0
  15. redeploy-0.2.78/redeploy/cli/commands/plan_cmd.py +78 -0
  16. redeploy-0.2.78/redeploy/cli/commands/probe.py +91 -0
  17. redeploy-0.2.78/redeploy/cli/commands/probe_display.py +77 -0
  18. redeploy-0.2.78/redeploy/cli/commands/run_cmd.py +168 -0
  19. redeploy-0.2.78/redeploy/discovery/__init__.py +31 -0
  20. redeploy-0.2.78/redeploy/discovery/auto_probe.py +186 -0
  21. redeploy-0.2.78/redeploy/discovery/helpers.py +32 -0
  22. redeploy-0.2.78/redeploy/discovery/probe_parse.py +45 -0
  23. redeploy-0.2.78/redeploy/discovery/registry.py +64 -0
  24. redeploy-0.2.78/redeploy/discovery/scanners.py +254 -0
  25. redeploy-0.2.78/redeploy/discovery/ssh_credentials.py +111 -0
  26. redeploy-0.2.78/redeploy/discovery/types.py +38 -0
  27. redeploy-0.2.78/redeploy/tests/test_devices_display.py +33 -0
  28. redeploy-0.2.78/redeploy/tests/test_discovery_probe.py +45 -0
  29. redeploy-0.2.78/redeploy/tests/test_plan_apply_report.py +91 -0
  30. redeploy-0.2.78/redeploy/tests/test_plan_apply_shared.py +21 -0
  31. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy.egg-info/PKG-INFO +5 -5
  32. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy.egg-info/SOURCES.txt +22 -1
  33. redeploy-0.2.76/redeploy/cli/commands/device_map.py +0 -299
  34. redeploy-0.2.76/redeploy/cli/commands/plan_apply.py +0 -1005
  35. redeploy-0.2.76/redeploy/cli/commands/probe.py +0 -144
  36. redeploy-0.2.76/redeploy/discovery.py +0 -789
  37. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/analyze/__init__.py +0 -0
  38. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/analyze/checkers/__init__.py +0 -0
  39. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/analyze/checkers/base.py +0 -0
  40. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/analyze/checkers/binary.py +0 -0
  41. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/analyze/checkers/command_ref.py +0 -0
  42. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/analyze/checkers/compose.py +0 -0
  43. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/analyze/checkers/docker_build.py +0 -0
  44. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/analyze/checkers/env.py +0 -0
  45. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/analyze/checkers/path.py +0 -0
  46. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/analyze/checkers/reference.py +0 -0
  47. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/analyze/ignore.py +0 -0
  48. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/analyze/models.py +0 -0
  49. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/analyze/preflight_schema.py +0 -0
  50. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/analyze/spec_analyzer.py +0 -0
  51. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/apply/__init__.py +0 -0
  52. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/apply/exceptions.py +0 -0
  53. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/apply/executor.py +0 -0
  54. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/apply/handlers.py +0 -0
  55. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/apply/progress.py +0 -0
  56. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/apply/rollback.py +0 -0
  57. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/apply/state.py +0 -0
  58. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/apply/state_apply.py +0 -0
  59. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/apply/utils/__init__.py +0 -0
  60. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/apply/utils/run_container_build.py +0 -0
  61. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/audit/__init__.py +0 -0
  62. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/audit/auditor.py +0 -0
  63. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/audit/extractor.py +0 -0
  64. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/audit/models.py +0 -0
  65. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/audit/paths.py +0 -0
  66. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/audit/patterns.py +0 -0
  67. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/audit/probe.py +0 -0
  68. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/blueprint/__init__.py +0 -0
  69. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/blueprint/extractor.py +0 -0
  70. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/blueprint/generators/__init__.py +0 -0
  71. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/blueprint/generators/docker_compose.py +0 -0
  72. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/blueprint/generators/migration.py +0 -0
  73. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/blueprint/sources/__init__.py +0 -0
  74. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/blueprint/sources/compose.py +0 -0
  75. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/blueprint/sources/hardware.py +0 -0
  76. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/blueprint/sources/infra.py +0 -0
  77. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/blueprint/sources/migration.py +0 -0
  78. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/__init__.py +0 -0
  79. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/__init__.py +0 -0
  80. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/audit.py +0 -0
  81. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/blueprint.py +0 -0
  82. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/bump_fix.py +0 -0
  83. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/detect.py +0 -0
  84. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/device_map_renderers.py +0 -0
  85. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/diagnose.py +0 -0
  86. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/diff.py +0 -0
  87. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/exec_.py +0 -0
  88. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/export.py +0 -0
  89. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/gh_workflow.py +0 -0
  90. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/hardware.py +0 -0
  91. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/import_.py +0 -0
  92. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/init.py +0 -0
  93. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/inspect.py +0 -0
  94. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/lint.py +0 -0
  95. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/mcp_cmd.py +0 -0
  96. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/patterns.py +0 -0
  97. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/plugin.py +0 -0
  98. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/prompt_cmd.py +0 -0
  99. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/push.py +0 -0
  100. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/state.py +0 -0
  101. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/status.py +0 -0
  102. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/target.py +0 -0
  103. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/version/__init__.py +0 -0
  104. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/version/commands.py +0 -0
  105. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/version/helpers.py +0 -0
  106. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/version/monorepo.py +0 -0
  107. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/version/release.py +0 -0
  108. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/version/scanner.py +0 -0
  109. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/version/utils/__init__.py +0 -0
  110. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/version/utils/changelog_config.py +0 -0
  111. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/version/utils/git_config.py +0 -0
  112. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/commands/workflow.py +0 -0
  113. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/core.py +0 -0
  114. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/display.py +0 -0
  115. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli/query.py +0 -0
  116. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/cli.py +0 -0
  117. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/config_apply/__init__.py +0 -0
  118. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/config_apply/applier.py +0 -0
  119. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/config_apply/handlers/__init__.py +0 -0
  120. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/config_apply/handlers/display.py +0 -0
  121. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/config_apply/loader.py +0 -0
  122. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/data_sync.py +0 -0
  123. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/detect/__init__.py +0 -0
  124. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/detect/builtin/__init__.py +0 -0
  125. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/detect/detector.py +0 -0
  126. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/detect/hardware.py +0 -0
  127. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/detect/hardware_rules.py +0 -0
  128. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/detect/probes.py +0 -0
  129. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/detect/remote.py +0 -0
  130. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/detect/templates.py +0 -0
  131. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/detect/workflow.py +0 -0
  132. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/dsl/__init__.py +0 -0
  133. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/dsl/loader.py +0 -0
  134. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/dsl/parser.py +0 -0
  135. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/dsl_python/__init__.py +0 -0
  136. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/dsl_python/context.py +0 -0
  137. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/dsl_python/decorators.py +0 -0
  138. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/dsl_python/docker_steps.py +0 -0
  139. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/dsl_python/exceptions.py +0 -0
  140. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/dsl_python/runner.py +0 -0
  141. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/dsl_python/steps.py +0 -0
  142. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/fleet.py +0 -0
  143. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/hardware/__init__.py +0 -0
  144. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/hardware/config_txt.py +0 -0
  145. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/hardware/data/__init__.py +0 -0
  146. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/hardware/data/hyperpixel.py +0 -0
  147. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/hardware/data/official.py +0 -0
  148. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/hardware/data/waveshare.py +0 -0
  149. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/hardware/fixes.py +0 -0
  150. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/hardware/kiosk/__init__.py +0 -0
  151. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/hardware/kiosk/autostart.py +0 -0
  152. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/hardware/kiosk/browsers.py +0 -0
  153. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/hardware/kiosk/compositors.py +0 -0
  154. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/hardware/kiosk/output_profiles.py +0 -0
  155. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/hardware/panels.py +0 -0
  156. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/hardware/raspi_config.py +0 -0
  157. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/heal/__init__.py +0 -0
  158. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/heal/decider.py +0 -0
  159. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/heal/hint_provider.py +0 -0
  160. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/heal/log_writer.py +0 -0
  161. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/heal/loop_detector.py +0 -0
  162. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/heal/runner.py +0 -0
  163. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/heal.py +0 -0
  164. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/iac/__init__.py +0 -0
  165. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/iac/base.py +0 -0
  166. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/iac/config_hints.py +0 -0
  167. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/iac/docker_compose.py +0 -0
  168. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/iac/parsers/__init__.py +0 -0
  169. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/iac/parsers/compose.py +0 -0
  170. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/iac/registry.py +0 -0
  171. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/integrations/__init__.py +0 -0
  172. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/integrations/op3_bridge.py +0 -0
  173. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/markpact/__init__.py +0 -0
  174. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/markpact/compiler.py +0 -0
  175. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/markpact/models.py +0 -0
  176. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/markpact/parser.py +0 -0
  177. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/mcp_server.py +0 -0
  178. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/models/__init__.py +0 -0
  179. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/models/blueprint.py +0 -0
  180. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/models/devices.py +0 -0
  181. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/models/enums.py +0 -0
  182. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/models/hardware.py +0 -0
  183. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/models/infra.py +0 -0
  184. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/models/manifest.py +0 -0
  185. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/models/persisted.py +0 -0
  186. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/models/pipeline.py +0 -0
  187. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/models/plan.py +0 -0
  188. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/models/spec.py +0 -0
  189. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/observe.py +0 -0
  190. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/parse.py +0 -0
  191. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/patterns.py +0 -0
  192. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/plan/__init__.py +0 -0
  193. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/plan/planner.py +0 -0
  194. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/plugins/__init__.py +0 -0
  195. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/plugins/builtin/__init__.py +0 -0
  196. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/plugins/builtin/browser_reload.py +0 -0
  197. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/plugins/builtin/hardware_diagnostic.py +0 -0
  198. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/plugins/builtin/notify.py +0 -0
  199. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/plugins/builtin/process_control.py +0 -0
  200. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/plugins/builtin/systemd_reload.py +0 -0
  201. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/schema.py +0 -0
  202. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/spec_loader.py +0 -0
  203. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/ssh.py +0 -0
  204. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/steps/__init__.py +0 -0
  205. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/steps/docker.py +0 -0
  206. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/steps/generic.py +0 -0
  207. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/steps/hardware.py +0 -0
  208. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/steps/k3s.py +0 -0
  209. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/steps/kiosk.py +0 -0
  210. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/steps/podman.py +0 -0
  211. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/steps/process.py +0 -0
  212. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/steps/scm.py +0 -0
  213. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/steps/transfer.py +0 -0
  214. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/__init__.py +0 -0
  215. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_analyze.py +0 -0
  216. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_audit.py +0 -0
  217. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_cli.py +0 -0
  218. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_data_sync.py +0 -0
  219. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_discovery.py +0 -0
  220. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_dsl.py +0 -0
  221. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_examples.py +0 -0
  222. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_executor.py +0 -0
  223. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_fleet.py +0 -0
  224. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_git_integration.py +0 -0
  225. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_heal.py +0 -0
  226. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_heal_decider.py +0 -0
  227. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_hooks.py +0 -0
  228. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_iac.py +0 -0
  229. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_iac_compose_contract.py +0 -0
  230. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_iac_docker_compose_contract.py +0 -0
  231. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_manifest.py +0 -0
  232. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_markpact_compiler.py +0 -0
  233. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_markpact_parser.py +0 -0
  234. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_models.py +0 -0
  235. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_observe.py +0 -0
  236. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_parse.py +0 -0
  237. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_patterns.py +0 -0
  238. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_planner_edge.py +0 -0
  239. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_plugins.py +0 -0
  240. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_probes.py +0 -0
  241. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_public_api.py +0 -0
  242. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_resume.py +0 -0
  243. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_security_hardening.py +0 -0
  244. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_spec_loader.py +0 -0
  245. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_ssh.py +0 -0
  246. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_steps_registry.py +0 -0
  247. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_templates.py +0 -0
  248. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_verify.py +0 -0
  249. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_version.py +0 -0
  250. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_version_changelog.py +0 -0
  251. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_version_cli.py +0 -0
  252. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_version_manifest.py +0 -0
  253. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_version_sources.py +0 -0
  254. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/tests/test_workflow.py +0 -0
  255. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/verify.py +0 -0
  256. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/version/__init__.py +0 -0
  257. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/version/bump.py +0 -0
  258. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/version/changelog.py +0 -0
  259. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/version/commits.py +0 -0
  260. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/version/diff.py +0 -0
  261. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/version/git_integration.py +0 -0
  262. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/version/git_transaction.py +0 -0
  263. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/version/manifest.py +0 -0
  264. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/version/sources/__init__.py +0 -0
  265. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/version/sources/base.py +0 -0
  266. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/version/sources/json_.py +0 -0
  267. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/version/sources/plain.py +0 -0
  268. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/version/sources/regex.py +0 -0
  269. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/version/sources/toml_.py +0 -0
  270. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/version/sources/yaml_.py +0 -0
  271. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/version/transaction.py +0 -0
  272. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy/version.py +0 -0
  273. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy.egg-info/dependency_links.txt +0 -0
  274. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy.egg-info/entry_points.txt +0 -0
  275. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy.egg-info/requires.txt +0 -0
  276. {redeploy-0.2.76 → redeploy-0.2.78}/redeploy.egg-info/top_level.txt +0 -0
  277. {redeploy-0.2.76 → redeploy-0.2.78}/setup.cfg +0 -0
  278. {redeploy-0.2.76 → redeploy-0.2.78}/tests/test_blueprint_sources.py +0 -0
  279. {redeploy-0.2.76 → redeploy-0.2.78}/tests/test_blueprint_sources_compose.py +0 -0
  280. {redeploy-0.2.76 → redeploy-0.2.78}/tests/test_cli_gh_workflow.py +0 -0
  281. {redeploy-0.2.76 → redeploy-0.2.78}/tests/test_cli_query.py +0 -0
  282. {redeploy-0.2.76 → redeploy-0.2.78}/tests/test_example_argocd_flux_plugin.py +0 -0
  283. {redeploy-0.2.76 → redeploy-0.2.78}/tests/test_example_gitops_ci_plugin.py +0 -0
  284. {redeploy-0.2.76 → redeploy-0.2.78}/tests/test_example_helm_kustomize_plugin.py +0 -0
  285. {redeploy-0.2.76 → redeploy-0.2.78}/tests/test_hardware_config.py +0 -0
  286. {redeploy-0.2.76 → redeploy-0.2.78}/tests/test_hardware_kiosk.py +0 -0
  287. {redeploy-0.2.76 → redeploy-0.2.78}/tests/test_hardware_rules.py +0 -0
  288. {redeploy-0.2.76 → redeploy-0.2.78}/tests/test_heal_decider.py +0 -0
  289. {redeploy-0.2.76 → redeploy-0.2.78}/tests/test_heal_loop_detector.py +0 -0
  290. {redeploy-0.2.76 → redeploy-0.2.78}/tests/test_iac.py +0 -0
  291. {redeploy-0.2.76 → redeploy-0.2.78}/tests/test_iac_config_hints.py +0 -0
  292. {redeploy-0.2.76 → redeploy-0.2.78}/tests/test_iac_plugin_loading.py +0 -0
  293. {redeploy-0.2.76 → redeploy-0.2.78}/tests/test_import_plugin_template.py +0 -0
  294. {redeploy-0.2.76 → redeploy-0.2.78}/tests/test_parse.py +0 -0
  295. {redeploy-0.2.76 → redeploy-0.2.78}/tests/test_prompt_llm.py +0 -0
  296. {redeploy-0.2.76 → redeploy-0.2.78}/tests/test_redeploy.py +0 -0
  297. {redeploy-0.2.76 → redeploy-0.2.78}/tests/test_steps_kiosk.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redeploy
3
- Version: 0.2.76
3
+ Version: 0.2.78
4
4
  Summary: Infrastructure migration toolkit: detect → plan → apply
5
5
  License-Expression: Apache-2.0
6
6
  Requires-Python: >=3.11
@@ -33,17 +33,17 @@ Requires-Dist: mcp>=1.0; extra == "mcp"
33
33
 
34
34
  ## AI Cost Tracking
35
35
 
36
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.76-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
37
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$5.41-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-42.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
36
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.78-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
37
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$6.32-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-42.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
38
38
 
39
- - 🤖 **LLM usage:** $5.4083 (98 commits)
39
+ - 🤖 **LLM usage:** $6.3244 (100 commits)
40
40
  - 👤 **Human dev:** ~$4198 (42.0h @ $100/h, 30min dedup)
41
41
 
42
42
  Generated on 2026-05-20 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
43
43
 
44
44
  ---
45
45
 
46
- ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.76-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
46
+ ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.78-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
47
47
 
48
48
  Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
49
49
 
@@ -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.76-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-$5.41-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-42.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
6
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.78-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-$6.32-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-42.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $5.4083 (98 commits)
9
+ - 🤖 **LLM usage:** $6.3244 (100 commits)
10
10
  - 👤 **Human dev:** ~$4198 (42.0h @ $100/h, 30min dedup)
11
11
 
12
12
  Generated on 2026-05-20 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.76-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.78-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.76"
7
+ version = "0.2.78"
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.76"
10
+ __version__ = "0.2.78"
11
11
 
12
12
  # ── Core models ───────────────────────────────────────────────────────────────
13
13
  from .models import ( # noqa: F401
@@ -0,0 +1,51 @@
1
+ """`redeploy apply` command."""
2
+ from __future__ import annotations
3
+
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import click
8
+ from rich.console import Console
9
+
10
+
11
+ @click.command()
12
+ @click.option(
13
+ "--plan", "plan_file", default="migration-plan.yaml", show_default=True,
14
+ type=click.Path(exists=True), help="Migration plan file",
15
+ )
16
+ @click.option("--dry-run", is_flag=True, help="Show steps without executing")
17
+ @click.option("--step", default=None, help="Run only a specific step by ID")
18
+ @click.option("-o", "--output", default=None, type=click.Path(), help="Save results to file after apply")
19
+ @click.pass_context
20
+ def apply(ctx, plan_file, dry_run, step, output):
21
+ """Execute a migration plan."""
22
+ from ...apply import Executor
23
+
24
+ console = Console()
25
+ executor = Executor.from_file(Path(plan_file))
26
+
27
+ if step:
28
+ matched = [s for s in executor.plan.steps if s.id == step]
29
+ if not matched:
30
+ console.print(f"[red]Step '{step}' not found in plan[/red]")
31
+ ids = ", ".join(s.id for s in executor.plan.steps)
32
+ console.print(f"Available: {ids}")
33
+ sys.exit(1)
34
+ executor.plan.steps = matched
35
+
36
+ executor.dry_run = dry_run
37
+ prefix = "[DRY RUN] " if dry_run else ""
38
+ console.print(
39
+ f"\n{prefix}[bold]Applying: {executor.plan.from_strategy.value}"
40
+ f" → {executor.plan.to_strategy.value}[/bold] "
41
+ f"({len(executor.plan.steps)} steps)"
42
+ )
43
+
44
+ ok = executor.run()
45
+ console.print(f"\n{executor.summary()}")
46
+
47
+ if output:
48
+ executor.save_results(Path(output))
49
+
50
+ if not ok:
51
+ sys.exit(1)
@@ -0,0 +1,78 @@
1
+ """device-map command — Generate a full standardized device snapshot."""
2
+ from __future__ import annotations
3
+
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import click
8
+ from rich.console import Console
9
+
10
+ from .device_map_actions import (
11
+ emit_device_map,
12
+ execute_query_device_map,
13
+ print_device_map_diff,
14
+ print_saved_maps,
15
+ probe_device_map,
16
+ )
17
+
18
+
19
+ @click.command("device-map")
20
+ @click.argument("host", required=False, default=None)
21
+ @click.option("--name", default="", help="Human-friendly device label")
22
+ @click.option("--tag", "tags", multiple=True, help="Tag(s) to attach (repeatable)")
23
+ @click.option("--save", is_flag=True, help="Persist map to ~/.config/redeploy/device-maps/")
24
+ @click.option("--out", "out_path", default=None, type=click.Path(), help="Save to specific file")
25
+ @click.option("--format", "output_fmt", default="yaml", type=click.Choice(["yaml", "json"]))
26
+ @click.option("--no-infra", is_flag=True, help="Skip infra probe (faster — hardware only)")
27
+ @click.option("--list", "list_saved", is_flag=True, help="List saved device maps")
28
+ @click.option("--show", "show_file", default=None, type=click.Path(exists=True), help="Load and display a saved device-map file")
29
+ @click.option("--diff", "diff_files", nargs=2, type=click.Path(exists=True), help="Diff two saved device-map files")
30
+ @click.option("--apply-config", "apply_config", default=None, type=click.Path(exists=True, dir_okay=False))
31
+ @click.option("--query", "query_expr", default=None, metavar="EXPR", help="JMESPath query on the device map")
32
+ @click.option("--ssh-key", default=None, type=click.Path(), help="SSH private key path")
33
+ def device_map_cmd(
34
+ host, name, tags, save, out_path, output_fmt,
35
+ no_infra, list_saved, show_file, diff_files, ssh_key, apply_config, query_expr,
36
+ ):
37
+ """Generate a full standardized device snapshot (hardware + infra + diagnostics)."""
38
+ from ...models import DeviceMap
39
+
40
+ console = Console()
41
+
42
+ if list_saved:
43
+ print_saved_maps(console)
44
+ return
45
+
46
+ if show_file:
47
+ click.echo(DeviceMap.load(Path(show_file)).to_yaml())
48
+ return
49
+
50
+ if diff_files:
51
+ print_device_map_diff(console, Path(diff_files[0]), Path(diff_files[1]))
52
+ return
53
+
54
+ if not host:
55
+ console.print("[red]✗ HOST required (or use --list / --show / --diff)[/red]")
56
+ sys.exit(1)
57
+
58
+ if apply_config:
59
+ from ...config_apply import apply_config_file
60
+ apply_config_file(apply_config, ssh_key=ssh_key, console=console)
61
+ return
62
+
63
+ device_map = probe_device_map(
64
+ console, host, name=name, tags=tags, ssh_key=ssh_key, no_infra=no_infra,
65
+ )
66
+
67
+ if query_expr:
68
+ execute_query_device_map(console, device_map, query_expr, output_fmt)
69
+ return
70
+
71
+ emit_device_map(device_map, output_fmt)
72
+
73
+ if save or out_path:
74
+ saved_path = device_map.save(Path(out_path) if out_path else None)
75
+ console.print(f"\n[green]✓ saved:[/green] {saved_path}")
76
+
77
+ if device_map.has_errors:
78
+ sys.exit(1)
@@ -0,0 +1,174 @@
1
+ """Non-Click logic for `redeploy device-map`."""
2
+ from __future__ import annotations
3
+
4
+ import sys
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING
8
+
9
+ from rich import box
10
+ from rich.console import Console
11
+ from rich.table import Table
12
+
13
+ if TYPE_CHECKING:
14
+ from ...models import DeviceMap
15
+
16
+
17
+ def print_saved_maps(console: Console) -> None:
18
+ from ...models import DeviceMap
19
+
20
+ maps = DeviceMap.list_saved()
21
+ if not maps:
22
+ console.print("[dim]No saved device maps. Run:[/dim] redeploy device-map HOST --save")
23
+ return
24
+
25
+ table = Table(show_header=True, box=box.SIMPLE, padding=(0, 2))
26
+ table.add_column("File", style="bold")
27
+ table.add_column("Host", style="cyan")
28
+ table.add_column("Board")
29
+ table.add_column("Display")
30
+ table.add_column("Issues")
31
+ table.add_column("Scanned", style="dim")
32
+
33
+ for path in maps:
34
+ try:
35
+ dm = DeviceMap.load(path)
36
+ board = dm.hardware.board if dm.hardware else "?"
37
+ errors = sum(1 for i in dm.issues if i.get("severity") in ("error", "critical"))
38
+ warn = sum(1 for i in dm.issues if i.get("severity") == "warning")
39
+ issues_str = (
40
+ (f"[red]{errors}E[/red] " if errors else "")
41
+ + (f"[yellow]{warn}W[/yellow]" if warn else "")
42
+ or "[green]OK[/green]"
43
+ )
44
+ table.add_row(
45
+ path.name,
46
+ dm.host,
47
+ board or "?",
48
+ dm.display_summary,
49
+ issues_str,
50
+ dm.scanned_at.strftime("%Y-%m-%d %H:%M"),
51
+ )
52
+ except Exception as exc:
53
+ table.add_row(path.name, "[red]parse error[/red]", str(exc)[:40], "", "", "")
54
+
55
+ console.print(table)
56
+
57
+
58
+ def print_device_map_diff(console: Console, path_a: Path, path_b: Path) -> None:
59
+ from ...models import DeviceMap
60
+
61
+ map_a = DeviceMap.load(path_a)
62
+ map_b = DeviceMap.load(path_b)
63
+ console.print(
64
+ f"\n[bold]diff[/bold] [cyan]{map_a.id}[/cyan] ({map_a.scanned_at.strftime('%m-%d %H:%M')})"
65
+ f" → [cyan]{map_b.id}[/cyan] ({map_b.scanned_at.strftime('%m-%d %H:%M')})\n"
66
+ )
67
+
68
+ def flatten(obj, prefix: str = "") -> dict:
69
+ out: dict = {}
70
+ if isinstance(obj, dict):
71
+ for key, value in obj.items():
72
+ out.update(flatten(value, f"{prefix}.{key}" if prefix else key))
73
+ elif isinstance(obj, list):
74
+ out[prefix] = str(obj)
75
+ else:
76
+ out[prefix] = str(obj)
77
+ return out
78
+
79
+ flat_a = flatten(map_a.model_dump(mode="json"))
80
+ flat_b = flatten(map_b.model_dump(mode="json"))
81
+ all_keys = sorted(set(flat_a) | set(flat_b))
82
+ changed = [(k, flat_a.get(k), flat_b.get(k)) for k in all_keys if flat_a.get(k) != flat_b.get(k)]
83
+ changed = [(k, old, new) for k, old, new in changed if not any(s in k for s in ("scanned_at", "id"))]
84
+
85
+ if not changed:
86
+ console.print("[green]✓ No differences[/green]")
87
+ return
88
+
89
+ table = Table(show_header=True, box=box.SIMPLE, padding=(0, 1))
90
+ table.add_column("Key")
91
+ table.add_column(f"A: {map_a.id[:20]}", style="red")
92
+ table.add_column(f"B: {map_b.id[:20]}", style="green")
93
+ for key, old, new in changed[:50]:
94
+ table.add_row(key, str(old or "—")[:60], str(new or "—")[:60])
95
+ console.print(table)
96
+ if len(changed) > 50:
97
+ console.print(f"[dim]... and {len(changed) - 50} more differences[/dim]")
98
+
99
+
100
+ def probe_device_map(
101
+ console: Console,
102
+ host: str,
103
+ *,
104
+ name: str,
105
+ tags: tuple[str, ...],
106
+ ssh_key: str | None,
107
+ no_infra: bool,
108
+ ):
109
+ from ...detect import Detector
110
+ from ...detect.hardware import probe_hardware
111
+ from ...detect.remote import RemoteProbe
112
+ from ...models import DeviceMap
113
+
114
+ probe = RemoteProbe(host, ssh_key=ssh_key) if ssh_key else RemoteProbe(host)
115
+
116
+ with console.status(f"[cyan]Probing hardware on {host}…[/cyan]"):
117
+ try:
118
+ hw_info = probe_hardware(probe)
119
+ except ConnectionError as exc:
120
+ console.print(f"[red]✗ {exc}[/red]")
121
+ sys.exit(2)
122
+
123
+ infra_state = None
124
+ if not no_infra:
125
+ with console.status(f"[cyan]Probing infra on {host}…[/cyan]"):
126
+ try:
127
+ infra_state = Detector(host).run()
128
+ except Exception as exc:
129
+ console.print(f"[yellow]⚠ infra probe failed (continuing): {exc}[/yellow]")
130
+
131
+ issues: list[dict] = []
132
+ if hw_info:
133
+ for diag in hw_info.diagnostics:
134
+ issues.append({
135
+ "source": "hardware",
136
+ "component": diag.component,
137
+ "severity": diag.severity,
138
+ "message": diag.message,
139
+ "fix": diag.fix,
140
+ })
141
+ if infra_state:
142
+ for conflict in infra_state.conflicts:
143
+ issues.append({
144
+ "source": "infra",
145
+ "component": "service",
146
+ "severity": conflict.severity.value,
147
+ "message": f"{conflict.type}: {conflict.description}",
148
+ "fix": conflict.fix_hint,
149
+ })
150
+
151
+ return DeviceMap(
152
+ id=host,
153
+ host=host,
154
+ name=name,
155
+ tags=list(tags),
156
+ scanned_at=datetime.now(timezone.utc),
157
+ hardware=hw_info,
158
+ infra=infra_state,
159
+ issues=issues,
160
+ )
161
+
162
+
163
+ def execute_query_device_map(console: Console, dm: "DeviceMap", query_expr: str, output_fmt: str) -> None:
164
+ from ...cli.query import execute_query
165
+ execute_query(dm, query_expr, output_fmt, echo=console.print)
166
+
167
+
168
+ def emit_device_map(dm: "DeviceMap", output_fmt: str) -> None:
169
+ import click
170
+ if output_fmt == "json":
171
+ import json as json_mod
172
+ click.echo(json_mod.dumps(dm.model_dump(mode="json"), indent=2))
173
+ else:
174
+ click.echo(dm.to_yaml())
@@ -7,7 +7,6 @@ from pathlib import Path
7
7
 
8
8
  import click
9
9
  from rich.console import Console
10
- from rich.table import Table
11
10
 
12
11
 
13
12
  @click.command()
@@ -28,18 +27,13 @@ def devices(tag, strategy, rpi, reachable, as_json):
28
27
  import json as _json
29
28
  from ...models import DeviceRegistry
30
29
 
30
+ from .devices_display import filter_devices, render_devices_table
31
+
31
32
  console = Console()
32
33
  reg = DeviceRegistry.load()
33
- devs = reg.devices
34
-
35
- if tag:
36
- devs = [d for d in devs if tag in d.tags]
37
- if strategy:
38
- devs = [d for d in devs if d.strategy == strategy]
39
- if rpi:
40
- devs = [d for d in devs if "raspberry-pi" in d.tags]
41
- if reachable:
42
- devs = [d for d in devs if d.is_reachable]
34
+ devs = filter_devices(
35
+ reg.devices, tag=tag, strategy=strategy, rpi=rpi, reachable=reachable,
36
+ )
43
37
 
44
38
  if as_json:
45
39
  print(
@@ -55,35 +49,7 @@ def devices(tag, strategy, rpi, reachable, as_json):
55
49
  console.print("[dim]No devices found. Run:[/dim] redeploy scan")
56
50
  return
57
51
 
58
- t = Table(show_header=True, box=None, padding=(0, 2))
59
- t.add_column("ID", style="bold")
60
- t.add_column("Host")
61
- t.add_column("Strategy", style="cyan")
62
- t.add_column("App")
63
- t.add_column("Tags", style="dim")
64
- t.add_column("Last seen", style="dim")
65
- t.add_column("SSH", style="dim")
66
-
67
- for d in devs:
68
- seen = d.last_seen.strftime("%m-%d %H:%M") if d.last_seen else "never"
69
- ssh = "[green]✓[/green]" if d.last_ssh_ok else "[red]✗[/red]"
70
- tags_str = ",".join(d.tags) or "—"
71
- # Highlight raspberry-pi tag
72
- if "raspberry-pi" in d.tags:
73
- tags_str = tags_str.replace("raspberry-pi", "[bold magenta]raspberry-pi[/bold magenta]")
74
- t.add_row(
75
- d.id,
76
- d.host,
77
- d.strategy,
78
- d.app or "—",
79
- tags_str,
80
- seen,
81
- ssh,
82
- )
83
- console.print(t)
84
- console.print(
85
- f"\n [dim]{len(devs)} device(s) • registry: {DeviceRegistry.default_path()}[/dim]"
86
- )
52
+ render_devices_table(console, devs)
87
53
 
88
54
 
89
55
  @click.command()
@@ -0,0 +1,61 @@
1
+ """Rich table rendering for `redeploy devices`."""
2
+ from __future__ import annotations
3
+
4
+ from rich.console import Console
5
+ from rich.table import Table
6
+
7
+ from ...models import DeviceRegistry, KnownDevice
8
+
9
+
10
+ def filter_devices(
11
+ devices: list[KnownDevice],
12
+ *,
13
+ tag: str | None,
14
+ strategy: str | None,
15
+ rpi: bool,
16
+ reachable: bool,
17
+ ) -> list[KnownDevice]:
18
+ result = devices
19
+ if tag:
20
+ result = [d for d in result if tag in d.tags]
21
+ if strategy:
22
+ result = [d for d in result if d.strategy == strategy]
23
+ if rpi:
24
+ result = [d for d in result if "raspberry-pi" in d.tags]
25
+ if reachable:
26
+ result = [d for d in result if d.is_reachable]
27
+ return result
28
+
29
+
30
+ def render_devices_table(console: Console, devices: list[KnownDevice]) -> None:
31
+ table = Table(show_header=True, box=None, padding=(0, 2))
32
+ table.add_column("ID", style="bold")
33
+ table.add_column("Host")
34
+ table.add_column("Strategy", style="cyan")
35
+ table.add_column("App")
36
+ table.add_column("Tags", style="dim")
37
+ table.add_column("Last seen", style="dim")
38
+ table.add_column("SSH", style="dim")
39
+
40
+ for device in devices:
41
+ seen = device.last_seen.strftime("%m-%d %H:%M") if device.last_seen else "never"
42
+ ssh = "[green]✓[/green]" if device.last_ssh_ok else "[red]✗[/red]"
43
+ tags_str = ",".join(device.tags) or "—"
44
+ if "raspberry-pi" in device.tags:
45
+ tags_str = tags_str.replace(
46
+ "raspberry-pi", "[bold magenta]raspberry-pi[/bold magenta]",
47
+ )
48
+ table.add_row(
49
+ device.id,
50
+ device.host,
51
+ device.strategy,
52
+ device.app or "—",
53
+ tags_str,
54
+ seen,
55
+ ssh,
56
+ )
57
+
58
+ console.print(table)
59
+ console.print(
60
+ f"\n [dim]{len(devices)} device(s) • registry: {DeviceRegistry.default_path()}[/dim]"
61
+ )
@@ -0,0 +1,75 @@
1
+ """`redeploy migrate` command — detect → plan → apply."""
2
+ from __future__ import annotations
3
+
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import click
8
+ from rich.console import Console
9
+
10
+
11
+ @click.command()
12
+ @click.option("--host", required=True, help="SSH host (user@ip) or 'local'")
13
+ @click.option("--app", default=None, show_default=True, help="Application name (default from redeploy.yaml)")
14
+ @click.option("--domain", default=None)
15
+ @click.option("--target", default=None, type=click.Path(), help="Target config YAML")
16
+ @click.option(
17
+ "--strategy", default="docker_full", show_default=True,
18
+ type=click.Choice(["docker_full", "podman_quadlet", "k3s", "native_kiosk", "systemd"]),
19
+ )
20
+ @click.option("--version", "target_version", default=None)
21
+ @click.option("--compose", multiple=True)
22
+ @click.option("--env-file", default=None)
23
+ @click.option("--dry-run", is_flag=True)
24
+ @click.option("--infra-out", default="infra.yaml", show_default=True, type=click.Path())
25
+ @click.option("--plan-out", default="migration-plan.yaml", show_default=True, type=click.Path())
26
+ @click.pass_context
27
+ def migrate(ctx, host, app, domain, target, strategy, target_version,
28
+ compose, env_file, dry_run, infra_out, plan_out):
29
+ """Full pipeline: detect → plan → apply."""
30
+ from ...apply import Executor
31
+ from ...detect import Detector
32
+ from ...models import DeployStrategy, ProjectManifest
33
+ from ...plan import Planner
34
+
35
+ console = Console()
36
+ manifest = ProjectManifest.find_and_load(Path.cwd())
37
+ app = app or (manifest.app if manifest else "c2004")
38
+ domain = domain or (manifest.domain if manifest else None)
39
+
40
+ console.print("\n[bold]Step 1/3 — detect[/bold]")
41
+ detector = Detector(host=host, app=app, domain=domain)
42
+ state = detector.run()
43
+ detector.save(state, Path(infra_out))
44
+ console.print(
45
+ f" Strategy: {state.detected_strategy} "
46
+ f" Version: {state.current_version or '?'} "
47
+ f" Conflicts: {len(state.conflicts)}"
48
+ )
49
+
50
+ console.print("\n[bold]Step 2/3 — plan[/bold]")
51
+ planner = Planner.from_files(Path(infra_out), Path(target) if target else None)
52
+ planner.target.strategy = DeployStrategy(strategy)
53
+ if domain:
54
+ planner.target.domain = domain
55
+ if target_version:
56
+ planner.target.verify_version = target_version
57
+ if compose:
58
+ planner.target.compose_files = list(compose)
59
+ if env_file:
60
+ planner.target.env_file = env_file
61
+
62
+ migration = planner.run()
63
+ planner.save(migration, Path(plan_out))
64
+ console.print(
65
+ f" Steps: {len(migration.steps)} Risk: {migration.risk.value} "
66
+ f"Downtime: {migration.estimated_downtime}"
67
+ )
68
+
69
+ console.print(f"\n[bold]Step 3/3 — apply{' (dry-run)' if dry_run else ''}[/bold]")
70
+ executor = Executor(migration, dry_run=dry_run)
71
+ ok = executor.run()
72
+ console.print(f"\n{executor.summary()}")
73
+
74
+ if not ok:
75
+ sys.exit(1)
@@ -0,0 +1,9 @@
1
+ """plan, apply, migrate, run commands — re-exports for CLI registration."""
2
+ from __future__ import annotations
3
+
4
+ from .apply_cmd import apply
5
+ from .migrate_cmd import migrate
6
+ from .plan_cmd import plan
7
+ from .run_cmd import run
8
+
9
+ __all__ = ["plan", "apply", "migrate", "run"]