redeploy 0.2.41__tar.gz → 0.2.43__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 (179) hide show
  1. {redeploy-0.2.41 → redeploy-0.2.43}/PKG-INFO +157 -6
  2. {redeploy-0.2.41 → redeploy-0.2.43}/README.md +155 -5
  3. {redeploy-0.2.41 → redeploy-0.2.43}/pyproject.toml +2 -1
  4. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/__init__.py +1 -1
  5. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/apply/executor.py +4 -0
  6. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/apply/handlers.py +134 -0
  7. redeploy-0.2.43/redeploy/apply/state_apply.py +222 -0
  8. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/cli/__init__.py +2 -0
  9. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/cli/commands/blueprint.py +31 -1
  10. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/cli/commands/device_map.py +11 -0
  11. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/cli/commands/hardware.py +42 -1
  12. redeploy-0.2.43/redeploy/cli/commands/push.py +110 -0
  13. redeploy-0.2.43/redeploy/hardware/kiosk/__init__.py +18 -0
  14. redeploy-0.2.43/redeploy/hardware/kiosk/autostart.py +84 -0
  15. redeploy-0.2.43/redeploy/hardware/kiosk/browsers.py +78 -0
  16. redeploy-0.2.43/redeploy/hardware/kiosk/compositors.py +80 -0
  17. redeploy-0.2.43/redeploy/hardware/kiosk/output_profiles.py +68 -0
  18. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/models.py +3 -0
  19. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy.egg-info/PKG-INFO +157 -6
  20. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy.egg-info/SOURCES.txt +8 -0
  21. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy.egg-info/requires.txt +1 -0
  22. redeploy-0.2.43/tests/test_hardware_kiosk.py +230 -0
  23. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/apply/__init__.py +0 -0
  24. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/apply/exceptions.py +0 -0
  25. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/apply/progress.py +0 -0
  26. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/apply/rollback.py +0 -0
  27. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/apply/state.py +0 -0
  28. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/apply/utils/__init__.py +0 -0
  29. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/apply/utils/run_container_build.py +0 -0
  30. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/audit.py +0 -0
  31. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/blueprint/__init__.py +0 -0
  32. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/blueprint/extractor.py +0 -0
  33. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/blueprint/generators/__init__.py +0 -0
  34. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/blueprint/generators/docker_compose.py +0 -0
  35. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/blueprint/generators/migration.py +0 -0
  36. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/cli/commands/__init__.py +0 -0
  37. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/cli/commands/audit.py +0 -0
  38. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/cli/commands/detect.py +0 -0
  39. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/cli/commands/devices.py +0 -0
  40. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/cli/commands/diagnose.py +0 -0
  41. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/cli/commands/diff.py +0 -0
  42. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/cli/commands/exec_.py +0 -0
  43. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/cli/commands/export.py +0 -0
  44. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/cli/commands/import_.py +0 -0
  45. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/cli/commands/init.py +0 -0
  46. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/cli/commands/inspect.py +0 -0
  47. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/cli/commands/patterns.py +0 -0
  48. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/cli/commands/plan_apply.py +0 -0
  49. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/cli/commands/plugin.py +0 -0
  50. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/cli/commands/probe.py +0 -0
  51. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/cli/commands/state.py +0 -0
  52. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/cli/commands/status.py +0 -0
  53. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/cli/commands/target.py +0 -0
  54. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/cli/commands/version/__init__.py +0 -0
  55. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/cli/commands/version/commands.py +0 -0
  56. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/cli/commands/version/helpers.py +0 -0
  57. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/cli/commands/version/monorepo.py +0 -0
  58. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/cli/commands/version/release.py +0 -0
  59. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/cli/commands/version/scanner.py +0 -0
  60. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/cli/commands/workflow.py +0 -0
  61. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/cli/core.py +0 -0
  62. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/cli/display.py +0 -0
  63. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/cli.py +0 -0
  64. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/data_sync.py +0 -0
  65. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/detect/__init__.py +0 -0
  66. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/detect/builtin/__init__.py +0 -0
  67. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/detect/detector.py +0 -0
  68. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/detect/hardware.py +0 -0
  69. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/detect/hardware_rules.py +0 -0
  70. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/detect/probes.py +0 -0
  71. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/detect/remote.py +0 -0
  72. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/detect/templates.py +0 -0
  73. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/detect/workflow.py +0 -0
  74. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/discovery.py +0 -0
  75. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/dsl/__init__.py +0 -0
  76. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/dsl/loader.py +0 -0
  77. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/dsl/parser.py +0 -0
  78. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/dsl_python/__init__.py +0 -0
  79. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/dsl_python/context.py +0 -0
  80. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/dsl_python/decorators.py +0 -0
  81. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/dsl_python/docker_steps.py +0 -0
  82. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/dsl_python/exceptions.py +0 -0
  83. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/dsl_python/runner.py +0 -0
  84. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/dsl_python/steps.py +0 -0
  85. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/fleet.py +0 -0
  86. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/hardware/__init__.py +0 -0
  87. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/hardware/config_txt.py +0 -0
  88. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/hardware/data/__init__.py +0 -0
  89. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/hardware/data/hyperpixel.py +0 -0
  90. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/hardware/data/official.py +0 -0
  91. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/hardware/data/waveshare.py +0 -0
  92. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/hardware/fixes.py +0 -0
  93. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/hardware/panels.py +0 -0
  94. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/hardware/raspi_config.py +0 -0
  95. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/iac/__init__.py +0 -0
  96. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/iac/base.py +0 -0
  97. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/iac/docker_compose.py +0 -0
  98. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/iac/parsers/__init__.py +0 -0
  99. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/iac/parsers/compose.py +0 -0
  100. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/iac/registry.py +0 -0
  101. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/markpact/__init__.py +0 -0
  102. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/markpact/compiler.py +0 -0
  103. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/markpact/models.py +0 -0
  104. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/markpact/parser.py +0 -0
  105. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/observe.py +0 -0
  106. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/parse.py +0 -0
  107. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/patterns.py +0 -0
  108. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/plan/__init__.py +0 -0
  109. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/plan/planner.py +0 -0
  110. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/plugins/__init__.py +0 -0
  111. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/plugins/builtin/__init__.py +0 -0
  112. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/plugins/builtin/browser_reload.py +0 -0
  113. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/plugins/builtin/hardware_diagnostic.py +0 -0
  114. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/plugins/builtin/notify.py +0 -0
  115. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/plugins/builtin/process_control.py +0 -0
  116. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/plugins/builtin/systemd_reload.py +0 -0
  117. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/spec_loader.py +0 -0
  118. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/ssh.py +0 -0
  119. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/steps.py +0 -0
  120. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/tests/__init__.py +0 -0
  121. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/tests/test_audit.py +0 -0
  122. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/tests/test_cli.py +0 -0
  123. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/tests/test_data_sync.py +0 -0
  124. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/tests/test_discovery.py +0 -0
  125. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/tests/test_dsl.py +0 -0
  126. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/tests/test_examples.py +0 -0
  127. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/tests/test_executor.py +0 -0
  128. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/tests/test_fleet.py +0 -0
  129. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/tests/test_git_integration.py +0 -0
  130. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/tests/test_iac.py +0 -0
  131. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/tests/test_manifest.py +0 -0
  132. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/tests/test_markpact_compiler.py +0 -0
  133. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/tests/test_markpact_parser.py +0 -0
  134. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/tests/test_models.py +0 -0
  135. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/tests/test_observe.py +0 -0
  136. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/tests/test_parse.py +0 -0
  137. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/tests/test_patterns.py +0 -0
  138. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/tests/test_planner_edge.py +0 -0
  139. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/tests/test_plugins.py +0 -0
  140. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/tests/test_probes.py +0 -0
  141. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/tests/test_public_api.py +0 -0
  142. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/tests/test_resume.py +0 -0
  143. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/tests/test_spec_loader.py +0 -0
  144. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/tests/test_ssh.py +0 -0
  145. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/tests/test_templates.py +0 -0
  146. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/tests/test_verify.py +0 -0
  147. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/tests/test_version.py +0 -0
  148. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/tests/test_version_changelog.py +0 -0
  149. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/tests/test_version_cli.py +0 -0
  150. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/tests/test_version_manifest.py +0 -0
  151. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/tests/test_version_sources.py +0 -0
  152. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/tests/test_workflow.py +0 -0
  153. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/verify.py +0 -0
  154. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/version/__init__.py +0 -0
  155. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/version/bump.py +0 -0
  156. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/version/changelog.py +0 -0
  157. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/version/commits.py +0 -0
  158. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/version/diff.py +0 -0
  159. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/version/git_integration.py +0 -0
  160. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/version/git_transaction.py +0 -0
  161. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/version/manifest.py +0 -0
  162. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/version/sources/__init__.py +0 -0
  163. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/version/sources/base.py +0 -0
  164. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/version/sources/json_.py +0 -0
  165. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/version/sources/plain.py +0 -0
  166. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/version/sources/regex.py +0 -0
  167. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/version/sources/toml_.py +0 -0
  168. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/version/sources/yaml_.py +0 -0
  169. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/version/transaction.py +0 -0
  170. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy/version.py +0 -0
  171. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy.egg-info/dependency_links.txt +0 -0
  172. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy.egg-info/entry_points.txt +0 -0
  173. {redeploy-0.2.41 → redeploy-0.2.43}/redeploy.egg-info/top_level.txt +0 -0
  174. {redeploy-0.2.41 → redeploy-0.2.43}/setup.cfg +0 -0
  175. {redeploy-0.2.41 → redeploy-0.2.43}/tests/test_hardware_config.py +0 -0
  176. {redeploy-0.2.41 → redeploy-0.2.43}/tests/test_hardware_rules.py +0 -0
  177. {redeploy-0.2.41 → redeploy-0.2.43}/tests/test_iac.py +0 -0
  178. {redeploy-0.2.41 → redeploy-0.2.43}/tests/test_parse.py +0 -0
  179. {redeploy-0.2.41 → redeploy-0.2.43}/tests/test_redeploy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redeploy
3
- Version: 0.2.41
3
+ Version: 0.2.43
4
4
  Summary: Infrastructure migration toolkit: detect → plan → apply
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -12,6 +12,7 @@ Requires-Dist: loguru>=0.7
12
12
  Requires-Dist: paramiko>=3.0
13
13
  Requires-Dist: httpx>=0.25
14
14
  Requires-Dist: rich>=13.0
15
+ Requires-Dist: jmespath>=1.0
15
16
  Requires-Dist: goal>=2.1.0
16
17
  Requires-Dist: costs>=0.1.20
17
18
  Requires-Dist: pfix>=0.1.60
@@ -27,17 +28,17 @@ Requires-Dist: pfix>=0.1.60; extra == "dev"
27
28
 
28
29
  ## AI Cost Tracking
29
30
 
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)
31
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.43-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
32
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-21.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
32
33
 
33
- - 🤖 **LLM usage:** $7.5000 (62 commits)
34
- - 👤 **Human dev:** ~$2044 (20.4h @ $100/h, 30min dedup)
34
+ - 🤖 **LLM usage:** $7.5000 (64 commits)
35
+ - 👤 **Human dev:** ~$2099 (21.0h @ $100/h, 30min dedup)
35
36
 
36
37
  Generated on 2026-04-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
37
38
 
38
39
  ---
39
40
 
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
+ ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.43-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
41
42
 
42
43
  Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
43
44
 
@@ -152,6 +153,72 @@ redeploy device-rm root@10.0.0.5
152
153
 
153
154
  Registry is stored at `~/.config/redeploy/devices.yaml` (chmod 600 — safe for SSH key paths).
154
155
 
156
+ ## Declarative config workflow
157
+
158
+ redeploy supports a declarative configuration workflow for hardware settings — scan to YAML, edit locally, and apply to device:
159
+
160
+ ```bash
161
+ # 1. Scan hardware state to YAML
162
+ redeploy hardware pi@192.168.188.109 > hardware.yaml
163
+ redeploy device-map pi@192.168.188.109 > device-map.yaml
164
+
165
+ # 2. Edit YAML locally (e.g., change display rotation)
166
+ # hardware.yaml:
167
+ # drm_outputs:
168
+ # - name: card0-DSI-2
169
+ # connector: DSI-2
170
+ # transform: '270' # ← edit this value
171
+
172
+ # 3. Apply config to remote device
173
+ redeploy hardware pi@192.168.188.109 --apply-config hardware.yaml
174
+ redeploy device-map pi@192.168.188.109 --apply-config device-map.yaml
175
+ ```
176
+
177
+ **What `--apply-config` does:**
178
+ - Applies display transforms via `wlr-randr` (Wayland compositor)
179
+ - Updates kanshi config (`~/.config/kanshi/config`) for persistent display rotation
180
+ - Sets backlight brightness and power state
181
+ - Supports both YAML and JSON config files
182
+
183
+ **Supported commands:**
184
+ - `redeploy hardware --apply-config FILE`
185
+ - `redeploy device-map --apply-config FILE`
186
+ - `redeploy blueprint show FILE --apply-config FILE`
187
+
188
+ ## JMESPath query support
189
+
190
+ Extract specific values from YAML/JSON output using JMESPath query language (similar to XPath for XML):
191
+
192
+ ```bash
193
+ # Simple path queries
194
+ redeploy hardware pi@192.168.188.109 --query "drm_outputs[0].transform"
195
+ redeploy hardware pi@192.168.188.109 --query "kernel"
196
+ redeploy device-map pi@192.168.188.109 --query "host"
197
+
198
+ # Filter queries
199
+ redeploy hardware pi@192.168.188.109 --query "backlights[?name==\`11-0045\`].brightness"
200
+ redeploy hardware pi@192.168.188.109 --query "drm_outputs[?connector==\`DSI-2\`].transform"
201
+
202
+ # From saved YAML files
203
+ redeploy blueprint show blueprint.yaml --query "hardware.drm_outputs[0].transform"
204
+ redeploy device-map --show device-map.yaml --query "tags"
205
+
206
+ # JSON output
207
+ redeploy hardware pi@192.168.188.109 --query "tags" --format json
208
+ ```
209
+
210
+ **JMESPath features:**
211
+ - **Simple paths:** `kernel`, `host`, `board`
212
+ - **Array indexing:** `drm_outputs[0].transform`
213
+ - **Filtering:** `[?name==\`11-0045\`]`
214
+ - **Projections:** `backlights[0].[name,brightness]`
215
+ - **Wildcards:** `drm_outputs[*].transform`
216
+
217
+ **Supported commands:**
218
+ - `redeploy hardware --query EXPR`
219
+ - `redeploy device-map --query EXPR`
220
+ - `redeploy blueprint show FILE --query EXPR`
221
+
155
222
  ## CLI reference
156
223
 
157
224
  ### `redeploy run SPEC [options]`
@@ -614,6 +681,90 @@ redeploy run examples/01-vps-version-bump/migration.yaml --plan-only
614
681
  redeploy run examples/04-rpi-kiosk/migration.yaml --plan-only
615
682
  ```
616
683
 
684
+ ## Hardware diagnostics commands
685
+
686
+ ### `redeploy hardware HOST [options]`
687
+
688
+ Probe and diagnose hardware on a remote host (DSI display, DRM connectors, backlight, I2C, config.txt, Wayland compositor).
689
+
690
+ | Option | Description |
691
+ |--------|-------------|
692
+ | `--format [yaml|json]` | Output format (default: yaml) |
693
+ | `--fix` | Print fix commands for all issues found |
694
+ | `--apply-fix COMPONENT` | Run fix for specific component via SSH |
695
+ | `--panel PANEL_ID` | Specify panel ID explicitly |
696
+ | `--list-panels` | List available panel definitions |
697
+ | `--set-transform TRANSFORM` | Set display rotation for DSI output (normal, 90, 180, 270, flipped, etc.) |
698
+ | `--apply-config FILE` | Apply display settings from YAML/JSON config file |
699
+ | `--query EXPR` | Extract specific values using JMESPath query |
700
+ | `--ssh-key PATH` | SSH private key path |
701
+
702
+ ```bash
703
+ redeploy hardware pi@192.168.188.109
704
+ redeploy hardware pi@192.168.188.109 --fix
705
+ redeploy hardware pi@192.168.188.109 --set-transform 270
706
+ redeploy hardware pi@192.168.188.109 --apply-config hardware.yaml
707
+ redeploy hardware pi@192.168.188.109 --query "drm_outputs[0].transform"
708
+ ```
709
+
710
+ ### `redeploy device-map HOST [options]`
711
+
712
+ Generate full device snapshot (hardware + infra + diagnostics).
713
+
714
+ | Option | Description |
715
+ |--------|-------------|
716
+ | `--name TEXT` | Human-friendly device label |
717
+ | `--tag TEXT` | Tag(s) to attach (repeatable) |
718
+ | `--save` | Persist map to `~/.config/redeploy/device-maps/` |
719
+ | `--out PATH` | Save to specific file |
720
+ | `--format [yaml|json]` | Output format (default: yaml) |
721
+ | `--no-infra` | Skip infra probe (hardware only) |
722
+ | `--list` | List saved device maps |
723
+ | `--show PATH` | Load and display saved device-map file |
724
+ | `--diff PATH...` | Diff two saved device-map files |
725
+ | `--apply-config FILE` | Apply hardware/infra settings from YAML config file |
726
+ | `--query EXPR` | Extract specific values using JMESPath query |
727
+ | `--ssh-key PATH` | SSH private key path |
728
+
729
+ ```bash
730
+ redeploy device-map pi@192.168.188.109 --save --name "kiosk-lab"
731
+ redeploy device-map --list
732
+ redeploy device-map --show ~/.config/redeploy/device-maps/pi_at_192.168.188.109.yaml
733
+ redeploy device-map pi@192.168.188.109 --apply-config device-map.yaml
734
+ redeploy device-map pi@192.168.188.109 --query "hardware.drm_outputs[0].transform"
735
+ ```
736
+
737
+ ### `redeploy blueprint [command]`
738
+
739
+ Manage device blueprints (capture, show, list, twin, migrate).
740
+
741
+ #### `redeploy blueprint capture HOST [options]`
742
+
743
+ Capture device state as blueprint.
744
+
745
+ | Option | Description |
746
+ |--------|-------------|
747
+ | `--format [yaml|json]` | Output format (default: yaml) |
748
+ | `--save` | Persist to `~/.config/redeploy/blueprints/` |
749
+ | `--out PATH` | Save to specific file |
750
+ | `--ssh-key PATH` | SSH private key path |
751
+
752
+ #### `redeploy blueprint show FILE [options]`
753
+
754
+ Display saved blueprint.
755
+
756
+ | Option | Description |
757
+ |--------|-------------|
758
+ | `--format [yaml|json]` | Output format (default: yaml) |
759
+ | `--apply-config FILE` | Apply blueprint settings from YAML config file |
760
+ | `--query EXPR` | Extract specific values using JMESPath query |
761
+
762
+ ```bash
763
+ redeploy blueprint capture pi@192.168.188.109 > blueprint.yaml
764
+ redeploy blueprint show blueprint.yaml --apply-config blueprint.yaml
765
+ redeploy blueprint show blueprint.yaml --query "hardware.drm_outputs[0].transform"
766
+ ```
767
+
617
768
  ## License
618
769
 
619
770
  Licensed under Apache-2.0.
@@ -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.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)
6
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.43-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-21.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $7.5000 (62 commits)
10
- - 👤 **Human dev:** ~$2044 (20.4h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $7.5000 (64 commits)
10
+ - 👤 **Human dev:** ~$2099 (21.0h @ $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.41-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.43-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
 
@@ -128,6 +128,72 @@ redeploy device-rm root@10.0.0.5
128
128
 
129
129
  Registry is stored at `~/.config/redeploy/devices.yaml` (chmod 600 — safe for SSH key paths).
130
130
 
131
+ ## Declarative config workflow
132
+
133
+ redeploy supports a declarative configuration workflow for hardware settings — scan to YAML, edit locally, and apply to device:
134
+
135
+ ```bash
136
+ # 1. Scan hardware state to YAML
137
+ redeploy hardware pi@192.168.188.109 > hardware.yaml
138
+ redeploy device-map pi@192.168.188.109 > device-map.yaml
139
+
140
+ # 2. Edit YAML locally (e.g., change display rotation)
141
+ # hardware.yaml:
142
+ # drm_outputs:
143
+ # - name: card0-DSI-2
144
+ # connector: DSI-2
145
+ # transform: '270' # ← edit this value
146
+
147
+ # 3. Apply config to remote device
148
+ redeploy hardware pi@192.168.188.109 --apply-config hardware.yaml
149
+ redeploy device-map pi@192.168.188.109 --apply-config device-map.yaml
150
+ ```
151
+
152
+ **What `--apply-config` does:**
153
+ - Applies display transforms via `wlr-randr` (Wayland compositor)
154
+ - Updates kanshi config (`~/.config/kanshi/config`) for persistent display rotation
155
+ - Sets backlight brightness and power state
156
+ - Supports both YAML and JSON config files
157
+
158
+ **Supported commands:**
159
+ - `redeploy hardware --apply-config FILE`
160
+ - `redeploy device-map --apply-config FILE`
161
+ - `redeploy blueprint show FILE --apply-config FILE`
162
+
163
+ ## JMESPath query support
164
+
165
+ Extract specific values from YAML/JSON output using JMESPath query language (similar to XPath for XML):
166
+
167
+ ```bash
168
+ # Simple path queries
169
+ redeploy hardware pi@192.168.188.109 --query "drm_outputs[0].transform"
170
+ redeploy hardware pi@192.168.188.109 --query "kernel"
171
+ redeploy device-map pi@192.168.188.109 --query "host"
172
+
173
+ # Filter queries
174
+ redeploy hardware pi@192.168.188.109 --query "backlights[?name==\`11-0045\`].brightness"
175
+ redeploy hardware pi@192.168.188.109 --query "drm_outputs[?connector==\`DSI-2\`].transform"
176
+
177
+ # From saved YAML files
178
+ redeploy blueprint show blueprint.yaml --query "hardware.drm_outputs[0].transform"
179
+ redeploy device-map --show device-map.yaml --query "tags"
180
+
181
+ # JSON output
182
+ redeploy hardware pi@192.168.188.109 --query "tags" --format json
183
+ ```
184
+
185
+ **JMESPath features:**
186
+ - **Simple paths:** `kernel`, `host`, `board`
187
+ - **Array indexing:** `drm_outputs[0].transform`
188
+ - **Filtering:** `[?name==\`11-0045\`]`
189
+ - **Projections:** `backlights[0].[name,brightness]`
190
+ - **Wildcards:** `drm_outputs[*].transform`
191
+
192
+ **Supported commands:**
193
+ - `redeploy hardware --query EXPR`
194
+ - `redeploy device-map --query EXPR`
195
+ - `redeploy blueprint show FILE --query EXPR`
196
+
131
197
  ## CLI reference
132
198
 
133
199
  ### `redeploy run SPEC [options]`
@@ -590,6 +656,90 @@ redeploy run examples/01-vps-version-bump/migration.yaml --plan-only
590
656
  redeploy run examples/04-rpi-kiosk/migration.yaml --plan-only
591
657
  ```
592
658
 
659
+ ## Hardware diagnostics commands
660
+
661
+ ### `redeploy hardware HOST [options]`
662
+
663
+ Probe and diagnose hardware on a remote host (DSI display, DRM connectors, backlight, I2C, config.txt, Wayland compositor).
664
+
665
+ | Option | Description |
666
+ |--------|-------------|
667
+ | `--format [yaml|json]` | Output format (default: yaml) |
668
+ | `--fix` | Print fix commands for all issues found |
669
+ | `--apply-fix COMPONENT` | Run fix for specific component via SSH |
670
+ | `--panel PANEL_ID` | Specify panel ID explicitly |
671
+ | `--list-panels` | List available panel definitions |
672
+ | `--set-transform TRANSFORM` | Set display rotation for DSI output (normal, 90, 180, 270, flipped, etc.) |
673
+ | `--apply-config FILE` | Apply display settings from YAML/JSON config file |
674
+ | `--query EXPR` | Extract specific values using JMESPath query |
675
+ | `--ssh-key PATH` | SSH private key path |
676
+
677
+ ```bash
678
+ redeploy hardware pi@192.168.188.109
679
+ redeploy hardware pi@192.168.188.109 --fix
680
+ redeploy hardware pi@192.168.188.109 --set-transform 270
681
+ redeploy hardware pi@192.168.188.109 --apply-config hardware.yaml
682
+ redeploy hardware pi@192.168.188.109 --query "drm_outputs[0].transform"
683
+ ```
684
+
685
+ ### `redeploy device-map HOST [options]`
686
+
687
+ Generate full device snapshot (hardware + infra + diagnostics).
688
+
689
+ | Option | Description |
690
+ |--------|-------------|
691
+ | `--name TEXT` | Human-friendly device label |
692
+ | `--tag TEXT` | Tag(s) to attach (repeatable) |
693
+ | `--save` | Persist map to `~/.config/redeploy/device-maps/` |
694
+ | `--out PATH` | Save to specific file |
695
+ | `--format [yaml|json]` | Output format (default: yaml) |
696
+ | `--no-infra` | Skip infra probe (hardware only) |
697
+ | `--list` | List saved device maps |
698
+ | `--show PATH` | Load and display saved device-map file |
699
+ | `--diff PATH...` | Diff two saved device-map files |
700
+ | `--apply-config FILE` | Apply hardware/infra settings from YAML config file |
701
+ | `--query EXPR` | Extract specific values using JMESPath query |
702
+ | `--ssh-key PATH` | SSH private key path |
703
+
704
+ ```bash
705
+ redeploy device-map pi@192.168.188.109 --save --name "kiosk-lab"
706
+ redeploy device-map --list
707
+ redeploy device-map --show ~/.config/redeploy/device-maps/pi_at_192.168.188.109.yaml
708
+ redeploy device-map pi@192.168.188.109 --apply-config device-map.yaml
709
+ redeploy device-map pi@192.168.188.109 --query "hardware.drm_outputs[0].transform"
710
+ ```
711
+
712
+ ### `redeploy blueprint [command]`
713
+
714
+ Manage device blueprints (capture, show, list, twin, migrate).
715
+
716
+ #### `redeploy blueprint capture HOST [options]`
717
+
718
+ Capture device state as blueprint.
719
+
720
+ | Option | Description |
721
+ |--------|-------------|
722
+ | `--format [yaml|json]` | Output format (default: yaml) |
723
+ | `--save` | Persist to `~/.config/redeploy/blueprints/` |
724
+ | `--out PATH` | Save to specific file |
725
+ | `--ssh-key PATH` | SSH private key path |
726
+
727
+ #### `redeploy blueprint show FILE [options]`
728
+
729
+ Display saved blueprint.
730
+
731
+ | Option | Description |
732
+ |--------|-------------|
733
+ | `--format [yaml|json]` | Output format (default: yaml) |
734
+ | `--apply-config FILE` | Apply blueprint settings from YAML config file |
735
+ | `--query EXPR` | Extract specific values using JMESPath query |
736
+
737
+ ```bash
738
+ redeploy blueprint capture pi@192.168.188.109 > blueprint.yaml
739
+ redeploy blueprint show blueprint.yaml --apply-config blueprint.yaml
740
+ redeploy blueprint show blueprint.yaml --query "hardware.drm_outputs[0].transform"
741
+ ```
742
+
593
743
  ## License
594
744
 
595
745
  Licensed under Apache-2.0.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "redeploy"
7
- version = "0.2.41"
7
+ version = "0.2.43"
8
8
  description = "Infrastructure migration toolkit: detect → plan → apply"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -17,6 +17,7 @@ dependencies = [
17
17
  "paramiko>=3.0",
18
18
  "httpx>=0.25",
19
19
  "rich>=13.0",
20
+ "jmespath>=1.0",
20
21
  "goal>=2.1.0",
21
22
  "costs>=0.1.20",
22
23
  "pfix>=0.1.60",
@@ -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.41"
10
+ __version__ = "0.2.43"
11
11
 
12
12
  # ── Core models ───────────────────────────────────────────────────────────────
13
13
  from .models import ( # noqa: F401
@@ -16,6 +16,7 @@ from .handlers import (
16
16
  run_docker_health_wait, run_container_log_tail, run_http_check,
17
17
  run_version_check, run_plugin, run_wait, run_inline_script,
18
18
  run_ensure_config_line, run_raspi_config,
19
+ run_ensure_kanshi_profile, run_ensure_autostart_entry, run_ensure_browser_kiosk_script,
19
20
  )
20
21
  from .progress import ProgressEmitter
21
22
  from .rollback import rollback_steps
@@ -230,6 +231,9 @@ class Executor:
230
231
  StepAction.INLINE_SCRIPT: lambda s: run_inline_script(s, self.probe, self.plan),
231
232
  StepAction.ENSURE_CONFIG_LINE: lambda s: run_ensure_config_line(s, self.probe),
232
233
  StepAction.RASPI_CONFIG: lambda s: run_raspi_config(s, self.probe),
234
+ StepAction.ENSURE_KANSHI_PROFILE: lambda s: run_ensure_kanshi_profile(s, self.probe),
235
+ StepAction.ENSURE_AUTOSTART_ENTRY: lambda s: run_ensure_autostart_entry(s, self.probe),
236
+ StepAction.ENSURE_BROWSER_KIOSK_SCRIPT: lambda s: run_ensure_browser_kiosk_script(s, self.probe),
233
237
  }
234
238
 
235
239
  handler = dispatch.get(step.action)
@@ -398,3 +398,137 @@ def run_raspi_config(step: MigrationStep, probe: "RemoteProbe") -> None:
398
398
 
399
399
  step.status = StepStatus.DONE
400
400
  step.result = f"applied: {cmd}"
401
+
402
+
403
+ # ── kiosk handlers ────────────────────────────────────────────────────────────
404
+
405
+ def run_ensure_kanshi_profile(step: MigrationStep, probe: "RemoteProbe") -> None:
406
+ """Idempotently write or replace a named kanshi output profile.
407
+
408
+ step.command — the profile block to write (rendered kanshi syntax)
409
+ step.config_file — kanshi config path (default: ~/.config/kanshi/config)
410
+ """
411
+ from ..hardware.kiosk.output_profiles import OutputProfile
412
+
413
+ profile_block = step.command
414
+ if not profile_block:
415
+ raise StepError(step, "ensure_kanshi_profile requires step.command with profile block")
416
+
417
+ config_path = step.config_file or "~/.config/kanshi/config"
418
+ mkdir_cmd = f"mkdir -p $(dirname {config_path})"
419
+ probe.run(mkdir_cmd, timeout=10)
420
+
421
+ r = probe.run(f"cat {config_path} 2>/dev/null || echo ''", timeout=10)
422
+ existing = r.out if r.ok else ""
423
+
424
+ # Extract profile name from block (first line: "profile <name> {")
425
+ first_line = profile_block.strip().splitlines()[0]
426
+ import re
427
+ m = re.match(r"profile\s+(\S+)\s*\{", first_line)
428
+ if not m:
429
+ raise StepError(step, f"Cannot parse profile name from block: {first_line!r}")
430
+ profile_name = m.group(1)
431
+
432
+ # Replace existing profile block or append
433
+ pattern = re.compile(
434
+ rf"^profile\s+{re.escape(profile_name)}\s*\{{[^}}]*\}}", re.MULTILINE | re.DOTALL
435
+ )
436
+ if pattern.search(existing):
437
+ if profile_block.strip() in existing:
438
+ step.status = StepStatus.DONE
439
+ step.result = f"no-op: profile '{profile_name}' already correct"
440
+ return
441
+ new_content = pattern.sub(profile_block.strip(), existing)
442
+ changed = True
443
+ else:
444
+ sep = "\n" if existing and not existing.endswith("\n") else ""
445
+ new_content = existing + sep + profile_block.strip() + "\n"
446
+ changed = True
447
+
448
+ encoded = base64.b64encode(new_content.encode()).decode()
449
+ tmp = f"/tmp/redeploy-kanshi-{step.id}.conf"
450
+ write_r = probe.run(
451
+ f"echo '{encoded}' | base64 -d | tee {tmp} > /dev/null && mv {tmp} {config_path}",
452
+ timeout=15,
453
+ )
454
+ if not write_r.ok:
455
+ raise StepError(step, f"Cannot write {config_path}: {write_r.stderr[:200]}")
456
+
457
+ # Reload kanshi if running
458
+ probe.run("pkill -SIGUSR1 kanshi 2>/dev/null || true", timeout=5)
459
+
460
+ step.status = StepStatus.DONE
461
+ step.result = f"profile '{profile_name}' written to {config_path}"
462
+
463
+
464
+ def run_ensure_autostart_entry(step: MigrationStep, probe: "RemoteProbe") -> None:
465
+ """Idempotently add or replace a keyed entry in a compositor autostart file.
466
+
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
472
+
473
+ if not step.config_file or not step.config_line:
474
+ raise StepError(step, "ensure_autostart_entry requires config_file and config_line")
475
+
476
+ key = step.config_section or "redeploy"
477
+ entry = AutostartEntry(key=key, line=step.config_line)
478
+
479
+ r = probe.run(f"cat {step.config_file} 2>/dev/null || echo ''", timeout=10)
480
+ existing = r.out if r.ok else ""
481
+
482
+ new_content, changed = ensure_autostart_entry(existing, entry)
483
+ if not changed:
484
+ step.status = StepStatus.DONE
485
+ step.result = f"no-op: entry [{key}] already correct"
486
+ return
487
+
488
+ mkdir_cmd = f"mkdir -p $(dirname {step.config_file})"
489
+ probe.run(mkdir_cmd, timeout=10)
490
+
491
+ encoded = base64.b64encode(new_content.encode()).decode()
492
+ tmp = f"/tmp/redeploy-autostart-{step.id}.txt"
493
+ write_r = probe.run(
494
+ f"echo '{encoded}' | base64 -d | tee {tmp} > /dev/null && mv {tmp} {step.config_file}",
495
+ timeout=15,
496
+ )
497
+ if not write_r.ok:
498
+ raise StepError(step, f"Cannot write {step.config_file}: {write_r.stderr[:200]}")
499
+
500
+ step.status = StepStatus.DONE
501
+ step.result = f"entry [{key}] written to {step.config_file}"
502
+
503
+
504
+ def run_ensure_browser_kiosk_script(step: MigrationStep, probe: "RemoteProbe") -> None:
505
+ """Write a kiosk-launch.sh script to the remote device.
506
+
507
+ step.command — the full script content to write
508
+ step.dst — destination path (default: ~/kiosk-launch.sh)
509
+ """
510
+ script_content = step.command
511
+ if not script_content:
512
+ raise StepError(step, "ensure_browser_kiosk_script requires step.command with script body")
513
+
514
+ dst = step.dst or "~/kiosk-launch.sh"
515
+
516
+ r = probe.run(f"cat {dst} 2>/dev/null || echo ''", timeout=10)
517
+ existing = r.out if r.ok else ""
518
+ if existing.strip() == script_content.strip():
519
+ step.status = StepStatus.DONE
520
+ step.result = f"no-op: {dst} already correct"
521
+ return
522
+
523
+ encoded = base64.b64encode(script_content.encode()).decode()
524
+ tmp = f"/tmp/redeploy-kiosk-{step.id}.sh"
525
+ write_r = probe.run(
526
+ f"echo '{encoded}' | base64 -d | tee {tmp} > /dev/null"
527
+ f" && chmod +x {tmp} && mv {tmp} {dst}",
528
+ timeout=15,
529
+ )
530
+ if not write_r.ok:
531
+ raise StepError(step, f"Cannot write {dst}: {write_r.stderr[:200]}")
532
+
533
+ step.status = StepStatus.DONE
534
+ step.result = f"kiosk script written to {dst}"