redeploy 0.2.69__tar.gz → 0.2.71__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 (256) hide show
  1. {redeploy-0.2.69 → redeploy-0.2.71}/PKG-INFO +45 -6
  2. {redeploy-0.2.69 → redeploy-0.2.71}/README.md +44 -5
  3. {redeploy-0.2.69 → redeploy-0.2.71}/pyproject.toml +1 -1
  4. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/__init__.py +1 -1
  5. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/apply/handlers.py +8 -14
  6. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/__init__.py +2 -0
  7. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/exec_.py +4 -11
  8. redeploy-0.2.71/redeploy/cli/commands/gh_workflow.py +291 -0
  9. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/hardware.py +2 -68
  10. redeploy-0.2.71/redeploy/config_apply/handlers/__init__.py +10 -0
  11. redeploy-0.2.71/redeploy/config_apply/handlers/display.py +125 -0
  12. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/detect/probes.py +7 -1
  13. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/heal/runner.py +19 -2
  14. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/markpact/__init__.py +2 -0
  15. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/markpact/parser.py +30 -0
  16. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/mcp_server.py +42 -0
  17. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_heal.py +56 -0
  18. redeploy-0.2.71/redeploy/tests/test_security_hardening.py +93 -0
  19. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy.egg-info/PKG-INFO +45 -6
  20. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy.egg-info/SOURCES.txt +5 -0
  21. redeploy-0.2.71/tests/test_cli_gh_workflow.py +132 -0
  22. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/analyze/__init__.py +0 -0
  23. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/analyze/checkers/__init__.py +0 -0
  24. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/analyze/preflight_schema.py +0 -0
  25. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/analyze/spec_analyzer.py +0 -0
  26. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/apply/__init__.py +0 -0
  27. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/apply/exceptions.py +0 -0
  28. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/apply/executor.py +0 -0
  29. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/apply/progress.py +0 -0
  30. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/apply/rollback.py +0 -0
  31. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/apply/state.py +0 -0
  32. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/apply/state_apply.py +0 -0
  33. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/apply/utils/__init__.py +0 -0
  34. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/apply/utils/run_container_build.py +0 -0
  35. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/audit.py +0 -0
  36. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/blueprint/__init__.py +0 -0
  37. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/blueprint/extractor.py +0 -0
  38. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/blueprint/generators/__init__.py +0 -0
  39. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/blueprint/generators/docker_compose.py +0 -0
  40. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/blueprint/generators/migration.py +0 -0
  41. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/blueprint/sources/__init__.py +0 -0
  42. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/blueprint/sources/compose.py +0 -0
  43. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/blueprint/sources/hardware.py +0 -0
  44. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/blueprint/sources/infra.py +0 -0
  45. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/blueprint/sources/migration.py +0 -0
  46. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/__init__.py +0 -0
  47. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/audit.py +0 -0
  48. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/blueprint.py +0 -0
  49. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/bump_fix.py +0 -0
  50. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/detect.py +0 -0
  51. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/device_map.py +0 -0
  52. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/device_map_renderers.py +0 -0
  53. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/devices.py +0 -0
  54. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/diagnose.py +0 -0
  55. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/diff.py +0 -0
  56. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/export.py +0 -0
  57. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/import_.py +0 -0
  58. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/init.py +0 -0
  59. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/inspect.py +0 -0
  60. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/lint.py +0 -0
  61. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/mcp_cmd.py +0 -0
  62. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/patterns.py +0 -0
  63. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/plan_apply.py +0 -0
  64. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/plugin.py +0 -0
  65. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/probe.py +0 -0
  66. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/prompt_cmd.py +0 -0
  67. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/push.py +0 -0
  68. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/state.py +0 -0
  69. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/status.py +0 -0
  70. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/target.py +0 -0
  71. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/version/__init__.py +0 -0
  72. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/version/commands.py +0 -0
  73. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/version/helpers.py +0 -0
  74. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/version/monorepo.py +0 -0
  75. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/version/release.py +0 -0
  76. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/version/scanner.py +0 -0
  77. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/version/utils/__init__.py +0 -0
  78. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/version/utils/changelog_config.py +0 -0
  79. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/version/utils/git_config.py +0 -0
  80. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/commands/workflow.py +0 -0
  81. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/core.py +0 -0
  82. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/display.py +0 -0
  83. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli/query.py +0 -0
  84. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/cli.py +0 -0
  85. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/config_apply/__init__.py +0 -0
  86. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/config_apply/applier.py +0 -0
  87. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/config_apply/loader.py +0 -0
  88. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/data_sync.py +0 -0
  89. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/detect/__init__.py +0 -0
  90. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/detect/builtin/__init__.py +0 -0
  91. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/detect/detector.py +0 -0
  92. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/detect/hardware.py +0 -0
  93. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/detect/hardware_rules.py +0 -0
  94. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/detect/remote.py +0 -0
  95. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/detect/templates.py +0 -0
  96. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/detect/workflow.py +0 -0
  97. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/discovery.py +0 -0
  98. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/dsl/__init__.py +0 -0
  99. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/dsl/loader.py +0 -0
  100. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/dsl/parser.py +0 -0
  101. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/dsl_python/__init__.py +0 -0
  102. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/dsl_python/context.py +0 -0
  103. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/dsl_python/decorators.py +0 -0
  104. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/dsl_python/docker_steps.py +0 -0
  105. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/dsl_python/exceptions.py +0 -0
  106. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/dsl_python/runner.py +0 -0
  107. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/dsl_python/steps.py +0 -0
  108. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/fleet.py +0 -0
  109. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/hardware/__init__.py +0 -0
  110. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/hardware/config_txt.py +0 -0
  111. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/hardware/data/__init__.py +0 -0
  112. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/hardware/data/hyperpixel.py +0 -0
  113. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/hardware/data/official.py +0 -0
  114. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/hardware/data/waveshare.py +0 -0
  115. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/hardware/fixes.py +0 -0
  116. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/hardware/kiosk/__init__.py +0 -0
  117. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/hardware/kiosk/autostart.py +0 -0
  118. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/hardware/kiosk/browsers.py +0 -0
  119. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/hardware/kiosk/compositors.py +0 -0
  120. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/hardware/kiosk/output_profiles.py +0 -0
  121. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/hardware/panels.py +0 -0
  122. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/hardware/raspi_config.py +0 -0
  123. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/heal/__init__.py +0 -0
  124. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/heal/decider.py +0 -0
  125. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/heal/hint_provider.py +0 -0
  126. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/heal/log_writer.py +0 -0
  127. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/heal/loop_detector.py +0 -0
  128. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/heal.py +0 -0
  129. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/iac/__init__.py +0 -0
  130. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/iac/base.py +0 -0
  131. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/iac/config_hints.py +0 -0
  132. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/iac/docker_compose.py +0 -0
  133. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/iac/parsers/__init__.py +0 -0
  134. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/iac/parsers/compose.py +0 -0
  135. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/iac/registry.py +0 -0
  136. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/integrations/__init__.py +0 -0
  137. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/integrations/op3_bridge.py +0 -0
  138. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/markpact/compiler.py +0 -0
  139. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/markpact/models.py +0 -0
  140. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/models/__init__.py +0 -0
  141. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/models/blueprint.py +0 -0
  142. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/models/devices.py +0 -0
  143. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/models/enums.py +0 -0
  144. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/models/hardware.py +0 -0
  145. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/models/infra.py +0 -0
  146. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/models/manifest.py +0 -0
  147. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/models/persisted.py +0 -0
  148. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/models/pipeline.py +0 -0
  149. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/models/plan.py +0 -0
  150. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/models/spec.py +0 -0
  151. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/observe.py +0 -0
  152. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/parse.py +0 -0
  153. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/patterns.py +0 -0
  154. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/plan/__init__.py +0 -0
  155. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/plan/planner.py +0 -0
  156. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/plugins/__init__.py +0 -0
  157. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/plugins/builtin/__init__.py +0 -0
  158. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/plugins/builtin/browser_reload.py +0 -0
  159. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/plugins/builtin/hardware_diagnostic.py +0 -0
  160. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/plugins/builtin/notify.py +0 -0
  161. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/plugins/builtin/process_control.py +0 -0
  162. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/plugins/builtin/systemd_reload.py +0 -0
  163. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/schema.py +0 -0
  164. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/spec_loader.py +0 -0
  165. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/ssh.py +0 -0
  166. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/steps/__init__.py +0 -0
  167. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/steps/docker.py +0 -0
  168. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/steps/generic.py +0 -0
  169. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/steps/hardware.py +0 -0
  170. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/steps/k3s.py +0 -0
  171. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/steps/kiosk.py +0 -0
  172. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/steps/podman.py +0 -0
  173. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/steps/process.py +0 -0
  174. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/steps/scm.py +0 -0
  175. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/steps/transfer.py +0 -0
  176. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/__init__.py +0 -0
  177. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_analyze.py +0 -0
  178. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_audit.py +0 -0
  179. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_cli.py +0 -0
  180. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_data_sync.py +0 -0
  181. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_discovery.py +0 -0
  182. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_dsl.py +0 -0
  183. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_examples.py +0 -0
  184. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_executor.py +0 -0
  185. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_fleet.py +0 -0
  186. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_git_integration.py +0 -0
  187. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_heal_decider.py +0 -0
  188. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_hooks.py +0 -0
  189. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_iac.py +0 -0
  190. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_iac_compose_contract.py +0 -0
  191. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_iac_docker_compose_contract.py +0 -0
  192. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_manifest.py +0 -0
  193. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_markpact_compiler.py +0 -0
  194. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_markpact_parser.py +0 -0
  195. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_models.py +0 -0
  196. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_observe.py +0 -0
  197. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_parse.py +0 -0
  198. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_patterns.py +0 -0
  199. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_planner_edge.py +0 -0
  200. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_plugins.py +0 -0
  201. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_probes.py +0 -0
  202. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_public_api.py +0 -0
  203. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_resume.py +0 -0
  204. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_spec_loader.py +0 -0
  205. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_ssh.py +0 -0
  206. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_steps_registry.py +0 -0
  207. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_templates.py +0 -0
  208. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_verify.py +0 -0
  209. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_version.py +0 -0
  210. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_version_changelog.py +0 -0
  211. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_version_cli.py +0 -0
  212. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_version_manifest.py +0 -0
  213. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_version_sources.py +0 -0
  214. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/tests/test_workflow.py +0 -0
  215. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/verify.py +0 -0
  216. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/version/__init__.py +0 -0
  217. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/version/bump.py +0 -0
  218. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/version/changelog.py +0 -0
  219. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/version/commits.py +0 -0
  220. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/version/diff.py +0 -0
  221. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/version/git_integration.py +0 -0
  222. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/version/git_transaction.py +0 -0
  223. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/version/manifest.py +0 -0
  224. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/version/sources/__init__.py +0 -0
  225. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/version/sources/base.py +0 -0
  226. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/version/sources/json_.py +0 -0
  227. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/version/sources/plain.py +0 -0
  228. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/version/sources/regex.py +0 -0
  229. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/version/sources/toml_.py +0 -0
  230. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/version/sources/yaml_.py +0 -0
  231. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/version/transaction.py +0 -0
  232. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy/version.py +0 -0
  233. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy.egg-info/dependency_links.txt +0 -0
  234. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy.egg-info/entry_points.txt +0 -0
  235. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy.egg-info/requires.txt +0 -0
  236. {redeploy-0.2.69 → redeploy-0.2.71}/redeploy.egg-info/top_level.txt +0 -0
  237. {redeploy-0.2.69 → redeploy-0.2.71}/setup.cfg +0 -0
  238. {redeploy-0.2.69 → redeploy-0.2.71}/tests/test_blueprint_sources.py +0 -0
  239. {redeploy-0.2.69 → redeploy-0.2.71}/tests/test_blueprint_sources_compose.py +0 -0
  240. {redeploy-0.2.69 → redeploy-0.2.71}/tests/test_cli_query.py +0 -0
  241. {redeploy-0.2.69 → redeploy-0.2.71}/tests/test_example_argocd_flux_plugin.py +0 -0
  242. {redeploy-0.2.69 → redeploy-0.2.71}/tests/test_example_gitops_ci_plugin.py +0 -0
  243. {redeploy-0.2.69 → redeploy-0.2.71}/tests/test_example_helm_kustomize_plugin.py +0 -0
  244. {redeploy-0.2.69 → redeploy-0.2.71}/tests/test_hardware_config.py +0 -0
  245. {redeploy-0.2.69 → redeploy-0.2.71}/tests/test_hardware_kiosk.py +0 -0
  246. {redeploy-0.2.69 → redeploy-0.2.71}/tests/test_hardware_rules.py +0 -0
  247. {redeploy-0.2.69 → redeploy-0.2.71}/tests/test_heal_decider.py +0 -0
  248. {redeploy-0.2.69 → redeploy-0.2.71}/tests/test_heal_loop_detector.py +0 -0
  249. {redeploy-0.2.69 → redeploy-0.2.71}/tests/test_iac.py +0 -0
  250. {redeploy-0.2.69 → redeploy-0.2.71}/tests/test_iac_config_hints.py +0 -0
  251. {redeploy-0.2.69 → redeploy-0.2.71}/tests/test_iac_plugin_loading.py +0 -0
  252. {redeploy-0.2.69 → redeploy-0.2.71}/tests/test_import_plugin_template.py +0 -0
  253. {redeploy-0.2.69 → redeploy-0.2.71}/tests/test_parse.py +0 -0
  254. {redeploy-0.2.69 → redeploy-0.2.71}/tests/test_prompt_llm.py +0 -0
  255. {redeploy-0.2.69 → redeploy-0.2.71}/tests/test_redeploy.py +0 -0
  256. {redeploy-0.2.69 → redeploy-0.2.71}/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.71
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.71-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-37.3h-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 (92 commits)
39
+ - 👤 **Human dev:** ~$3734 (37.3h @ $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.71-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.71-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-37.3h-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 (92 commits)
10
+ - 👤 **Human dev:** ~$3734 (37.3h @ $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.71-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.71"
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.71"
11
11
 
12
12
  # ── Core models ───────────────────────────────────────────────────────────────
13
13
  from .models import ( # noqa: F401
@@ -350,7 +350,7 @@ def _resolve_command_ref(command_ref: str, step: MigrationStep, plan: MigrationP
350
350
  - "#section-id" - script from section in current spec file
351
351
  - "#kiosk-browser-configuration-script" - markpact:ref block
352
352
  """
353
- from ..markpact.parser import extract_script_from_markdown, extract_script_by_ref
353
+ from ..markpact import resolve_script_ref
354
354
 
355
355
  # Parse command_ref
356
356
  if "#" in command_ref:
@@ -367,21 +367,15 @@ def _resolve_command_ref(command_ref: str, step: MigrationStep, plan: MigrationP
367
367
  if not file_path.exists():
368
368
  raise StepError(step, f"Command ref file not found: {file_path}")
369
369
 
370
- # Extract script from markdown - try both methods:
371
- # 1. First try markpact:ref codeblock (new format)
372
- # 2. Then try section heading (old format)
373
370
  markdown_content = file_path.read_text(encoding="utf-8")
371
+ result = resolve_script_ref(markdown_content, section_id, language="bash")
374
372
 
375
- # Try markpact:ref format first
376
- script = extract_script_by_ref(markdown_content, section_id, language="bash")
377
-
378
- # Fallback to section heading format
379
- if script is None:
380
- script = extract_script_from_markdown(markdown_content, section_id, language="bash")
381
-
382
- if script is None:
383
- raise StepError(step, f"Could not find bash script for ref '{section_id}' in {file_path} (tried markpact:ref and section heading)")
384
-
373
+ if result is None:
374
+ raise StepError(
375
+ step,
376
+ f"Could not find bash script for ref '{section_id}' in {file_path} (tried markpact:ref and section heading)"
377
+ )
378
+ script, _ = result
385
379
  return script
386
380
 
387
381
 
@@ -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")
@@ -202,19 +202,12 @@ def _extract_all_scripts(md_path, ref_list, console) -> list[tuple[str, str, str
202
202
 
203
203
  def _extract_script_for_ref(md_content, ref_id) -> tuple[str, str] | None:
204
204
  """Extract script for a single ref."""
205
- from ...markpact.parser import extract_script_from_markdown, extract_script_by_ref
206
-
207
- # Try markpact:ref first
208
- script = extract_script_by_ref(md_content, ref_id, language="bash")
209
- lookup_method = "markpact:ref"
210
-
211
- if script is None and ref_id.startswith("#"):
212
- script = extract_script_from_markdown(md_content, ref_id[1:], language="bash")
213
- lookup_method = "section"
205
+ from ...markpact import resolve_script_ref
214
206
 
215
- if script is None:
207
+ result = resolve_script_ref(md_content, ref_id, language="bash")
208
+ if result is None:
216
209
  return None
217
- return script, lookup_method
210
+ return result
218
211
 
219
212
 
220
213
  def _execute_single_script(ref_id, script, host, timeout) -> bool:
@@ -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"]