redeploy 0.2.42__tar.gz → 0.2.44__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 (181) hide show
  1. {redeploy-0.2.42 → redeploy-0.2.44}/PKG-INFO +8 -6
  2. {redeploy-0.2.42 → redeploy-0.2.44}/README.md +5 -5
  3. {redeploy-0.2.42 → redeploy-0.2.44}/pyproject.toml +2 -1
  4. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/__init__.py +1 -1
  5. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/apply/executor.py +4 -0
  6. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/apply/handlers.py +134 -0
  7. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/cli/commands/device_map.py +38 -3
  8. redeploy-0.2.44/redeploy/hardware/kiosk/__init__.py +18 -0
  9. redeploy-0.2.44/redeploy/hardware/kiosk/autostart.py +84 -0
  10. redeploy-0.2.44/redeploy/hardware/kiosk/browsers.py +78 -0
  11. redeploy-0.2.44/redeploy/hardware/kiosk/compositors.py +80 -0
  12. redeploy-0.2.44/redeploy/hardware/kiosk/output_profiles.py +68 -0
  13. redeploy-0.2.44/redeploy/integrations/__init__.py +1 -0
  14. redeploy-0.2.44/redeploy/integrations/op3_bridge.py +74 -0
  15. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/models.py +3 -0
  16. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy.egg-info/PKG-INFO +8 -6
  17. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy.egg-info/SOURCES.txt +8 -0
  18. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy.egg-info/requires.txt +3 -0
  19. redeploy-0.2.44/tests/test_hardware_kiosk.py +230 -0
  20. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/apply/__init__.py +0 -0
  21. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/apply/exceptions.py +0 -0
  22. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/apply/progress.py +0 -0
  23. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/apply/rollback.py +0 -0
  24. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/apply/state.py +0 -0
  25. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/apply/state_apply.py +0 -0
  26. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/apply/utils/__init__.py +0 -0
  27. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/apply/utils/run_container_build.py +0 -0
  28. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/audit.py +0 -0
  29. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/blueprint/__init__.py +0 -0
  30. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/blueprint/extractor.py +0 -0
  31. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/blueprint/generators/__init__.py +0 -0
  32. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/blueprint/generators/docker_compose.py +0 -0
  33. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/blueprint/generators/migration.py +0 -0
  34. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/cli/__init__.py +0 -0
  35. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/cli/commands/__init__.py +0 -0
  36. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/cli/commands/audit.py +0 -0
  37. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/cli/commands/blueprint.py +0 -0
  38. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/cli/commands/detect.py +0 -0
  39. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/cli/commands/devices.py +0 -0
  40. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/cli/commands/diagnose.py +0 -0
  41. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/cli/commands/diff.py +0 -0
  42. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/cli/commands/exec_.py +0 -0
  43. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/cli/commands/export.py +0 -0
  44. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/cli/commands/hardware.py +0 -0
  45. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/cli/commands/import_.py +0 -0
  46. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/cli/commands/init.py +0 -0
  47. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/cli/commands/inspect.py +0 -0
  48. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/cli/commands/patterns.py +0 -0
  49. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/cli/commands/plan_apply.py +0 -0
  50. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/cli/commands/plugin.py +0 -0
  51. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/cli/commands/probe.py +0 -0
  52. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/cli/commands/push.py +0 -0
  53. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/cli/commands/state.py +0 -0
  54. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/cli/commands/status.py +0 -0
  55. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/cli/commands/target.py +0 -0
  56. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/cli/commands/version/__init__.py +0 -0
  57. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/cli/commands/version/commands.py +0 -0
  58. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/cli/commands/version/helpers.py +0 -0
  59. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/cli/commands/version/monorepo.py +0 -0
  60. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/cli/commands/version/release.py +0 -0
  61. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/cli/commands/version/scanner.py +0 -0
  62. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/cli/commands/workflow.py +0 -0
  63. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/cli/core.py +0 -0
  64. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/cli/display.py +0 -0
  65. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/cli.py +0 -0
  66. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/data_sync.py +0 -0
  67. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/detect/__init__.py +0 -0
  68. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/detect/builtin/__init__.py +0 -0
  69. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/detect/detector.py +0 -0
  70. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/detect/hardware.py +0 -0
  71. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/detect/hardware_rules.py +0 -0
  72. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/detect/probes.py +0 -0
  73. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/detect/remote.py +0 -0
  74. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/detect/templates.py +0 -0
  75. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/detect/workflow.py +0 -0
  76. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/discovery.py +0 -0
  77. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/dsl/__init__.py +0 -0
  78. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/dsl/loader.py +0 -0
  79. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/dsl/parser.py +0 -0
  80. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/dsl_python/__init__.py +0 -0
  81. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/dsl_python/context.py +0 -0
  82. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/dsl_python/decorators.py +0 -0
  83. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/dsl_python/docker_steps.py +0 -0
  84. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/dsl_python/exceptions.py +0 -0
  85. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/dsl_python/runner.py +0 -0
  86. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/dsl_python/steps.py +0 -0
  87. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/fleet.py +0 -0
  88. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/hardware/__init__.py +0 -0
  89. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/hardware/config_txt.py +0 -0
  90. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/hardware/data/__init__.py +0 -0
  91. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/hardware/data/hyperpixel.py +0 -0
  92. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/hardware/data/official.py +0 -0
  93. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/hardware/data/waveshare.py +0 -0
  94. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/hardware/fixes.py +0 -0
  95. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/hardware/panels.py +0 -0
  96. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/hardware/raspi_config.py +0 -0
  97. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/iac/__init__.py +0 -0
  98. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/iac/base.py +0 -0
  99. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/iac/docker_compose.py +0 -0
  100. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/iac/parsers/__init__.py +0 -0
  101. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/iac/parsers/compose.py +0 -0
  102. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/iac/registry.py +0 -0
  103. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/markpact/__init__.py +0 -0
  104. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/markpact/compiler.py +0 -0
  105. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/markpact/models.py +0 -0
  106. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/markpact/parser.py +0 -0
  107. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/observe.py +0 -0
  108. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/parse.py +0 -0
  109. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/patterns.py +0 -0
  110. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/plan/__init__.py +0 -0
  111. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/plan/planner.py +0 -0
  112. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/plugins/__init__.py +0 -0
  113. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/plugins/builtin/__init__.py +0 -0
  114. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/plugins/builtin/browser_reload.py +0 -0
  115. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/plugins/builtin/hardware_diagnostic.py +0 -0
  116. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/plugins/builtin/notify.py +0 -0
  117. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/plugins/builtin/process_control.py +0 -0
  118. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/plugins/builtin/systemd_reload.py +0 -0
  119. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/spec_loader.py +0 -0
  120. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/ssh.py +0 -0
  121. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/steps.py +0 -0
  122. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/tests/__init__.py +0 -0
  123. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/tests/test_audit.py +0 -0
  124. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/tests/test_cli.py +0 -0
  125. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/tests/test_data_sync.py +0 -0
  126. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/tests/test_discovery.py +0 -0
  127. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/tests/test_dsl.py +0 -0
  128. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/tests/test_examples.py +0 -0
  129. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/tests/test_executor.py +0 -0
  130. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/tests/test_fleet.py +0 -0
  131. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/tests/test_git_integration.py +0 -0
  132. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/tests/test_iac.py +0 -0
  133. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/tests/test_manifest.py +0 -0
  134. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/tests/test_markpact_compiler.py +0 -0
  135. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/tests/test_markpact_parser.py +0 -0
  136. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/tests/test_models.py +0 -0
  137. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/tests/test_observe.py +0 -0
  138. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/tests/test_parse.py +0 -0
  139. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/tests/test_patterns.py +0 -0
  140. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/tests/test_planner_edge.py +0 -0
  141. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/tests/test_plugins.py +0 -0
  142. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/tests/test_probes.py +0 -0
  143. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/tests/test_public_api.py +0 -0
  144. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/tests/test_resume.py +0 -0
  145. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/tests/test_spec_loader.py +0 -0
  146. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/tests/test_ssh.py +0 -0
  147. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/tests/test_templates.py +0 -0
  148. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/tests/test_verify.py +0 -0
  149. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/tests/test_version.py +0 -0
  150. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/tests/test_version_changelog.py +0 -0
  151. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/tests/test_version_cli.py +0 -0
  152. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/tests/test_version_manifest.py +0 -0
  153. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/tests/test_version_sources.py +0 -0
  154. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/tests/test_workflow.py +0 -0
  155. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/verify.py +0 -0
  156. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/version/__init__.py +0 -0
  157. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/version/bump.py +0 -0
  158. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/version/changelog.py +0 -0
  159. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/version/commits.py +0 -0
  160. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/version/diff.py +0 -0
  161. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/version/git_integration.py +0 -0
  162. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/version/git_transaction.py +0 -0
  163. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/version/manifest.py +0 -0
  164. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/version/sources/__init__.py +0 -0
  165. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/version/sources/base.py +0 -0
  166. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/version/sources/json_.py +0 -0
  167. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/version/sources/plain.py +0 -0
  168. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/version/sources/regex.py +0 -0
  169. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/version/sources/toml_.py +0 -0
  170. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/version/sources/yaml_.py +0 -0
  171. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/version/transaction.py +0 -0
  172. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy/version.py +0 -0
  173. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy.egg-info/dependency_links.txt +0 -0
  174. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy.egg-info/entry_points.txt +0 -0
  175. {redeploy-0.2.42 → redeploy-0.2.44}/redeploy.egg-info/top_level.txt +0 -0
  176. {redeploy-0.2.42 → redeploy-0.2.44}/setup.cfg +0 -0
  177. {redeploy-0.2.42 → redeploy-0.2.44}/tests/test_hardware_config.py +0 -0
  178. {redeploy-0.2.42 → redeploy-0.2.44}/tests/test_hardware_rules.py +0 -0
  179. {redeploy-0.2.42 → redeploy-0.2.44}/tests/test_iac.py +0 -0
  180. {redeploy-0.2.42 → redeploy-0.2.44}/tests/test_parse.py +0 -0
  181. {redeploy-0.2.42 → redeploy-0.2.44}/tests/test_redeploy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redeploy
3
- Version: 0.2.42
3
+ Version: 0.2.44
4
4
  Summary: Infrastructure migration toolkit: detect → plan → apply
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -22,23 +22,25 @@ Requires-Dist: ruff>=0.4; extra == "dev"
22
22
  Requires-Dist: goal>=2.1.0; extra == "dev"
23
23
  Requires-Dist: costs>=0.1.20; extra == "dev"
24
24
  Requires-Dist: pfix>=0.1.60; extra == "dev"
25
+ Provides-Extra: op3
26
+ Requires-Dist: op3>=0.1.4; extra == "op3"
25
27
 
26
28
  # redeploy
27
29
 
28
30
 
29
31
  ## AI Cost Tracking
30
32
 
31
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.42-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-20.9h-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.44-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
34
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-21.4h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
33
35
 
34
- - 🤖 **LLM usage:** $7.5000 (63 commits)
35
- - 👤 **Human dev:** ~$2088 (20.9h @ $100/h, 30min dedup)
36
+ - 🤖 **LLM usage:** $7.5000 (65 commits)
37
+ - 👤 **Human dev:** ~$2136 (21.4h @ $100/h, 30min dedup)
36
38
 
37
39
  Generated on 2026-04-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
38
40
 
39
41
  ---
40
42
 
41
- ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.42-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.44-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
42
44
 
43
45
  Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
44
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.42-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.9h-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.44-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-21.4h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $7.5000 (63 commits)
10
- - 👤 **Human dev:** ~$2088 (20.9h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $7.5000 (65 commits)
10
+ - 👤 **Human dev:** ~$2136 (21.4h @ $100/h, 30min dedup)
11
11
 
12
12
  Generated on 2026-04-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
13
13
 
14
14
  ---
15
15
 
16
- ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.42-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.44-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.42"
7
+ version = "0.2.44"
8
8
  description = "Infrastructure migration toolkit: detect → plan → apply"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -32,6 +32,7 @@ dev = ["pytest>=8.0", "ruff>=0.4",
32
32
  "costs>=0.1.20",
33
33
  "pfix>=0.1.60",
34
34
  ]
35
+ op3 = ["op3>=0.1.4"]
35
36
 
36
37
  [tool.ruff]
37
38
  line-length = 100
@@ -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.42"
10
+ __version__ = "0.2.44"
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}"
@@ -50,10 +50,16 @@ from rich.table import Table
50
50
  @click.option("--apply-config", "apply_config", default=None,
51
51
  type=click.Path(exists=True, dir_okay=False),
52
52
  help="Apply hardware/infra settings from YAML config file to the remote host")
53
+ @click.option(
54
+ "--query", "query_expr",
55
+ default=None,
56
+ metavar="EXPR",
57
+ help="Extract specific values using JMESPath query (e.g. 'hardware.drm_outputs[0].transform', 'host')",
58
+ )
53
59
  @click.option("--ssh-key", default=None, type=click.Path(), help="SSH private key path")
54
60
  def device_map_cmd(
55
61
  host, name, tags, save, out_path, output_fmt,
56
- no_infra, list_saved, show_file, diff_files, ssh_key, apply_config,
62
+ no_infra, list_saved, show_file, diff_files, ssh_key, apply_config, query_expr,
57
63
  ):
58
64
  """Generate a full standardized device snapshot (hardware + infra + diagnostics).
59
65
 
@@ -105,6 +111,11 @@ def device_map_cmd(
105
111
  console.print("[red]✗ HOST required (or use --list / --show / --diff)[/red]")
106
112
  sys.exit(1)
107
113
 
114
+ # ── --apply-config ─────────────────────────────────────────────────────────
115
+ if apply_config:
116
+ _apply_config_from_file(console, apply_config, ssh_key)
117
+ return
118
+
108
119
  # ── probe ─────────────────────────────────────────────────────────────────
109
120
  from ...detect.hardware import probe_hardware
110
121
  from ...detect.remote import RemoteProbe
@@ -195,7 +206,7 @@ def _apply_config_from_file(console, config_path, ssh_key):
195
206
  import yaml
196
207
  import json as _json
197
208
 
198
- from ...remote import Remote
209
+ from ...detect.remote import RemoteProbe
199
210
 
200
211
  with open(config_path) as f:
201
212
  raw = f.read()
@@ -214,7 +225,7 @@ def _apply_config_from_file(console, config_path, ssh_key):
214
225
  console.print(f"[cyan]→ Applying config from {config_path} to {host}[/cyan]")
215
226
 
216
227
  # Connect to remote
217
- p = Remote(host, ssh_key=ssh_key)
228
+ p = RemoteProbe(host, ssh_key=ssh_key)
218
229
 
219
230
  applied = 0
220
231
 
@@ -321,6 +332,30 @@ def _update_kanshi_from_cfg(console, p, dsi_outputs: list):
321
332
  p.run("pkill -SIGUSR1 kanshi 2>/dev/null || true")
322
333
 
323
334
 
335
+ def _execute_query_device_map(console, dm, query_expr, output_fmt):
336
+ """Execute JMESPath query on DeviceMap model and output result."""
337
+ import jmespath
338
+ import json as _json
339
+
340
+ data = dm.model_dump(mode="json")
341
+
342
+ try:
343
+ result = jmespath.search(query_expr, data)
344
+ except jmespath.exceptions.JMESPathError as e:
345
+ console.print(f"[red]✗ JMESPath error:[/red] {e}")
346
+ sys.exit(1)
347
+
348
+ if result is None:
349
+ console.print("[dim]No match found for query[/dim]")
350
+ return
351
+
352
+ if output_fmt == "json":
353
+ click.echo(_json.dumps(result, indent=2, default=str))
354
+ else:
355
+ import yaml
356
+ click.echo(yaml.safe_dump(result, sort_keys=False, default_flow_style=False))
357
+
358
+
324
359
  # ── rendering helpers ─────────────────────────────────────────────────────────
325
360
 
326
361
 
@@ -0,0 +1,18 @@
1
+ """Kiosk display configuration helpers — encoding pi109 session knowledge."""
2
+
3
+ from .autostart import AutostartEntry, ensure_autostart_entry, generate_labwc_autostart
4
+ from .browsers import BrowserKioskProfile, CHROMIUM_WAYLAND_KIOSK
5
+ from .compositors import CompositorDefinition, LABWC
6
+ from .output_profiles import OutputProfile, dsi_only_profile
7
+
8
+ __all__ = [
9
+ "AutostartEntry",
10
+ "ensure_autostart_entry",
11
+ "generate_labwc_autostart",
12
+ "BrowserKioskProfile",
13
+ "CHROMIUM_WAYLAND_KIOSK",
14
+ "CompositorDefinition",
15
+ "LABWC",
16
+ "OutputProfile",
17
+ "dsi_only_profile",
18
+ ]
@@ -0,0 +1,84 @@
1
+ """Autostart file management for kiosk compositors.
2
+
3
+ Encodes the pi109 lesson: labwc autostart order is critical.
4
+ 1. kanshid & — start output manager
5
+ 2. sleep 3 — wait for kanshi to disable HDMI-A-2
6
+ 3. bash ~/kiosk-launch.sh & — start browser AFTER output is configured
7
+
8
+ If Chromium starts before kanshi applies the DSI-only profile, it may render
9
+ on HDMI-A-2 (which would be blank since no monitor is connected).
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ from dataclasses import dataclass
15
+
16
+
17
+ @dataclass
18
+ class AutostartEntry:
19
+ """One entry in a compositor autostart file."""
20
+
21
+ key: str
22
+ """Unique identifier used for idempotent replacement, e.g. 'kiosk-browser'."""
23
+
24
+ line: str
25
+ """The shell line to write, e.g. 'bash ~/kiosk-launch.sh &'."""
26
+
27
+ comment: str = ""
28
+ """Optional inline comment explaining the entry."""
29
+
30
+ def render(self) -> str:
31
+ """Render as a single autostart file line."""
32
+ if self.comment:
33
+ return f"{self.line} # [{self.key}] {self.comment}"
34
+ return f"{self.line} # [{self.key}]"
35
+
36
+
37
+ def ensure_autostart_entry(content: str, entry: AutostartEntry) -> tuple[str, bool]:
38
+ """Idempotently add or replace an entry in an autostart file.
39
+
40
+ Looks for a line containing ``[key]`` marker. If found, replaces it.
41
+ If not, appends it. Returns (new_content, changed).
42
+ """
43
+ marker = f"[{entry.key}]"
44
+ rendered = entry.render()
45
+ lines = content.splitlines(keepends=True)
46
+ for i, line in enumerate(lines):
47
+ if marker in line:
48
+ if line.rstrip("\n") == rendered:
49
+ return content, False # already correct
50
+ lines[i] = rendered + "\n"
51
+ return "".join(lines), True
52
+ # Not found — append
53
+ sep = "\n" if content and not content.endswith("\n") else ""
54
+ return content + sep + rendered + "\n", True
55
+
56
+
57
+ def generate_labwc_autostart(
58
+ kiosk_script: str = "~/kiosk-launch.sh",
59
+ kanshi_settle_secs: int = 3,
60
+ extra_entries: list[AutostartEntry] | None = None,
61
+ ) -> str:
62
+ """Generate a complete labwc autostart file for a kiosk deployment.
63
+
64
+ Encodes the pi109 ordering: kanshi → sleep → browser.
65
+
66
+ Args:
67
+ kiosk_script: Path to the kiosk launch shell script.
68
+ kanshi_settle_secs: Seconds to wait after starting kanshi.
69
+ Pi109 used sleep 3 for the Waveshare 8" panel.
70
+ extra_entries: Additional autostart entries appended at the end.
71
+ """
72
+ lines = [
73
+ "# Generated by redeploy hardware/kiosk — do not edit manually",
74
+ "# Pi109 ordering: kanshi first, then browser after settle time",
75
+ "",
76
+ "kanshid & # [kanshi] output manager (disables HDMI before browser starts)",
77
+ f"sleep {kanshi_settle_secs} # [kanshi-settle] wait for kanshi DSI-only profile",
78
+ f"bash {kiosk_script} & # [kiosk-browser] launch browser in kiosk mode",
79
+ ]
80
+ if extra_entries:
81
+ lines.append("")
82
+ for e in extra_entries:
83
+ lines.append(e.render())
84
+ return "\n".join(lines) + "\n"
@@ -0,0 +1,78 @@
1
+ """Browser kiosk profiles for Wayland kiosk deployments.
2
+
3
+ Encodes browser flag knowledge from pi109 debugging session.
4
+
5
+ Pi109 critical lessons:
6
+ - GNOME Keyring intercepts first Chromium launch without --password-store=basic
7
+ causing a blocking dialog that freezes the kiosk silently on RPi OS.
8
+ - --windowed is incompatible with --kiosk under labwc; it must be removed.
9
+ - --noerrdialogs prevents crash dialogs on recoverable GPU errors.
10
+ - --disable-infobars removes the "Chrome is being controlled" banner.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass, field
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class BrowserKioskProfile:
19
+ """Static definition of a browser kiosk launch profile."""
20
+
21
+ id: str
22
+ """Short identifier, e.g. 'chromium-wayland'."""
23
+
24
+ binary: str
25
+ """Binary name as found in PATH, e.g. 'chromium-browser'."""
26
+
27
+ required_flags: list[str] = field(default_factory=list)
28
+ """Flags that MUST be present for a functional kiosk session."""
29
+
30
+ incompatible_flags: list[str] = field(default_factory=list)
31
+ """Flags that break kiosk mode; raise if detected in a spec."""
32
+
33
+ optional_flags: list[str] = field(default_factory=list)
34
+ """Recommended but not strictly required."""
35
+
36
+ notes: list[str] = field(default_factory=list)
37
+ """Human-readable caveats from real debugging."""
38
+
39
+ def build_launch_cmd(self, url: str, extra_flags: list[str] | None = None) -> str:
40
+ """Build the Chromium launch command string for a kiosk-launch.sh."""
41
+ flags = list(self.required_flags)
42
+ flags += extra_flags or []
43
+ bad = [f for f in flags if f in self.incompatible_flags]
44
+ if bad:
45
+ raise ValueError(
46
+ f"Incompatible flags detected for {self.id!r}: {bad}\n"
47
+ f"Remove them: {self.incompatible_flags}"
48
+ )
49
+ return f"{self.binary} {' '.join(flags)} {url}"
50
+
51
+
52
+ CHROMIUM_WAYLAND_KIOSK = BrowserKioskProfile(
53
+ id="chromium-wayland",
54
+ binary="chromium-browser",
55
+ required_flags=[
56
+ "--kiosk",
57
+ "--password-store=basic", # Pi109: GNOME Keyring blocks without this
58
+ "--noerrdialogs",
59
+ "--disable-infobars",
60
+ "--disable-session-crashed-bubble",
61
+ "--disable-features=TranslateUI",
62
+ "--ozone-platform=wayland", # explicit; prevents X11 fallback
63
+ "--enable-features=UseOzonePlatform",
64
+ ],
65
+ incompatible_flags=[
66
+ "--windowed", # Pi109: conflicts with --kiosk under labwc
67
+ "--start-maximized", # overrides kiosk geometry
68
+ ],
69
+ optional_flags=[
70
+ "--check-for-update-interval=1", # disable auto-update check noise
71
+ "--incognito", # stateless sessions
72
+ ],
73
+ notes=[
74
+ "Pi109: --password-store=basic is MANDATORY on RPi OS with GNOME Keyring",
75
+ "--windowed + --kiosk causes transparent/non-fullscreen window under labwc",
76
+ "Set DISPLAY unset; XDG_SESSION_TYPE=wayland must be exported before launch",
77
+ ],
78
+ )
@@ -0,0 +1,80 @@
1
+ """Compositor definitions for kiosk deployments.
2
+
3
+ Encodes compositor-specific knowledge: where autostart lives, which output
4
+ manager is used, what environment variables are required.
5
+
6
+ Pi109 lesson: labwc uses kanshi for output management. The compositor must be
7
+ started BEFORE kanshi, which must settle BEFORE Chromium starts.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass, field
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class CompositorDefinition:
16
+ """Static definition of a Wayland compositor for kiosk use."""
17
+
18
+ id: str
19
+ """Short identifier, e.g. 'labwc'."""
20
+
21
+ autostart_path: str
22
+ """Path to the autostart file (relative to $HOME or absolute)."""
23
+
24
+ output_manager: str
25
+ """Tool used to manage outputs: 'kanshi', 'wlr-randr', 'none'."""
26
+
27
+ required_packages: list[str] = field(default_factory=list)
28
+ """Packages that must be present on the target device."""
29
+
30
+ required_env: list[str] = field(default_factory=list)
31
+ """Environment variables a kiosk launcher script must export."""
32
+
33
+ notes: list[str] = field(default_factory=list)
34
+ """Human-readable caveats encoded from real debugging sessions."""
35
+
36
+ def autostart_abs(self, home: str = "/home/pi") -> str:
37
+ path = self.autostart_path
38
+ if path.startswith("~"):
39
+ path = home + path[1:]
40
+ return path
41
+
42
+
43
+ LABWC = CompositorDefinition(
44
+ id="labwc",
45
+ autostart_path="~/.config/labwc/autostart",
46
+ output_manager="kanshi",
47
+ required_packages=["labwc", "kanshi", "wlr-randr"],
48
+ required_env=["WAYLAND_DISPLAY", "XDG_RUNTIME_DIR", "XDG_SESSION_TYPE"],
49
+ notes=[
50
+ # Pi109 session knowledge:
51
+ "kanshi must be started BEFORE Chromium (it disables HDMI-A-2)",
52
+ "sleep 3 between kanshid and browser start allows kanshi to apply profile",
53
+ "labwc autostart order: kanshid & → sleep 3 → bash ~/kiosk-launch.sh &",
54
+ "--windowed flag conflicts with --kiosk under labwc; remove it",
55
+ "XDG_SESSION_TYPE=wayland must be set or Chromium falls back to X11",
56
+ ],
57
+ )
58
+
59
+ WESTON = CompositorDefinition(
60
+ id="weston",
61
+ autostart_path="~/.config/weston.ini",
62
+ output_manager="weston",
63
+ required_packages=["weston"],
64
+ required_env=["WAYLAND_DISPLAY", "XDG_RUNTIME_DIR"],
65
+ notes=[],
66
+ )
67
+
68
+ SWAY = CompositorDefinition(
69
+ id="sway",
70
+ autostart_path="~/.config/sway/config",
71
+ output_manager="sway",
72
+ required_packages=["sway", "swaybar"],
73
+ required_env=["WAYLAND_DISPLAY", "XDG_RUNTIME_DIR", "XDG_SESSION_TYPE"],
74
+ notes=[],
75
+ )
76
+
77
+ #: registry: compositor id → definition
78
+ COMPOSITORS: dict[str, CompositorDefinition] = {
79
+ c.id: c for c in [LABWC, WESTON, SWAY]
80
+ }
@@ -0,0 +1,68 @@
1
+ """Kanshi output profile generation for kiosk deployments.
2
+
3
+ Encodes the pi109 lesson: HDMI outputs must be explicitly disabled in the
4
+ kanshi profile so the DSI panel is the only active output BEFORE Chromium is
5
+ launched. If HDMI-A-2 is enabled, Chromium may open on it instead of DSI-2.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+
11
+
12
+ @dataclass
13
+ class OutputProfile:
14
+ """A kanshi output profile definition."""
15
+
16
+ name: str
17
+ """Profile name used in kanshi config, e.g. 'waveshare-only'."""
18
+
19
+ enabled: list[str] = field(default_factory=list)
20
+ """Connector names to enable, e.g. ['DSI-2']."""
21
+
22
+ disabled: list[str] = field(default_factory=list)
23
+ """Connector names to explicitly disable, e.g. ['HDMI-A-2']."""
24
+
25
+ transforms: dict[str, str] = field(default_factory=dict)
26
+ """Optional per-output transform, e.g. {'DSI-2': '90'}."""
27
+
28
+ modes: dict[str, str] = field(default_factory=dict)
29
+ """Optional per-output mode override, e.g. {'DSI-2': '800x480'}."""
30
+
31
+ def to_kanshi_config(self) -> str:
32
+ """Render the kanshi profile block as a string."""
33
+ lines = [f"profile {self.name} {{"]
34
+ for conn in self.enabled:
35
+ parts = [f" output {conn} enable"]
36
+ if conn in self.modes:
37
+ parts.append(f"mode {self.modes[conn]}")
38
+ if conn in self.transforms:
39
+ parts.append(f"transform {self.transforms[conn]}")
40
+ lines.append(" ".join(parts))
41
+ for conn in self.disabled:
42
+ lines.append(f" output {conn} disable")
43
+ lines.append("}")
44
+ return "\n".join(lines)
45
+
46
+
47
+ def dsi_only_profile(
48
+ dsi_connector: str = "DSI-2",
49
+ hdmi_connectors: list[str] | None = None,
50
+ profile_name: str = "waveshare-only",
51
+ transform: str | None = None,
52
+ ) -> OutputProfile:
53
+ """Factory: DSI panel enabled, all HDMI outputs disabled.
54
+
55
+ Pi109 knowledge: RPi 5 exposes HDMI-A-1 and HDMI-A-2. Both must be
56
+ disabled in kanshi so Chromium opens on DSI-2.
57
+ """
58
+ if hdmi_connectors is None:
59
+ hdmi_connectors = ["HDMI-A-1", "HDMI-A-2"]
60
+ transforms = {}
61
+ if transform:
62
+ transforms[dsi_connector] = transform
63
+ return OutputProfile(
64
+ name=profile_name,
65
+ enabled=[dsi_connector],
66
+ disabled=hdmi_connectors,
67
+ transforms=transforms,
68
+ )
@@ -0,0 +1 @@
1
+ """Redeploy integrations."""