redeploy 0.2.74__tar.gz → 0.2.76__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 (275) hide show
  1. {redeploy-0.2.74 → redeploy-0.2.76}/PKG-INFO +8 -8
  2. {redeploy-0.2.74 → redeploy-0.2.76}/README.md +6 -6
  3. {redeploy-0.2.74 → redeploy-0.2.76}/pyproject.toml +2 -3
  4. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/__init__.py +1 -1
  5. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/analyze/__init__.py +2 -1
  6. redeploy-0.2.76/redeploy/analyze/checkers/__init__.py +47 -0
  7. redeploy-0.2.76/redeploy/analyze/checkers/base.py +19 -0
  8. redeploy-0.2.76/redeploy/analyze/checkers/binary.py +74 -0
  9. redeploy-0.2.76/redeploy/analyze/checkers/command_ref.py +83 -0
  10. redeploy-0.2.76/redeploy/analyze/checkers/compose.py +143 -0
  11. redeploy-0.2.76/redeploy/analyze/checkers/docker_build.py +160 -0
  12. redeploy-0.2.76/redeploy/analyze/checkers/env.py +23 -0
  13. redeploy-0.2.76/redeploy/analyze/checkers/path.py +94 -0
  14. redeploy-0.2.76/redeploy/analyze/checkers/reference.py +39 -0
  15. redeploy-0.2.76/redeploy/analyze/ignore.py +86 -0
  16. redeploy-0.2.76/redeploy/analyze/models.py +48 -0
  17. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/analyze/preflight_schema.py +1 -1
  18. redeploy-0.2.76/redeploy/analyze/spec_analyzer.py +118 -0
  19. redeploy-0.2.76/redeploy/audit/__init__.py +51 -0
  20. redeploy-0.2.76/redeploy/audit/auditor.py +155 -0
  21. redeploy-0.2.76/redeploy/audit/extractor.py +182 -0
  22. redeploy-0.2.76/redeploy/audit/models.py +81 -0
  23. redeploy-0.2.76/redeploy/audit/paths.py +34 -0
  24. redeploy-0.2.76/redeploy/audit/patterns.py +28 -0
  25. redeploy-0.2.76/redeploy/audit/probe.py +78 -0
  26. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy.egg-info/PKG-INFO +8 -8
  27. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy.egg-info/SOURCES.txt +17 -1
  28. redeploy-0.2.74/redeploy/analyze/checkers/__init__.py +0 -1
  29. redeploy-0.2.74/redeploy/analyze/spec_analyzer.py +0 -742
  30. redeploy-0.2.74/redeploy/audit.py +0 -609
  31. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/apply/__init__.py +0 -0
  32. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/apply/exceptions.py +0 -0
  33. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/apply/executor.py +0 -0
  34. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/apply/handlers.py +0 -0
  35. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/apply/progress.py +0 -0
  36. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/apply/rollback.py +0 -0
  37. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/apply/state.py +0 -0
  38. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/apply/state_apply.py +0 -0
  39. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/apply/utils/__init__.py +0 -0
  40. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/apply/utils/run_container_build.py +0 -0
  41. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/blueprint/__init__.py +0 -0
  42. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/blueprint/extractor.py +0 -0
  43. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/blueprint/generators/__init__.py +0 -0
  44. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/blueprint/generators/docker_compose.py +0 -0
  45. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/blueprint/generators/migration.py +0 -0
  46. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/blueprint/sources/__init__.py +0 -0
  47. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/blueprint/sources/compose.py +0 -0
  48. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/blueprint/sources/hardware.py +0 -0
  49. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/blueprint/sources/infra.py +0 -0
  50. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/blueprint/sources/migration.py +0 -0
  51. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/__init__.py +0 -0
  52. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/__init__.py +0 -0
  53. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/audit.py +0 -0
  54. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/blueprint.py +0 -0
  55. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/bump_fix.py +0 -0
  56. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/detect.py +0 -0
  57. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/device_map.py +0 -0
  58. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/device_map_renderers.py +0 -0
  59. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/devices.py +0 -0
  60. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/diagnose.py +0 -0
  61. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/diff.py +0 -0
  62. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/exec_.py +0 -0
  63. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/export.py +0 -0
  64. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/gh_workflow.py +0 -0
  65. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/hardware.py +0 -0
  66. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/import_.py +0 -0
  67. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/init.py +0 -0
  68. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/inspect.py +0 -0
  69. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/lint.py +0 -0
  70. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/mcp_cmd.py +0 -0
  71. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/patterns.py +0 -0
  72. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/plan_apply.py +0 -0
  73. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/plugin.py +0 -0
  74. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/probe.py +0 -0
  75. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/prompt_cmd.py +0 -0
  76. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/push.py +0 -0
  77. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/state.py +0 -0
  78. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/status.py +0 -0
  79. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/target.py +0 -0
  80. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/version/__init__.py +0 -0
  81. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/version/commands.py +0 -0
  82. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/version/helpers.py +0 -0
  83. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/version/monorepo.py +0 -0
  84. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/version/release.py +0 -0
  85. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/version/scanner.py +0 -0
  86. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/version/utils/__init__.py +0 -0
  87. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/version/utils/changelog_config.py +0 -0
  88. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/version/utils/git_config.py +0 -0
  89. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/workflow.py +0 -0
  90. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/core.py +0 -0
  91. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/display.py +0 -0
  92. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/query.py +0 -0
  93. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli.py +0 -0
  94. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/config_apply/__init__.py +0 -0
  95. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/config_apply/applier.py +0 -0
  96. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/config_apply/handlers/__init__.py +0 -0
  97. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/config_apply/handlers/display.py +0 -0
  98. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/config_apply/loader.py +0 -0
  99. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/data_sync.py +0 -0
  100. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/detect/__init__.py +0 -0
  101. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/detect/builtin/__init__.py +0 -0
  102. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/detect/detector.py +0 -0
  103. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/detect/hardware.py +0 -0
  104. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/detect/hardware_rules.py +0 -0
  105. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/detect/probes.py +0 -0
  106. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/detect/remote.py +0 -0
  107. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/detect/templates.py +0 -0
  108. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/detect/workflow.py +0 -0
  109. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/discovery.py +0 -0
  110. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/dsl/__init__.py +0 -0
  111. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/dsl/loader.py +0 -0
  112. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/dsl/parser.py +0 -0
  113. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/dsl_python/__init__.py +0 -0
  114. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/dsl_python/context.py +0 -0
  115. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/dsl_python/decorators.py +0 -0
  116. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/dsl_python/docker_steps.py +0 -0
  117. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/dsl_python/exceptions.py +0 -0
  118. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/dsl_python/runner.py +0 -0
  119. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/dsl_python/steps.py +0 -0
  120. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/fleet.py +0 -0
  121. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/hardware/__init__.py +0 -0
  122. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/hardware/config_txt.py +0 -0
  123. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/hardware/data/__init__.py +0 -0
  124. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/hardware/data/hyperpixel.py +0 -0
  125. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/hardware/data/official.py +0 -0
  126. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/hardware/data/waveshare.py +0 -0
  127. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/hardware/fixes.py +0 -0
  128. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/hardware/kiosk/__init__.py +0 -0
  129. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/hardware/kiosk/autostart.py +0 -0
  130. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/hardware/kiosk/browsers.py +0 -0
  131. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/hardware/kiosk/compositors.py +0 -0
  132. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/hardware/kiosk/output_profiles.py +0 -0
  133. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/hardware/panels.py +0 -0
  134. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/hardware/raspi_config.py +0 -0
  135. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/heal/__init__.py +0 -0
  136. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/heal/decider.py +0 -0
  137. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/heal/hint_provider.py +0 -0
  138. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/heal/log_writer.py +0 -0
  139. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/heal/loop_detector.py +0 -0
  140. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/heal/runner.py +0 -0
  141. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/heal.py +0 -0
  142. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/iac/__init__.py +0 -0
  143. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/iac/base.py +0 -0
  144. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/iac/config_hints.py +0 -0
  145. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/iac/docker_compose.py +0 -0
  146. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/iac/parsers/__init__.py +0 -0
  147. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/iac/parsers/compose.py +0 -0
  148. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/iac/registry.py +0 -0
  149. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/integrations/__init__.py +0 -0
  150. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/integrations/op3_bridge.py +0 -0
  151. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/markpact/__init__.py +0 -0
  152. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/markpact/compiler.py +0 -0
  153. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/markpact/models.py +0 -0
  154. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/markpact/parser.py +0 -0
  155. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/mcp_server.py +0 -0
  156. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/models/__init__.py +0 -0
  157. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/models/blueprint.py +0 -0
  158. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/models/devices.py +0 -0
  159. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/models/enums.py +0 -0
  160. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/models/hardware.py +0 -0
  161. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/models/infra.py +0 -0
  162. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/models/manifest.py +0 -0
  163. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/models/persisted.py +0 -0
  164. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/models/pipeline.py +0 -0
  165. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/models/plan.py +0 -0
  166. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/models/spec.py +0 -0
  167. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/observe.py +0 -0
  168. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/parse.py +0 -0
  169. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/patterns.py +0 -0
  170. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/plan/__init__.py +0 -0
  171. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/plan/planner.py +0 -0
  172. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/plugins/__init__.py +0 -0
  173. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/plugins/builtin/__init__.py +0 -0
  174. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/plugins/builtin/browser_reload.py +0 -0
  175. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/plugins/builtin/hardware_diagnostic.py +0 -0
  176. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/plugins/builtin/notify.py +0 -0
  177. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/plugins/builtin/process_control.py +0 -0
  178. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/plugins/builtin/systemd_reload.py +0 -0
  179. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/schema.py +0 -0
  180. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/spec_loader.py +0 -0
  181. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/ssh.py +0 -0
  182. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/steps/__init__.py +0 -0
  183. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/steps/docker.py +0 -0
  184. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/steps/generic.py +0 -0
  185. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/steps/hardware.py +0 -0
  186. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/steps/k3s.py +0 -0
  187. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/steps/kiosk.py +0 -0
  188. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/steps/podman.py +0 -0
  189. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/steps/process.py +0 -0
  190. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/steps/scm.py +0 -0
  191. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/steps/transfer.py +0 -0
  192. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/__init__.py +0 -0
  193. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_analyze.py +0 -0
  194. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_audit.py +0 -0
  195. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_cli.py +0 -0
  196. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_data_sync.py +0 -0
  197. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_discovery.py +0 -0
  198. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_dsl.py +0 -0
  199. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_examples.py +0 -0
  200. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_executor.py +0 -0
  201. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_fleet.py +0 -0
  202. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_git_integration.py +0 -0
  203. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_heal.py +0 -0
  204. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_heal_decider.py +0 -0
  205. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_hooks.py +0 -0
  206. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_iac.py +0 -0
  207. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_iac_compose_contract.py +0 -0
  208. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_iac_docker_compose_contract.py +0 -0
  209. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_manifest.py +0 -0
  210. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_markpact_compiler.py +0 -0
  211. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_markpact_parser.py +0 -0
  212. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_models.py +0 -0
  213. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_observe.py +0 -0
  214. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_parse.py +0 -0
  215. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_patterns.py +0 -0
  216. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_planner_edge.py +0 -0
  217. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_plugins.py +0 -0
  218. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_probes.py +0 -0
  219. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_public_api.py +0 -0
  220. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_resume.py +0 -0
  221. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_security_hardening.py +0 -0
  222. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_spec_loader.py +0 -0
  223. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_ssh.py +0 -0
  224. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_steps_registry.py +0 -0
  225. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_templates.py +0 -0
  226. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_verify.py +0 -0
  227. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_version.py +0 -0
  228. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_version_changelog.py +0 -0
  229. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_version_cli.py +0 -0
  230. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_version_manifest.py +0 -0
  231. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_version_sources.py +0 -0
  232. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_workflow.py +0 -0
  233. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/verify.py +0 -0
  234. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/version/__init__.py +0 -0
  235. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/version/bump.py +0 -0
  236. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/version/changelog.py +0 -0
  237. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/version/commits.py +0 -0
  238. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/version/diff.py +0 -0
  239. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/version/git_integration.py +0 -0
  240. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/version/git_transaction.py +0 -0
  241. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/version/manifest.py +0 -0
  242. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/version/sources/__init__.py +0 -0
  243. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/version/sources/base.py +0 -0
  244. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/version/sources/json_.py +0 -0
  245. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/version/sources/plain.py +0 -0
  246. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/version/sources/regex.py +0 -0
  247. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/version/sources/toml_.py +0 -0
  248. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/version/sources/yaml_.py +0 -0
  249. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/version/transaction.py +0 -0
  250. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/version.py +0 -0
  251. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy.egg-info/dependency_links.txt +0 -0
  252. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy.egg-info/entry_points.txt +0 -0
  253. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy.egg-info/requires.txt +0 -0
  254. {redeploy-0.2.74 → redeploy-0.2.76}/redeploy.egg-info/top_level.txt +0 -0
  255. {redeploy-0.2.74 → redeploy-0.2.76}/setup.cfg +0 -0
  256. {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_blueprint_sources.py +0 -0
  257. {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_blueprint_sources_compose.py +0 -0
  258. {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_cli_gh_workflow.py +0 -0
  259. {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_cli_query.py +0 -0
  260. {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_example_argocd_flux_plugin.py +0 -0
  261. {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_example_gitops_ci_plugin.py +0 -0
  262. {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_example_helm_kustomize_plugin.py +0 -0
  263. {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_hardware_config.py +0 -0
  264. {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_hardware_kiosk.py +0 -0
  265. {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_hardware_rules.py +0 -0
  266. {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_heal_decider.py +0 -0
  267. {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_heal_loop_detector.py +0 -0
  268. {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_iac.py +0 -0
  269. {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_iac_config_hints.py +0 -0
  270. {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_iac_plugin_loading.py +0 -0
  271. {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_import_plugin_template.py +0 -0
  272. {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_parse.py +0 -0
  273. {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_prompt_llm.py +0 -0
  274. {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_redeploy.py +0 -0
  275. {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_steps_kiosk.py +0 -0
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redeploy
3
- Version: 0.2.74
3
+ Version: 0.2.76
4
4
  Summary: Infrastructure migration toolkit: detect → plan → apply
5
- License: Apache-2.0
5
+ License-Expression: Apache-2.0
6
6
  Requires-Python: >=3.11
7
7
  Description-Content-Type: text/markdown
8
8
  Requires-Dist: pydantic>=2.0
@@ -33,17 +33,17 @@ Requires-Dist: mcp>=1.0; extra == "mcp"
33
33
 
34
34
  ## AI Cost Tracking
35
35
 
36
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.74-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
37
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-40.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
36
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.76-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
37
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$5.41-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-42.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
38
38
 
39
- - 🤖 **LLM usage:** $7.5000 (95 commits)
40
- - 👤 **Human dev:** ~$3998 (40.0h @ $100/h, 30min dedup)
39
+ - 🤖 **LLM usage:** $5.4083 (98 commits)
40
+ - 👤 **Human dev:** ~$4198 (42.0h @ $100/h, 30min dedup)
41
41
 
42
- Generated on 2026-04-30 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
42
+ Generated on 2026-05-20 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
43
43
 
44
44
  ---
45
45
 
46
- ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.74-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
46
+ ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.76-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
47
47
 
48
48
  Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
49
49
 
@@ -3,17 +3,17 @@
3
3
 
4
4
  ## AI Cost Tracking
5
5
 
6
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.74-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-40.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
6
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.76-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$5.41-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-42.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $7.5000 (95 commits)
10
- - 👤 **Human dev:** ~$3998 (40.0h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $5.4083 (98 commits)
10
+ - 👤 **Human dev:** ~$4198 (42.0h @ $100/h, 30min dedup)
11
11
 
12
- Generated on 2026-04-30 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
12
+ Generated on 2026-05-20 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
13
13
 
14
14
  ---
15
15
 
16
- ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.74-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.76-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
17
17
 
18
18
  Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
19
19
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "redeploy"
7
- version = "0.2.74"
7
+ version = "0.2.76"
8
8
  description = "Infrastructure migration toolkit: detect → plan → apply"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -22,6 +22,7 @@ dependencies = [
22
22
  "costs>=0.1.20",
23
23
  "pfix>=0.1.60",
24
24
  ]
25
+ license = "Apache-2.0"
25
26
 
26
27
  [project.scripts]
27
28
  redeploy = "redeploy.cli:cli"
@@ -36,8 +37,6 @@ dev = ["pytest>=8.0", "ruff>=0.4",
36
37
  op3 = ["op3>=0.1.8"]
37
38
  mcp = ["mcp>=1.0"]
38
39
 
39
- [project.license]
40
- text = "Apache-2.0"
41
40
  [tool.ruff]
42
41
  line-length = 100
43
42
 
@@ -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.74"
10
+ __version__ = "0.2.76"
11
11
 
12
12
  # ── Core models ───────────────────────────────────────────────────────────────
13
13
  from .models import ( # noqa: F401
@@ -1,7 +1,8 @@
1
1
  """Static analysis for migration specs — pre-flight checks before plan/apply."""
2
2
  from __future__ import annotations
3
3
 
4
- from .spec_analyzer import SpecAnalyzer, AnalysisResult, IssueSeverity
4
+ from .models import AnalysisResult, IssueSeverity
5
+ from .spec_analyzer import SpecAnalyzer
5
6
  from .preflight_schema import PreflightResult, generate_preflight_schema, save_preflight_schema
6
7
 
7
8
  __all__ = [
@@ -0,0 +1,47 @@
1
+ """Built-in static checkers for redeploy specs."""
2
+ from .base import Checker
3
+ from .binary import BinaryChecker, extract_binaries
4
+ from .command_ref import CommandRefChecker
5
+ from .compose import ComposeChecker, scan_compose_file
6
+ from .docker_build import DockerBuildChecker, collect_sync_mappings, parse_docker_build
7
+ from .env import EnvFileChecker
8
+ from .path import CommandPathChecker, PathChecker, is_inside, resolve_local_path
9
+ from .reference import ReferenceChecker
10
+
11
+ # Backward-compatible private aliases used in tests.
12
+ _Checker = Checker
13
+ _PathChecker = PathChecker
14
+ _CommandPathChecker = CommandPathChecker
15
+ _ReferenceChecker = ReferenceChecker
16
+ _ComposeChecker = ComposeChecker
17
+ _DockerBuildChecker = DockerBuildChecker
18
+ _CommandRefChecker = CommandRefChecker
19
+ _EnvFileChecker = EnvFileChecker
20
+ _BinaryChecker = BinaryChecker
21
+
22
+ __all__ = [
23
+ "Checker",
24
+ "PathChecker",
25
+ "CommandPathChecker",
26
+ "ReferenceChecker",
27
+ "ComposeChecker",
28
+ "DockerBuildChecker",
29
+ "CommandRefChecker",
30
+ "EnvFileChecker",
31
+ "BinaryChecker",
32
+ "scan_compose_file",
33
+ "collect_sync_mappings",
34
+ "parse_docker_build",
35
+ "extract_binaries",
36
+ "resolve_local_path",
37
+ "is_inside",
38
+ "_Checker",
39
+ "_PathChecker",
40
+ "_CommandPathChecker",
41
+ "_ReferenceChecker",
42
+ "_ComposeChecker",
43
+ "_DockerBuildChecker",
44
+ "_CommandRefChecker",
45
+ "_EnvFileChecker",
46
+ "_BinaryChecker",
47
+ ]
@@ -0,0 +1,19 @@
1
+ """Base class for static spec checkers."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+
6
+ from ..models import AnalysisResult
7
+ from ...markpact.models import MarkpactDocument
8
+ from ...models.spec import MigrationSpec
9
+
10
+
11
+ class Checker:
12
+ def check(
13
+ self,
14
+ spec: MigrationSpec,
15
+ document: MarkpactDocument | None,
16
+ base_dir: Path,
17
+ result: AnalysisResult,
18
+ ) -> None:
19
+ raise NotImplementedError
@@ -0,0 +1,74 @@
1
+ """Local binary availability checks for shell commands."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ import shlex
6
+ import shutil
7
+
8
+ from ..models import AnalysisResult, IssueSeverity
9
+ from .base import Checker
10
+
11
+ _WRAPPERS = frozenset({"sudo", "env", "command", "nohup", "time", "exec"})
12
+ _KEYWORDS = frozenset({
13
+ "if", "then", "else", "fi", "for", "do", "done", "while", "case", "esac",
14
+ "function", "in", "until", "select", "echo", "true", "false", "test", "[", "[[",
15
+ })
16
+ _SEPARATORS = frozenset({"&&", "||", "|", ";", "(", ")", "{", "}"})
17
+
18
+
19
+ def extract_binaries(cmd: str) -> list[str]:
20
+ try:
21
+ tokens = shlex.split(cmd, posix=True)
22
+ except ValueError:
23
+ tokens = cmd.split()
24
+
25
+ out: list[str] = []
26
+ expect_cmd = True
27
+ i = 0
28
+ while i < len(tokens):
29
+ tok = tokens[i]
30
+ if tok in _SEPARATORS:
31
+ expect_cmd = True
32
+ i += 1
33
+ continue
34
+ if expect_cmd:
35
+ if tok in _KEYWORDS or tok.startswith("$"):
36
+ if tok == "for":
37
+ i += 1
38
+ i += 1
39
+ expect_cmd = False
40
+ continue
41
+ if "=" in tok and not tok.startswith("--") and "/" not in tok:
42
+ i += 1
43
+ continue
44
+ if tok in _WRAPPERS:
45
+ i += 1
46
+ continue
47
+ if re.match(r"^[a-zA-Z_][a-zA-Z0-9_.-]*$", tok) and len(tok) > 1:
48
+ out.append(tok)
49
+ expect_cmd = False
50
+ i += 1
51
+ return out
52
+
53
+
54
+ class BinaryChecker(Checker):
55
+ """Warn if commands reference binaries not available locally (best-effort)."""
56
+
57
+ def check(self, spec, document, base_dir, result):
58
+ for step in spec.extra_steps:
59
+ action = str(step.get("action") or "")
60
+ if action in {"ensure_kanshi_profile"}:
61
+ continue
62
+ cmd = step.get("command") or ""
63
+ for binary in extract_binaries(cmd):
64
+ if shutil.which(binary) is None:
65
+ result.add(
66
+ IssueSeverity.WARNING,
67
+ "binaries",
68
+ f"Step '{step.get('id')}' command uses binary not found locally: '{binary}'",
69
+ step.get("id"),
70
+ suggestion=(
71
+ f"Install '{binary}' locally for lint parity, or ignore if it "
72
+ "exists only on remote host."
73
+ ),
74
+ )
@@ -0,0 +1,83 @@
1
+ """command_ref dependency validation."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+
6
+ from ..models import AnalysisResult, IssueSeverity
7
+ from .base import Checker
8
+ from .path import resolve_local_path
9
+
10
+
11
+ class CommandRefChecker(Checker):
12
+ """Validate command_ref references for nested markdown/script dependencies."""
13
+
14
+ def check(self, spec, document, base_dir, result):
15
+ from ...markpact.parser import resolve_script_ref
16
+
17
+ in_doc_refs: set[str] = set()
18
+ if document:
19
+ for block in document.blocks:
20
+ if block.kind == "ref" and block.ref_id:
21
+ in_doc_refs.add(block.ref_id)
22
+
23
+ for step in spec.extra_steps:
24
+ sid = step.get("id")
25
+ cref = step.get("command_ref")
26
+ if not cref:
27
+ continue
28
+
29
+ if "#" in cref:
30
+ self._check_external_ref(cref, sid, base_dir, document, in_doc_refs, result)
31
+ continue
32
+
33
+ if document and cref not in in_doc_refs:
34
+ result.add(
35
+ IssueSeverity.WARNING,
36
+ "command_ref",
37
+ f"Step '{sid}' references unknown command_ref '{cref}'",
38
+ sid,
39
+ suggestion="Define a matching markpact:ref block in this spec.",
40
+ )
41
+
42
+ def _check_external_ref(
43
+ self,
44
+ cref: str,
45
+ sid: str | None,
46
+ base_dir: Path,
47
+ document,
48
+ in_doc_refs: set[str],
49
+ result: AnalysisResult,
50
+ ) -> None:
51
+ from ...markpact.parser import resolve_script_ref
52
+
53
+ file_part, ref_id = cref.split("#", 1)
54
+ if file_part:
55
+ file_path = resolve_local_path(file_part, base_dir)
56
+ if not file_path.exists():
57
+ result.add(
58
+ IssueSeverity.ERROR,
59
+ "command_ref",
60
+ f"Step '{sid}' command_ref file missing: {file_part}",
61
+ sid,
62
+ suggestion=f"Create or fix path: {file_path}",
63
+ )
64
+ return
65
+ text = file_path.read_text(encoding="utf-8", errors="replace")
66
+ if resolve_script_ref(text, ref_id, language="bash") is None:
67
+ result.add(
68
+ IssueSeverity.ERROR,
69
+ "command_ref",
70
+ f"Step '{sid}' command_ref '#{ref_id}' not found in {file_part}",
71
+ sid,
72
+ suggestion="Add a matching markpact:ref bash block or section heading script.",
73
+ )
74
+ return
75
+
76
+ if document and ref_id not in in_doc_refs:
77
+ result.add(
78
+ IssueSeverity.WARNING,
79
+ "command_ref",
80
+ f"Step '{sid}' command_ref '#{ref_id}' not found as markpact:ref in current spec",
81
+ sid,
82
+ suggestion="If this is a heading-based script, keep as-is; otherwise add markpact:ref block.",
83
+ )
@@ -0,0 +1,143 @@
1
+ """Docker Compose static validation."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+
6
+ import yaml
7
+
8
+ from ..ignore import IgnoreList
9
+ from ..models import AnalysisResult, IssueSeverity
10
+ from .base import Checker
11
+
12
+
13
+ def _is_ignored(check_path: Path, base_dir: Path, ign: IgnoreList) -> bool:
14
+ try:
15
+ rel = check_path.resolve().relative_to(base_dir.resolve())
16
+ return ign.is_ignored(rel)
17
+ except ValueError:
18
+ return False
19
+
20
+
21
+ def _check_build_section(
22
+ svc_name: str,
23
+ svc: dict,
24
+ base_dir: Path,
25
+ result: AnalysisResult,
26
+ ign: IgnoreList,
27
+ ) -> None:
28
+ build = svc.get("build")
29
+ if not isinstance(build, dict):
30
+ return
31
+ ctx = build.get("context", ".")
32
+ ctx_path = base_dir / ctx if not str(ctx).startswith("/") else Path(ctx)
33
+ if not ctx_path.exists():
34
+ result.add(
35
+ IssueSeverity.ERROR, "compose",
36
+ f"Service '{svc_name}' build.context missing: {ctx}",
37
+ suggestion=f"Create directory {ctx_path} or fix context.",
38
+ )
39
+ dockerfile = build.get("dockerfile")
40
+ if not dockerfile:
41
+ return
42
+ df_path = ctx_path / dockerfile
43
+ if not df_path.exists() and not _is_ignored(df_path, base_dir, ign):
44
+ result.add(
45
+ IssueSeverity.ERROR, "compose",
46
+ f"Service '{svc_name}' Dockerfile missing: {df_path}",
47
+ suggestion=f"Create {df_path} or correct dockerfile path.",
48
+ )
49
+
50
+
51
+ def _check_env_files(
52
+ svc_name: str,
53
+ svc: dict,
54
+ base_dir: Path,
55
+ result: AnalysisResult,
56
+ ign: IgnoreList,
57
+ ) -> None:
58
+ env_files = svc.get("env_file", [])
59
+ if isinstance(env_files, str):
60
+ env_files = [env_files]
61
+ for ef in env_files:
62
+ ef_path = base_dir / ef if not str(ef).startswith("/") else Path(ef)
63
+ if not ef_path.exists() and not _is_ignored(ef_path, base_dir, ign):
64
+ result.add(
65
+ IssueSeverity.WARNING, "compose",
66
+ f"Service '{svc_name}' env_file missing: {ef}",
67
+ suggestion=f"Create {ef_path} or remove env_file entry.",
68
+ )
69
+
70
+
71
+ def _check_volumes(
72
+ svc_name: str,
73
+ svc: dict,
74
+ base_dir: Path,
75
+ result: AnalysisResult,
76
+ ign: IgnoreList,
77
+ ) -> None:
78
+ for vol in svc.get("volumes", []):
79
+ if not isinstance(vol, str) or ":" not in vol:
80
+ continue
81
+ host_part = vol.split(":", 1)[0]
82
+ if host_part.startswith("/"):
83
+ if not Path(host_part).exists() and not _is_ignored(Path(host_part), base_dir, ign):
84
+ result.add(
85
+ IssueSeverity.WARNING, "compose",
86
+ f"Service '{svc_name}' volume host path missing: {host_part}",
87
+ suggestion="Ensure host path exists before deployment.",
88
+ )
89
+ elif host_part != "." and "/" in host_part:
90
+ hp = base_dir / host_part
91
+ if not hp.exists() and not _is_ignored(hp, base_dir, ign):
92
+ result.add(
93
+ IssueSeverity.WARNING, "compose",
94
+ f"Service '{svc_name}' relative volume path missing: {hp}",
95
+ suggestion=f"Create {hp} or mount as named volume.",
96
+ )
97
+
98
+
99
+ def scan_compose_file(
100
+ path: Path,
101
+ base_dir: Path,
102
+ result: AnalysisResult,
103
+ ign: IgnoreList,
104
+ ) -> None:
105
+ try:
106
+ data = yaml.safe_load(path.read_text()) or {}
107
+ except Exception as exc:
108
+ result.add(IssueSeverity.ERROR, "compose", f"Cannot parse {path}: {exc}")
109
+ return
110
+
111
+ for svc_name, svc in (data.get("services") or {}).items():
112
+ if not isinstance(svc, dict):
113
+ continue
114
+ _check_build_section(svc_name, svc, base_dir, result, ign)
115
+ _check_env_files(svc_name, svc, base_dir, result, ign)
116
+ _check_volumes(svc_name, svc, base_dir, result, ign)
117
+
118
+
119
+ class ComposeChecker(Checker):
120
+ """Validate docker-compose files declared in spec or found in project."""
121
+
122
+ def check(self, spec, document, base_dir, result):
123
+ ign = IgnoreList(base_dir)
124
+ compose_files = list(spec.target.compose_files or [])
125
+ if not compose_files:
126
+ for candidate in (
127
+ "docker-compose.yml", "docker-compose.yaml",
128
+ "compose.yml", "compose.yaml",
129
+ ):
130
+ if (base_dir / candidate).exists():
131
+ compose_files.append(candidate)
132
+ break
133
+
134
+ for cf in compose_files:
135
+ path = base_dir / cf
136
+ if not path.exists():
137
+ result.add(
138
+ IssueSeverity.ERROR, "compose",
139
+ f"Declared compose file not found: {cf}",
140
+ suggestion=f"Create {path} or remove from target.compose_files.",
141
+ )
142
+ continue
143
+ scan_compose_file(path, base_dir, result, ign)
@@ -0,0 +1,160 @@
1
+ """Docker build command static validation."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ from pathlib import Path
6
+
7
+ from ..models import AnalysisResult, IssueSeverity
8
+ from .base import Checker
9
+
10
+ DOCKER_BUILD_RE = re.compile(r"docker\s+(?:buildx\s+)?build\s+")
11
+ FILE_FLAG_RE = re.compile(r"-f\s+(\S+)|--file[=\s]+(\S+)")
12
+ STANDARD_DOCKERFILES = frozenset({
13
+ "Dockerfile", "dockerfile", "Dockerfile.prod", "Dockerfile.dev",
14
+ })
15
+
16
+
17
+ def collect_sync_mappings(spec) -> tuple[set[str], list[tuple[str, str]]]:
18
+ sync_dests: set[str] = set()
19
+ sync_mappings: list[tuple[str, str]] = []
20
+ for step in spec.extra_steps:
21
+ sid = step.get("id", "")
22
+ if not str(sid).startswith("sync_"):
23
+ continue
24
+ dst = step.get("dst", "")
25
+ src = step.get("src", "")
26
+ if dst:
27
+ sync_dests.add(dst.rstrip("/"))
28
+ if src:
29
+ sync_mappings.append((dst.rstrip("/"), src))
30
+ return sync_dests, sync_mappings
31
+
32
+
33
+ def parse_docker_build(cmd: str) -> tuple[str | None, str]:
34
+ dockerfile = None
35
+ for match in FILE_FLAG_RE.finditer(cmd):
36
+ dockerfile = match.group(1) or match.group(2)
37
+ break
38
+
39
+ tokens = cmd.split()
40
+ context = "."
41
+ i = len(tokens) - 1
42
+ while i > 0:
43
+ token = tokens[i]
44
+ if token.startswith("-"):
45
+ i -= 1
46
+ continue
47
+ if i > 0 and tokens[i - 1] in ("-f", "--file"):
48
+ i -= 2
49
+ continue
50
+ if i > 0 and tokens[i - 1].startswith("--file="):
51
+ i -= 1
52
+ continue
53
+ context = token
54
+ break
55
+ i -= 1
56
+ return dockerfile, context
57
+
58
+
59
+ def match_local_src(
60
+ sync_mappings: list[tuple[str, str]], remote_context: str,
61
+ ) -> str | None:
62
+ for remote_dst, local_src in sync_mappings:
63
+ if not remote_dst.startswith("~/"):
64
+ continue
65
+ dst_clean = remote_dst.rstrip("/")
66
+ if remote_context == dst_clean or remote_context.startswith(dst_clean + "/"):
67
+ return local_src
68
+ return None
69
+
70
+
71
+ def resolve_local_dockerfile(local_src: str, dockerfile: str, base_dir: Path) -> Path | None:
72
+ src_path = Path(local_src.rstrip("/"))
73
+ if not src_path.is_absolute():
74
+ src_path = (base_dir / src_path).resolve()
75
+ if dockerfile.startswith("/"):
76
+ return Path(dockerfile)
77
+ return src_path / dockerfile
78
+
79
+
80
+ class DockerBuildChecker(Checker):
81
+ """Validate docker build commands: Dockerfile exists, context is consistent."""
82
+
83
+ def check(self, spec, document, base_dir, result):
84
+ sync_dests, sync_mappings = collect_sync_mappings(spec)
85
+
86
+ for step in spec.extra_steps:
87
+ cmd = step.get("command", "")
88
+ if not DOCKER_BUILD_RE.search(cmd):
89
+ continue
90
+ sid = step.get("id", "unknown")
91
+ dockerfile, context = parse_docker_build(cmd)
92
+ self._check_dockerfile(dockerfile, sid, base_dir, result)
93
+ if context.startswith("~/"):
94
+ self._check_remote_context(
95
+ context, dockerfile, sid, sync_dests, sync_mappings,
96
+ base_dir, result,
97
+ )
98
+
99
+ def _check_dockerfile(
100
+ self, dockerfile: str | None, sid: str, base_dir: Path, result: AnalysisResult,
101
+ ) -> None:
102
+ if not dockerfile or dockerfile.startswith("/") or dockerfile.startswith("~"):
103
+ return
104
+ df_path = base_dir / dockerfile
105
+ if not df_path.exists():
106
+ result.add(
107
+ IssueSeverity.ERROR, "docker_build",
108
+ f"Step '{sid}' docker build references missing Dockerfile: {dockerfile}",
109
+ sid,
110
+ suggestion=f"Create {df_path} or use standard 'Dockerfile' name.",
111
+ )
112
+ elif dockerfile not in STANDARD_DOCKERFILES:
113
+ result.add(
114
+ IssueSeverity.WARNING, "docker_build",
115
+ f"Step '{sid}' uses non-standard Dockerfile name: {dockerfile}",
116
+ sid,
117
+ suggestion="Consider using standard 'Dockerfile' with multi-arch support.",
118
+ )
119
+
120
+ def _check_remote_context(
121
+ self,
122
+ context: str,
123
+ dockerfile: str | None,
124
+ sid: str,
125
+ sync_dests: set[str],
126
+ sync_mappings: list[tuple[str, str]],
127
+ base_dir: Path,
128
+ result: AnalysisResult,
129
+ ) -> None:
130
+ context_clean = context.rstrip("/")
131
+ context_remote = context_clean[2:]
132
+ sync_remote_paths = {
133
+ d[2:].rstrip("/") for d in sync_dests if d.startswith("~/")
134
+ }
135
+ matched = any(
136
+ context_remote == sp or context_remote.startswith(sp + "/")
137
+ for sp in sync_remote_paths
138
+ )
139
+ if not matched and sync_dests:
140
+ result.add(
141
+ IssueSeverity.WARNING, "docker_build",
142
+ f"Step '{sid}' docker build uses remote context '{context}' "
143
+ f"that doesn't match any sync destination",
144
+ sid,
145
+ suggestion=f"Sync destinations: {', '.join(sorted(sync_dests))} — verify context path.",
146
+ )
147
+ if not dockerfile or not matched:
148
+ return
149
+ local_src = match_local_src(sync_mappings, context_clean)
150
+ if not local_src:
151
+ return
152
+ local_df = resolve_local_dockerfile(local_src, dockerfile, base_dir)
153
+ if local_df is not None and not local_df.exists():
154
+ result.add(
155
+ IssueSeverity.ERROR,
156
+ "docker_build",
157
+ f"Step '{sid}' Dockerfile '{dockerfile}' not found in synced local source '{local_src}'",
158
+ sid,
159
+ suggestion=f"Ensure {dockerfile} exists under {local_src} before sync/build.",
160
+ )
@@ -0,0 +1,23 @@
1
+ """Environment file checkers."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+
6
+ from ..models import AnalysisResult, IssueSeverity
7
+ from .base import Checker
8
+
9
+
10
+ class EnvFileChecker(Checker):
11
+ """Check that .env referenced by target.env_file exists."""
12
+
13
+ def check(self, spec, document, base_dir, result):
14
+ ef = spec.target.env_file or spec.source.env_file
15
+ if not ef:
16
+ return
17
+ path = base_dir / ef if not ef.startswith("/") else Path(ef)
18
+ if not path.exists():
19
+ result.add(
20
+ IssueSeverity.ERROR, "env",
21
+ f"env_file not found: {ef}",
22
+ suggestion=f"Create {path} or correct env_file path.",
23
+ )