redeploy 0.2.69__tar.gz → 0.2.70__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 (255) hide show
  1. {redeploy-0.2.69 → redeploy-0.2.70}/PKG-INFO +45 -6
  2. {redeploy-0.2.69 → redeploy-0.2.70}/README.md +44 -5
  3. {redeploy-0.2.69 → redeploy-0.2.70}/pyproject.toml +1 -1
  4. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/__init__.py +1 -1
  5. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/__init__.py +2 -0
  6. redeploy-0.2.70/redeploy/cli/commands/gh_workflow.py +291 -0
  7. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/hardware.py +2 -68
  8. redeploy-0.2.70/redeploy/config_apply/handlers/__init__.py +10 -0
  9. redeploy-0.2.70/redeploy/config_apply/handlers/display.py +99 -0
  10. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy.egg-info/PKG-INFO +45 -6
  11. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy.egg-info/SOURCES.txt +4 -0
  12. redeploy-0.2.70/tests/test_cli_gh_workflow.py +132 -0
  13. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/analyze/__init__.py +0 -0
  14. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/analyze/checkers/__init__.py +0 -0
  15. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/analyze/preflight_schema.py +0 -0
  16. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/analyze/spec_analyzer.py +0 -0
  17. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/apply/__init__.py +0 -0
  18. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/apply/exceptions.py +0 -0
  19. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/apply/executor.py +0 -0
  20. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/apply/handlers.py +0 -0
  21. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/apply/progress.py +0 -0
  22. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/apply/rollback.py +0 -0
  23. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/apply/state.py +0 -0
  24. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/apply/state_apply.py +0 -0
  25. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/apply/utils/__init__.py +0 -0
  26. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/apply/utils/run_container_build.py +0 -0
  27. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/audit.py +0 -0
  28. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/blueprint/__init__.py +0 -0
  29. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/blueprint/extractor.py +0 -0
  30. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/blueprint/generators/__init__.py +0 -0
  31. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/blueprint/generators/docker_compose.py +0 -0
  32. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/blueprint/generators/migration.py +0 -0
  33. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/blueprint/sources/__init__.py +0 -0
  34. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/blueprint/sources/compose.py +0 -0
  35. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/blueprint/sources/hardware.py +0 -0
  36. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/blueprint/sources/infra.py +0 -0
  37. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/blueprint/sources/migration.py +0 -0
  38. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/__init__.py +0 -0
  39. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/audit.py +0 -0
  40. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/blueprint.py +0 -0
  41. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/bump_fix.py +0 -0
  42. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/detect.py +0 -0
  43. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/device_map.py +0 -0
  44. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/device_map_renderers.py +0 -0
  45. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/devices.py +0 -0
  46. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/diagnose.py +0 -0
  47. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/diff.py +0 -0
  48. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/exec_.py +0 -0
  49. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/export.py +0 -0
  50. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/import_.py +0 -0
  51. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/init.py +0 -0
  52. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/inspect.py +0 -0
  53. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/lint.py +0 -0
  54. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/mcp_cmd.py +0 -0
  55. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/patterns.py +0 -0
  56. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/plan_apply.py +0 -0
  57. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/plugin.py +0 -0
  58. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/probe.py +0 -0
  59. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/prompt_cmd.py +0 -0
  60. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/push.py +0 -0
  61. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/state.py +0 -0
  62. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/status.py +0 -0
  63. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/target.py +0 -0
  64. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/version/__init__.py +0 -0
  65. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/version/commands.py +0 -0
  66. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/version/helpers.py +0 -0
  67. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/version/monorepo.py +0 -0
  68. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/version/release.py +0 -0
  69. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/version/scanner.py +0 -0
  70. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/version/utils/__init__.py +0 -0
  71. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/version/utils/changelog_config.py +0 -0
  72. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/version/utils/git_config.py +0 -0
  73. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/commands/workflow.py +0 -0
  74. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/core.py +0 -0
  75. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/display.py +0 -0
  76. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli/query.py +0 -0
  77. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/cli.py +0 -0
  78. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/config_apply/__init__.py +0 -0
  79. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/config_apply/applier.py +0 -0
  80. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/config_apply/loader.py +0 -0
  81. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/data_sync.py +0 -0
  82. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/detect/__init__.py +0 -0
  83. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/detect/builtin/__init__.py +0 -0
  84. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/detect/detector.py +0 -0
  85. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/detect/hardware.py +0 -0
  86. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/detect/hardware_rules.py +0 -0
  87. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/detect/probes.py +0 -0
  88. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/detect/remote.py +0 -0
  89. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/detect/templates.py +0 -0
  90. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/detect/workflow.py +0 -0
  91. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/discovery.py +0 -0
  92. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/dsl/__init__.py +0 -0
  93. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/dsl/loader.py +0 -0
  94. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/dsl/parser.py +0 -0
  95. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/dsl_python/__init__.py +0 -0
  96. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/dsl_python/context.py +0 -0
  97. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/dsl_python/decorators.py +0 -0
  98. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/dsl_python/docker_steps.py +0 -0
  99. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/dsl_python/exceptions.py +0 -0
  100. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/dsl_python/runner.py +0 -0
  101. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/dsl_python/steps.py +0 -0
  102. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/fleet.py +0 -0
  103. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/hardware/__init__.py +0 -0
  104. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/hardware/config_txt.py +0 -0
  105. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/hardware/data/__init__.py +0 -0
  106. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/hardware/data/hyperpixel.py +0 -0
  107. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/hardware/data/official.py +0 -0
  108. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/hardware/data/waveshare.py +0 -0
  109. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/hardware/fixes.py +0 -0
  110. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/hardware/kiosk/__init__.py +0 -0
  111. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/hardware/kiosk/autostart.py +0 -0
  112. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/hardware/kiosk/browsers.py +0 -0
  113. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/hardware/kiosk/compositors.py +0 -0
  114. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/hardware/kiosk/output_profiles.py +0 -0
  115. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/hardware/panels.py +0 -0
  116. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/hardware/raspi_config.py +0 -0
  117. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/heal/__init__.py +0 -0
  118. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/heal/decider.py +0 -0
  119. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/heal/hint_provider.py +0 -0
  120. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/heal/log_writer.py +0 -0
  121. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/heal/loop_detector.py +0 -0
  122. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/heal/runner.py +0 -0
  123. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/heal.py +0 -0
  124. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/iac/__init__.py +0 -0
  125. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/iac/base.py +0 -0
  126. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/iac/config_hints.py +0 -0
  127. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/iac/docker_compose.py +0 -0
  128. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/iac/parsers/__init__.py +0 -0
  129. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/iac/parsers/compose.py +0 -0
  130. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/iac/registry.py +0 -0
  131. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/integrations/__init__.py +0 -0
  132. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/integrations/op3_bridge.py +0 -0
  133. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/markpact/__init__.py +0 -0
  134. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/markpact/compiler.py +0 -0
  135. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/markpact/models.py +0 -0
  136. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/markpact/parser.py +0 -0
  137. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/mcp_server.py +0 -0
  138. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/models/__init__.py +0 -0
  139. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/models/blueprint.py +0 -0
  140. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/models/devices.py +0 -0
  141. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/models/enums.py +0 -0
  142. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/models/hardware.py +0 -0
  143. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/models/infra.py +0 -0
  144. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/models/manifest.py +0 -0
  145. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/models/persisted.py +0 -0
  146. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/models/pipeline.py +0 -0
  147. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/models/plan.py +0 -0
  148. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/models/spec.py +0 -0
  149. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/observe.py +0 -0
  150. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/parse.py +0 -0
  151. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/patterns.py +0 -0
  152. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/plan/__init__.py +0 -0
  153. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/plan/planner.py +0 -0
  154. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/plugins/__init__.py +0 -0
  155. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/plugins/builtin/__init__.py +0 -0
  156. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/plugins/builtin/browser_reload.py +0 -0
  157. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/plugins/builtin/hardware_diagnostic.py +0 -0
  158. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/plugins/builtin/notify.py +0 -0
  159. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/plugins/builtin/process_control.py +0 -0
  160. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/plugins/builtin/systemd_reload.py +0 -0
  161. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/schema.py +0 -0
  162. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/spec_loader.py +0 -0
  163. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/ssh.py +0 -0
  164. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/steps/__init__.py +0 -0
  165. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/steps/docker.py +0 -0
  166. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/steps/generic.py +0 -0
  167. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/steps/hardware.py +0 -0
  168. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/steps/k3s.py +0 -0
  169. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/steps/kiosk.py +0 -0
  170. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/steps/podman.py +0 -0
  171. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/steps/process.py +0 -0
  172. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/steps/scm.py +0 -0
  173. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/steps/transfer.py +0 -0
  174. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/__init__.py +0 -0
  175. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_analyze.py +0 -0
  176. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_audit.py +0 -0
  177. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_cli.py +0 -0
  178. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_data_sync.py +0 -0
  179. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_discovery.py +0 -0
  180. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_dsl.py +0 -0
  181. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_examples.py +0 -0
  182. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_executor.py +0 -0
  183. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_fleet.py +0 -0
  184. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_git_integration.py +0 -0
  185. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_heal.py +0 -0
  186. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_heal_decider.py +0 -0
  187. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_hooks.py +0 -0
  188. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_iac.py +0 -0
  189. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_iac_compose_contract.py +0 -0
  190. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_iac_docker_compose_contract.py +0 -0
  191. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_manifest.py +0 -0
  192. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_markpact_compiler.py +0 -0
  193. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_markpact_parser.py +0 -0
  194. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_models.py +0 -0
  195. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_observe.py +0 -0
  196. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_parse.py +0 -0
  197. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_patterns.py +0 -0
  198. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_planner_edge.py +0 -0
  199. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_plugins.py +0 -0
  200. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_probes.py +0 -0
  201. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_public_api.py +0 -0
  202. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_resume.py +0 -0
  203. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_spec_loader.py +0 -0
  204. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_ssh.py +0 -0
  205. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_steps_registry.py +0 -0
  206. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_templates.py +0 -0
  207. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_verify.py +0 -0
  208. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_version.py +0 -0
  209. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_version_changelog.py +0 -0
  210. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_version_cli.py +0 -0
  211. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_version_manifest.py +0 -0
  212. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_version_sources.py +0 -0
  213. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/tests/test_workflow.py +0 -0
  214. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/verify.py +0 -0
  215. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/version/__init__.py +0 -0
  216. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/version/bump.py +0 -0
  217. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/version/changelog.py +0 -0
  218. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/version/commits.py +0 -0
  219. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/version/diff.py +0 -0
  220. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/version/git_integration.py +0 -0
  221. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/version/git_transaction.py +0 -0
  222. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/version/manifest.py +0 -0
  223. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/version/sources/__init__.py +0 -0
  224. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/version/sources/base.py +0 -0
  225. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/version/sources/json_.py +0 -0
  226. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/version/sources/plain.py +0 -0
  227. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/version/sources/regex.py +0 -0
  228. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/version/sources/toml_.py +0 -0
  229. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/version/sources/yaml_.py +0 -0
  230. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/version/transaction.py +0 -0
  231. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy/version.py +0 -0
  232. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy.egg-info/dependency_links.txt +0 -0
  233. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy.egg-info/entry_points.txt +0 -0
  234. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy.egg-info/requires.txt +0 -0
  235. {redeploy-0.2.69 → redeploy-0.2.70}/redeploy.egg-info/top_level.txt +0 -0
  236. {redeploy-0.2.69 → redeploy-0.2.70}/setup.cfg +0 -0
  237. {redeploy-0.2.69 → redeploy-0.2.70}/tests/test_blueprint_sources.py +0 -0
  238. {redeploy-0.2.69 → redeploy-0.2.70}/tests/test_blueprint_sources_compose.py +0 -0
  239. {redeploy-0.2.69 → redeploy-0.2.70}/tests/test_cli_query.py +0 -0
  240. {redeploy-0.2.69 → redeploy-0.2.70}/tests/test_example_argocd_flux_plugin.py +0 -0
  241. {redeploy-0.2.69 → redeploy-0.2.70}/tests/test_example_gitops_ci_plugin.py +0 -0
  242. {redeploy-0.2.69 → redeploy-0.2.70}/tests/test_example_helm_kustomize_plugin.py +0 -0
  243. {redeploy-0.2.69 → redeploy-0.2.70}/tests/test_hardware_config.py +0 -0
  244. {redeploy-0.2.69 → redeploy-0.2.70}/tests/test_hardware_kiosk.py +0 -0
  245. {redeploy-0.2.69 → redeploy-0.2.70}/tests/test_hardware_rules.py +0 -0
  246. {redeploy-0.2.69 → redeploy-0.2.70}/tests/test_heal_decider.py +0 -0
  247. {redeploy-0.2.69 → redeploy-0.2.70}/tests/test_heal_loop_detector.py +0 -0
  248. {redeploy-0.2.69 → redeploy-0.2.70}/tests/test_iac.py +0 -0
  249. {redeploy-0.2.69 → redeploy-0.2.70}/tests/test_iac_config_hints.py +0 -0
  250. {redeploy-0.2.69 → redeploy-0.2.70}/tests/test_iac_plugin_loading.py +0 -0
  251. {redeploy-0.2.69 → redeploy-0.2.70}/tests/test_import_plugin_template.py +0 -0
  252. {redeploy-0.2.69 → redeploy-0.2.70}/tests/test_parse.py +0 -0
  253. {redeploy-0.2.69 → redeploy-0.2.70}/tests/test_prompt_llm.py +0 -0
  254. {redeploy-0.2.69 → redeploy-0.2.70}/tests/test_redeploy.py +0 -0
  255. {redeploy-0.2.69 → redeploy-0.2.70}/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.69
3
+ Version: 0.2.70
4
4
  Summary: Infrastructure migration toolkit: detect → plan → apply
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -32,17 +32,17 @@ Requires-Dist: mcp>=1.0; extra == "mcp"
32
32
 
33
33
  ## AI Cost Tracking
34
34
 
35
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.69-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
36
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-36.1h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
35
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.70-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
36
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-36.8h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
37
37
 
38
- - 🤖 **LLM usage:** $7.5000 (90 commits)
39
- - 👤 **Human dev:** ~$3606 (36.1h @ $100/h, 30min dedup)
38
+ - 🤖 **LLM usage:** $7.5000 (91 commits)
39
+ - 👤 **Human dev:** ~$3684 (36.8h @ $100/h, 30min dedup)
40
40
 
41
41
  Generated on 2026-04-23 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
42
42
 
43
43
  ---
44
44
 
45
- ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.69-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
45
+ ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.70-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
46
46
 
47
47
  Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
48
48
 
@@ -57,6 +57,7 @@ redeploy prompt → NLP → command via LLM (natural language interface)
57
57
  redeploy mcp → start MCP server (Claude Desktop / VS Code / remote API)
58
58
  redeploy scan → find devices on LAN (device registry)
59
59
  redeploy target → deploy to named device (fleet)
60
+ redeploy gh-workflow → analyze/run GitHub Actions workflows on demand
60
61
  ```
61
62
 
62
63
  ## Install
@@ -247,6 +248,44 @@ redeploy run --env rpi5 --detect # deploy to rpi5 with live probe
247
248
  redeploy run --dry-run # uses .env DEPLOY_* vars if no redeploy.yaml
248
249
  ```
249
250
 
251
+ ### `redeploy gh-workflow [list|analyze|run]`
252
+
253
+ Analyze and trigger GitHub Actions workflows from your repo on demand.
254
+
255
+ Prerequisites:
256
+ - GitHub CLI installed: `gh`
257
+ - Authenticated session: `gh auth login`
258
+ - Workflow must define `workflow_dispatch` under `on:` to be runnable manually
259
+
260
+ Common usage:
261
+
262
+ ```bash
263
+ # List all workflow files and dispatch readiness
264
+ redeploy gh-workflow list
265
+
266
+ # Analyze one workflow (triggers/jobs + hint if not dispatchable)
267
+ redeploy gh-workflow analyze version-drift
268
+
269
+ # Analyze all workflows in a custom repo path
270
+ redeploy gh-workflow analyze --repo-root /path/to/repo
271
+
272
+ # Trigger workflow_dispatch run on demand
273
+ redeploy gh-workflow run version-drift --ref main
274
+
275
+ # Pass workflow inputs (repeat --field)
276
+ redeploy gh-workflow run release --field env=prod --field force=true
277
+
278
+ # Trigger and wait for completion (non-zero exit when workflow fails)
279
+ redeploy gh-workflow run version-drift --watch
280
+
281
+ # Preview gh command without executing
282
+ redeploy gh-workflow run version-drift --dry-run
283
+ ```
284
+
285
+ Notes:
286
+ - `redeploy workflow ...` is for workflows from `redeploy.css`.
287
+ - `redeploy gh-workflow ...` is for GitHub Actions in `.github/workflows/`.
288
+
250
289
  ### Generic pipeline hooks (recommended)
251
290
 
252
291
  Use top-level `hooks:` in your migration spec to run custom actions in specific phases.
@@ -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.69-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-36.1h-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.70-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-36.8h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $7.5000 (90 commits)
10
- - 👤 **Human dev:** ~$3606 (36.1h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $7.5000 (91 commits)
10
+ - 👤 **Human dev:** ~$3684 (36.8h @ $100/h, 30min dedup)
11
11
 
12
12
  Generated on 2026-04-23 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.69-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.70-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
 
@@ -28,6 +28,7 @@ redeploy prompt → NLP → command via LLM (natural language interface)
28
28
  redeploy mcp → start MCP server (Claude Desktop / VS Code / remote API)
29
29
  redeploy scan → find devices on LAN (device registry)
30
30
  redeploy target → deploy to named device (fleet)
31
+ redeploy gh-workflow → analyze/run GitHub Actions workflows on demand
31
32
  ```
32
33
 
33
34
  ## Install
@@ -218,6 +219,44 @@ redeploy run --env rpi5 --detect # deploy to rpi5 with live probe
218
219
  redeploy run --dry-run # uses .env DEPLOY_* vars if no redeploy.yaml
219
220
  ```
220
221
 
222
+ ### `redeploy gh-workflow [list|analyze|run]`
223
+
224
+ Analyze and trigger GitHub Actions workflows from your repo on demand.
225
+
226
+ Prerequisites:
227
+ - GitHub CLI installed: `gh`
228
+ - Authenticated session: `gh auth login`
229
+ - Workflow must define `workflow_dispatch` under `on:` to be runnable manually
230
+
231
+ Common usage:
232
+
233
+ ```bash
234
+ # List all workflow files and dispatch readiness
235
+ redeploy gh-workflow list
236
+
237
+ # Analyze one workflow (triggers/jobs + hint if not dispatchable)
238
+ redeploy gh-workflow analyze version-drift
239
+
240
+ # Analyze all workflows in a custom repo path
241
+ redeploy gh-workflow analyze --repo-root /path/to/repo
242
+
243
+ # Trigger workflow_dispatch run on demand
244
+ redeploy gh-workflow run version-drift --ref main
245
+
246
+ # Pass workflow inputs (repeat --field)
247
+ redeploy gh-workflow run release --field env=prod --field force=true
248
+
249
+ # Trigger and wait for completion (non-zero exit when workflow fails)
250
+ redeploy gh-workflow run version-drift --watch
251
+
252
+ # Preview gh command without executing
253
+ redeploy gh-workflow run version-drift --dry-run
254
+ ```
255
+
256
+ Notes:
257
+ - `redeploy workflow ...` is for workflows from `redeploy.css`.
258
+ - `redeploy gh-workflow ...` is for GitHub Actions in `.github/workflows/`.
259
+
221
260
  ### Generic pipeline hooks (recommended)
222
261
 
223
262
  Use top-level `hooks:` in your migration spec to run custom actions in specific phases.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "redeploy"
7
- version = "0.2.69"
7
+ version = "0.2.70"
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.69"
10
+ __version__ = "0.2.70"
11
11
 
12
12
  # ── Core models ───────────────────────────────────────────────────────────────
13
13
  from .models import ( # noqa: F401
@@ -45,6 +45,7 @@ from .commands.probe import probe
45
45
  from .commands.target import target
46
46
  from .commands.version import version_cmd
47
47
  from .commands.workflow import workflow_cmd
48
+ from .commands.gh_workflow import gh_workflow_cmd
48
49
  from .commands.exec_ import exec_cmd, exec_multi_cmd
49
50
  from .commands.plan_apply import plan, apply, migrate, run
50
51
  from .commands.state import state_cmd
@@ -91,6 +92,7 @@ cli.add_command(status)
91
92
  cli.add_command(target)
92
93
  cli.add_command(version_cmd)
93
94
  cli.add_command(workflow_cmd)
95
+ cli.add_command(gh_workflow_cmd)
94
96
  cli.add_command(bump_cmd, name="bump")
95
97
  cli.add_command(fix_cmd, name="fix")
96
98
  cli.add_command(prompt_cmd, name="prompt")
@@ -0,0 +1,291 @@
1
+ """gh-workflow command - inspect and trigger GitHub Actions workflows on demand."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import shutil
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ import click
11
+ import yaml
12
+ from rich.console import Console
13
+ from rich.table import Table
14
+
15
+
16
+ def _find_repo_root(start: Path) -> Path | None:
17
+ cur = start.resolve()
18
+ for p in [cur, *cur.parents]:
19
+ if (p / ".git").exists() or (p / ".github" / "workflows").exists():
20
+ return p
21
+ return None
22
+
23
+
24
+ def _resolve_repo_root(repo_root_opt: str | None) -> Path:
25
+ root = Path(repo_root_opt).resolve() if repo_root_opt else _find_repo_root(Path.cwd())
26
+ if not root:
27
+ raise click.ClickException(
28
+ "Could not detect repository root. Use --repo-root PATH."
29
+ )
30
+ return root
31
+
32
+
33
+ def _workflows_dir(repo_root: Path) -> Path:
34
+ wf_dir = repo_root / ".github" / "workflows"
35
+ if not wf_dir.exists():
36
+ raise click.ClickException(f"No workflows directory found: {wf_dir}")
37
+ return wf_dir
38
+
39
+
40
+ def _load_yaml(path: Path) -> dict:
41
+ try:
42
+ raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
43
+ except Exception as exc:
44
+ raise click.ClickException(f"Failed to parse workflow YAML: {path}: {exc}") from exc
45
+ if not isinstance(raw, dict):
46
+ raise click.ClickException(f"Workflow file must contain a YAML mapping: {path}")
47
+ return raw
48
+
49
+
50
+ def _on_section(raw: dict) -> object:
51
+ # PyYAML can parse key "on" as boolean True (YAML 1.1).
52
+ return raw.get("on", raw.get(True))
53
+
54
+
55
+ def _trigger_list(on_value: object) -> list[str]:
56
+ if isinstance(on_value, str):
57
+ return [on_value]
58
+ if isinstance(on_value, list):
59
+ return [str(x) for x in on_value]
60
+ if isinstance(on_value, dict):
61
+ return [str(k) for k in on_value.keys()]
62
+ return []
63
+
64
+
65
+ def _has_workflow_dispatch(on_value: object) -> bool:
66
+ if isinstance(on_value, str):
67
+ return on_value == "workflow_dispatch"
68
+ if isinstance(on_value, list):
69
+ return "workflow_dispatch" in on_value
70
+ if isinstance(on_value, dict):
71
+ return "workflow_dispatch" in on_value
72
+ return False
73
+
74
+
75
+ def _collect_workflow_meta(path: Path) -> dict:
76
+ raw = _load_yaml(path)
77
+ on_value = _on_section(raw)
78
+ jobs = raw.get("jobs", {})
79
+ job_names = list(jobs.keys()) if isinstance(jobs, dict) else []
80
+ return {
81
+ "file": path.name,
82
+ "name": str(raw.get("name") or path.stem),
83
+ "triggers": _trigger_list(on_value),
84
+ "dispatch": _has_workflow_dispatch(on_value),
85
+ "jobs": job_names,
86
+ }
87
+
88
+
89
+ def _resolve_workflow_identifier(wf_dir: Path, workflow: str) -> Path:
90
+ candidate = wf_dir / workflow
91
+ if candidate.exists() and candidate.is_file():
92
+ return candidate
93
+
94
+ for ext in (".yml", ".yaml"):
95
+ p = wf_dir / f"{workflow}{ext}"
96
+ if p.exists() and p.is_file():
97
+ return p
98
+
99
+ for p in sorted(wf_dir.glob("*.y*ml")):
100
+ meta = _collect_workflow_meta(p)
101
+ if meta["name"] == workflow or p.stem == workflow:
102
+ return p
103
+
104
+ available = ", ".join(sorted(p.name for p in wf_dir.glob("*.y*ml")))
105
+ raise click.ClickException(
106
+ f"Workflow not found: {workflow}. Available files: {available}"
107
+ )
108
+
109
+
110
+ def _gh_available() -> bool:
111
+ return shutil.which("gh") is not None
112
+
113
+
114
+ def _parse_fields(fields: tuple[str, ...]) -> list[str]:
115
+ parsed: list[str] = []
116
+ for item in fields:
117
+ if "=" not in item:
118
+ raise click.ClickException(
119
+ f"Invalid --field '{item}'. Expected KEY=VALUE."
120
+ )
121
+ parsed.append(item)
122
+ return parsed
123
+
124
+
125
+ @click.group("gh-workflow")
126
+ def gh_workflow_cmd():
127
+ """Inspect and run GitHub Actions workflows on demand."""
128
+
129
+
130
+ @gh_workflow_cmd.command("list")
131
+ @click.option("--repo-root", default=None, type=click.Path(file_okay=False, path_type=str),
132
+ help="Repository root (auto-detected if omitted)")
133
+ def gh_workflow_list(repo_root: str | None):
134
+ """List workflow files and whether they are dispatchable."""
135
+ console = Console()
136
+ root = _resolve_repo_root(repo_root)
137
+ wf_dir = _workflows_dir(root)
138
+ files = sorted(wf_dir.glob("*.y*ml"))
139
+ if not files:
140
+ console.print(f"[yellow]No workflow files found in {wf_dir}[/yellow]")
141
+ return
142
+
143
+ table = Table(title=f"GitHub Workflows ({root.name})")
144
+ table.add_column("File", style="cyan")
145
+ table.add_column("Name")
146
+ table.add_column("Dispatch", justify="center")
147
+ table.add_column("Triggers")
148
+ table.add_column("Jobs", justify="right")
149
+
150
+ for path in files:
151
+ meta = _collect_workflow_meta(path)
152
+ dispatch = "yes" if meta["dispatch"] else "no"
153
+ triggers = ", ".join(meta["triggers"]) if meta["triggers"] else "-"
154
+ table.add_row(meta["file"], meta["name"], dispatch, triggers, str(len(meta["jobs"])))
155
+
156
+ console.print(table)
157
+
158
+
159
+ @gh_workflow_cmd.command("analyze")
160
+ @click.argument("workflow", required=False, default=None)
161
+ @click.option("--repo-root", default=None, type=click.Path(file_okay=False, path_type=str),
162
+ help="Repository root (auto-detected if omitted)")
163
+ def gh_workflow_analyze(workflow: str | None, repo_root: str | None):
164
+ """Analyze one workflow (or all workflows) for triggers/jobs/dispatch readiness."""
165
+ console = Console()
166
+ root = _resolve_repo_root(repo_root)
167
+ wf_dir = _workflows_dir(root)
168
+
169
+ selected: list[Path]
170
+ if workflow:
171
+ selected = [_resolve_workflow_identifier(wf_dir, workflow)]
172
+ else:
173
+ selected = sorted(wf_dir.glob("*.y*ml"))
174
+
175
+ for path in selected:
176
+ meta = _collect_workflow_meta(path)
177
+ console.print(f"[bold]{meta['name']}[/bold] [dim]({meta['file']})[/dim]")
178
+ console.print(f" dispatchable: {'yes' if meta['dispatch'] else 'no'}")
179
+ console.print(
180
+ " triggers: "
181
+ + (", ".join(meta["triggers"]) if meta["triggers"] else "none")
182
+ )
183
+ if meta["jobs"]:
184
+ console.print(" jobs: " + ", ".join(meta["jobs"]))
185
+ else:
186
+ console.print(" jobs: none")
187
+ if not meta["dispatch"]:
188
+ console.print(
189
+ " [yellow]hint:[/yellow] add 'workflow_dispatch:' under 'on:' to run on demand"
190
+ )
191
+ console.print()
192
+
193
+
194
+ @gh_workflow_cmd.command("run")
195
+ @click.argument("workflow")
196
+ @click.option("--repo-root", default=None, type=click.Path(file_okay=False, path_type=str),
197
+ help="Repository root (auto-detected if omitted)")
198
+ @click.option("--ref", default=None, help="Git ref (branch/tag/SHA) for workflow_dispatch")
199
+ @click.option(
200
+ "--field",
201
+ "fields",
202
+ multiple=True,
203
+ help="workflow_dispatch input as KEY=VALUE (repeatable)",
204
+ )
205
+ @click.option("--watch", is_flag=True, help="Watch the newly started run until completion")
206
+ @click.option("--dry-run", is_flag=True, help="Print commands without executing")
207
+ def gh_workflow_run(
208
+ workflow: str,
209
+ repo_root: str | None,
210
+ ref: str | None,
211
+ fields: tuple[str, ...],
212
+ watch: bool,
213
+ dry_run: bool,
214
+ ):
215
+ """Trigger a GitHub Actions workflow_dispatch run on demand via gh CLI."""
216
+ console = Console()
217
+ root = _resolve_repo_root(repo_root)
218
+ wf_dir = _workflows_dir(root)
219
+ wf_path = _resolve_workflow_identifier(wf_dir, workflow)
220
+ meta = _collect_workflow_meta(wf_path)
221
+
222
+ if not meta["dispatch"]:
223
+ raise click.ClickException(
224
+ f"Workflow '{meta['file']}' is not dispatchable (missing workflow_dispatch trigger)."
225
+ )
226
+
227
+ if not _gh_available():
228
+ raise click.ClickException(
229
+ "GitHub CLI 'gh' is required. Install gh and run 'gh auth login'."
230
+ )
231
+
232
+ parsed_fields = _parse_fields(fields)
233
+
234
+ run_cmd: list[str] = ["gh", "workflow", "run", wf_path.name]
235
+ if ref:
236
+ run_cmd.extend(["--ref", ref])
237
+ for item in parsed_fields:
238
+ run_cmd.extend(["-f", item])
239
+
240
+ console.print(f"[bold]workflow:[/bold] {meta['name']} ({wf_path.name})")
241
+ console.print("[bold]command:[/bold] " + " ".join(run_cmd))
242
+
243
+ if dry_run:
244
+ return
245
+
246
+ result = subprocess.run(run_cmd, cwd=str(root), capture_output=True, text=True)
247
+ if result.returncode != 0:
248
+ msg = result.stderr.strip() or result.stdout.strip() or "gh workflow run failed"
249
+ raise click.ClickException(msg)
250
+
251
+ if result.stdout.strip():
252
+ console.print(result.stdout.strip())
253
+
254
+ if not watch:
255
+ return
256
+
257
+ list_cmd = [
258
+ "gh",
259
+ "run",
260
+ "list",
261
+ "--workflow",
262
+ wf_path.name,
263
+ "--limit",
264
+ "1",
265
+ "--json",
266
+ "databaseId,url,status,conclusion",
267
+ ]
268
+ list_res = subprocess.run(list_cmd, cwd=str(root), capture_output=True, text=True)
269
+ if list_res.returncode != 0:
270
+ msg = list_res.stderr.strip() or list_res.stdout.strip() or "gh run list failed"
271
+ raise click.ClickException(msg)
272
+
273
+ try:
274
+ entries = json.loads(list_res.stdout)
275
+ except Exception as exc:
276
+ raise click.ClickException(f"Could not parse gh run list output: {exc}") from exc
277
+
278
+ if not entries:
279
+ raise click.ClickException("No run found to watch.")
280
+
281
+ run_id = str(entries[0].get("databaseId", "")).strip()
282
+ run_url = str(entries[0].get("url", "")).strip()
283
+ if run_url:
284
+ console.print(f"[bold]run:[/bold] {run_url}")
285
+ if not run_id:
286
+ raise click.ClickException("Run ID missing from gh run list output.")
287
+
288
+ watch_cmd = ["gh", "run", "watch", run_id, "--exit-status"]
289
+ watch_res = subprocess.run(watch_cmd, cwd=str(root))
290
+ if watch_res.returncode != 0:
291
+ sys.exit(watch_res.returncode)
@@ -186,79 +186,13 @@ def _print_diagnostics(console, hw, show_fix):
186
186
 
187
187
  def _apply_transform(console, p, hw, transform):
188
188
  """Apply display transform via wlr-randr and persist in kanshi config."""
189
- # Find DSI output name (e.g. "DSI-2")
190
189
  dsi = next((o for o in hw.drm_outputs if "DSI" in o.name), None)
191
190
  if not dsi:
192
191
  console.print("[red]✗ No DSI output found on this host[/red]")
193
192
  sys.exit(1)
194
193
 
195
- output_name = dsi.connector # e.g. "DSI-2"
196
-
197
- console.print(f"[cyan]→ Setting transform {transform} on {output_name}[/cyan]")
198
-
199
- # 1. Apply immediately via wlr-randr
200
- wlr_cmd = (
201
- f"WAYLAND_DISPLAY=wayland-0 XDG_RUNTIME_DIR=/run/user/$(id -u) "
202
- f"wlr-randr --output {output_name} --transform {transform} 2>&1"
203
- )
204
- r = p.run(wlr_cmd)
205
- if r.ok:
206
- console.print(f"[green] ✓ wlr-randr transform applied[/green]")
207
- else:
208
- console.print(f"[yellow] ⚠ wlr-randr failed (compositor may not be running): {r.out.strip()}[/yellow]")
209
-
210
- # 2. Persist in kanshi config — update or create transform line
211
- kanshi_cfg_path = "~/.config/kanshi/config"
212
- # Read current config
213
- read_r = p.run(f"cat {kanshi_cfg_path} 2>/dev/null")
214
- import re as _re
215
-
216
- if read_r.ok and read_r.out.strip():
217
- current = read_r.out
218
- output_line_pat = _re.compile(
219
- rf'(\s*output\s+{_re.escape(output_name)}\s+enable)(\s+transform\s+\S+)?'
220
- )
221
- if _re.search(output_line_pat, current):
222
- # Replace or add transform on the existing enable line
223
- new_cfg = _re.sub(
224
- output_line_pat,
225
- rf'\1 transform {transform}',
226
- current,
227
- )
228
- else:
229
- # Output line without "enable" — just append transform param
230
- new_cfg = _re.sub(
231
- rf'(\s*output\s+{_re.escape(output_name)}\b)',
232
- rf'\1 transform {transform}',
233
- current,
234
- )
235
- else:
236
- # No kanshi config — create minimal one
237
- new_cfg = (
238
- f"profile waveshare-only {{\n"
239
- f" output {output_name} enable transform {transform}\n"
240
- f"}}\n"
241
- )
242
-
243
- # Write updated config
244
- escaped = new_cfg.replace("'", "'\\''")
245
- write_r = p.run(f"mkdir -p ~/.config/kanshi && printf '%s' '{escaped}' > {kanshi_cfg_path}")
246
- if write_r.ok:
247
- console.print(f"[green] ✓ kanshi config updated ({kanshi_cfg_path})[/green]")
248
- else:
249
- console.print(f"[red] ✗ failed to write kanshi config: {write_r.out}[/red]")
250
- sys.exit(1)
251
-
252
- # 3. Reload kanshi if running
253
- pkill_r = p.run("pkill -SIGUSR1 kanshi 2>/dev/null || pkill kanshi 2>/dev/null; sleep 1; "
254
- "WAYLAND_DISPLAY=wayland-0 XDG_RUNTIME_DIR=/run/user/$(id -u) kanshi &")
255
- if pkill_r.ok:
256
- console.print("[green] ✓ kanshi reloaded[/green]")
257
-
258
- console.print(
259
- f"\n[bold green]✓ Transform {transform} applied to {output_name}[/bold green]\n"
260
- f" [dim]Persistent: {kanshi_cfg_path}[/dim]"
261
- )
194
+ from ...config_apply.handlers.display import apply_display_transform
195
+ apply_display_transform(console, p, dsi.connector, transform)
262
196
 
263
197
 
264
198
  def _execute_query(hw, query_expr, output_fmt):
@@ -0,0 +1,10 @@
1
+ """Hardware/infrastructure config-apply handlers.
2
+
3
+ Each handler receives a ``(console, probe, value)`` triple and performs
4
+ the remote mutation + local reporting.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from .display import apply_display_transform
9
+
10
+ __all__ = ["apply_display_transform"]
@@ -0,0 +1,99 @@
1
+ """Apply display / kanshi configuration changes on the remote host."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ import sys
6
+
7
+ from rich.console import Console
8
+
9
+ from ...detect.remote import RemoteProbe
10
+ from ...hardware.kiosk.compositors import DSI_OUTPUT_NAME
11
+
12
+
13
+ def apply_display_transform(
14
+ console: Console,
15
+ probe: RemoteProbe,
16
+ output_name: str,
17
+ transform: str,
18
+ ) -> None:
19
+ """Apply *transform* to *output_name* via wlr-randr and persist in kanshi config.
20
+
21
+ Parameters
22
+ ----------
23
+ console:
24
+ Rich console for progress output.
25
+ probe:
26
+ Active SSH probe to the target host.
27
+ output_name:
28
+ DRM connector name (e.g. ``DSI-2``).
29
+ transform:
30
+ One of the wlr-randr transforms (``normal``, ``90``, ``180``, ``270``,
31
+ ``flipped``, …).
32
+ """
33
+ console.print(f"[cyan]→ Setting transform {transform} on {output_name}[/cyan]")
34
+
35
+ # 1. Apply immediately via wlr-randr
36
+ wlr_cmd = (
37
+ f"WAYLAND_DISPLAY=wayland-0 XDG_RUNTIME_DIR=/run/user/$(id -u) "
38
+ f"wlr-randr --output {output_name} --transform {transform} 2>&1"
39
+ )
40
+ r = probe.run(wlr_cmd)
41
+ if r.ok:
42
+ console.print(f"[green] ✓ wlr-randr transform applied[/green]")
43
+ else:
44
+ console.print(
45
+ f"[yellow] ⚠ wlr-randr failed (compositor may not be running): "
46
+ f"{r.out.strip()}[/yellow]"
47
+ )
48
+
49
+ # 2. Persist in kanshi config — update or create transform line
50
+ kanshi_cfg_path = "~/.config/kanshi/config"
51
+ read_r = probe.run(f"cat {kanshi_cfg_path} 2>/dev/null")
52
+
53
+ if read_r.ok and read_r.out.strip():
54
+ current = read_r.out
55
+ output_line_pat = re.compile(
56
+ rf'(\s*output\s+{re.escape(output_name)}\s+enable)(\s+transform\s+\S+)?'
57
+ )
58
+ if re.search(output_line_pat, current):
59
+ new_cfg = re.sub(
60
+ output_line_pat,
61
+ rf'\1 transform {transform}',
62
+ current,
63
+ )
64
+ else:
65
+ new_cfg = re.sub(
66
+ rf'(\s*output\s+{re.escape(output_name)}\b)',
67
+ rf'\1 transform {transform}',
68
+ current,
69
+ )
70
+ else:
71
+ new_cfg = (
72
+ f"profile waveshare-only {{\n"
73
+ f" output {output_name} enable transform {transform}\n"
74
+ f"}}\n"
75
+ )
76
+
77
+ # Write updated config
78
+ escaped = new_cfg.replace("'", "'\\''")
79
+ write_r = probe.run(
80
+ f"mkdir -p ~/.config/kanshi && printf '%s' '{escaped}' > {kanshi_cfg_path}"
81
+ )
82
+ if write_r.ok:
83
+ console.print(f"[green] ✓ kanshi config updated ({kanshi_cfg_path})[/green]")
84
+ else:
85
+ console.print(f"[red] ✗ failed to write kanshi config: {write_r.out}[/red]")
86
+ sys.exit(1)
87
+
88
+ # 3. Reload kanshi if running
89
+ pkill_r = probe.run(
90
+ "pkill -SIGUSR1 kanshi 2>/dev/null || pkill kanshi 2>/dev/null; sleep 1; "
91
+ "WAYLAND_DISPLAY=wayland-0 XDG_RUNTIME_DIR=/run/user/$(id -u) kanshi &"
92
+ )
93
+ if pkill_r.ok:
94
+ console.print("[green] ✓ kanshi reloaded[/green]")
95
+
96
+ console.print(
97
+ f"\n[bold green]✓ Transform {transform} applied to {output_name}[/bold green]\n"
98
+ f" [dim]Persistent: {kanshi_cfg_path}[/dim]"
99
+ )