redeploy 0.2.30__tar.gz → 0.2.32__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 (142) hide show
  1. {redeploy-0.2.30 → redeploy-0.2.32}/PKG-INFO +6 -6
  2. {redeploy-0.2.30 → redeploy-0.2.32}/README.md +5 -5
  3. {redeploy-0.2.30 → redeploy-0.2.32}/pyproject.toml +1 -1
  4. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/__init__.py +1 -1
  5. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/models.py +86 -0
  6. redeploy-0.2.32/redeploy/plugins/builtin/hardware_diagnostic.py +282 -0
  7. redeploy-0.2.32/redeploy/plugins/builtin/process_control.py +123 -0
  8. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/steps.py +34 -0
  9. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy.egg-info/PKG-INFO +6 -6
  10. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy.egg-info/SOURCES.txt +2 -0
  11. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/apply/__init__.py +0 -0
  12. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/apply/exceptions.py +0 -0
  13. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/apply/executor.py +0 -0
  14. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/apply/handlers.py +0 -0
  15. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/apply/progress.py +0 -0
  16. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/apply/rollback.py +0 -0
  17. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/apply/state.py +0 -0
  18. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/audit.py +0 -0
  19. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/cli/__init__.py +0 -0
  20. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/cli/commands/__init__.py +0 -0
  21. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/cli/commands/audit.py +0 -0
  22. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/cli/commands/detect.py +0 -0
  23. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/cli/commands/devices.py +0 -0
  24. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/cli/commands/diagnose.py +0 -0
  25. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/cli/commands/diff.py +0 -0
  26. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/cli/commands/exec_.py +0 -0
  27. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/cli/commands/export.py +0 -0
  28. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/cli/commands/import_.py +0 -0
  29. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/cli/commands/init.py +0 -0
  30. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/cli/commands/inspect.py +0 -0
  31. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/cli/commands/patterns.py +0 -0
  32. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/cli/commands/plan_apply.py +0 -0
  33. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/cli/commands/plugin.py +0 -0
  34. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/cli/commands/probe.py +0 -0
  35. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/cli/commands/state.py +0 -0
  36. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/cli/commands/status.py +0 -0
  37. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/cli/commands/target.py +0 -0
  38. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/cli/commands/version.py +0 -0
  39. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/cli/commands/workflow.py +0 -0
  40. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/cli/core.py +0 -0
  41. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/cli/display.py +0 -0
  42. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/cli.py +0 -0
  43. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/data_sync.py +0 -0
  44. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/detect/__init__.py +0 -0
  45. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/detect/detector.py +0 -0
  46. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/detect/probes.py +0 -0
  47. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/detect/remote.py +0 -0
  48. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/detect/templates.py +0 -0
  49. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/detect/workflow.py +0 -0
  50. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/discovery.py +0 -0
  51. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/dsl/__init__.py +0 -0
  52. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/dsl/loader.py +0 -0
  53. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/dsl/parser.py +0 -0
  54. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/dsl_python/__init__.py +0 -0
  55. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/dsl_python/context.py +0 -0
  56. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/dsl_python/decorators.py +0 -0
  57. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/dsl_python/docker_steps.py +0 -0
  58. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/dsl_python/exceptions.py +0 -0
  59. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/dsl_python/runner.py +0 -0
  60. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/dsl_python/steps.py +0 -0
  61. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/fleet.py +0 -0
  62. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/iac/__init__.py +0 -0
  63. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/iac/base.py +0 -0
  64. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/iac/docker_compose.py +0 -0
  65. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/iac/parsers/__init__.py +0 -0
  66. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/iac/parsers/compose.py +0 -0
  67. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/iac/registry.py +0 -0
  68. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/markpact/__init__.py +0 -0
  69. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/markpact/compiler.py +0 -0
  70. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/markpact/models.py +0 -0
  71. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/markpact/parser.py +0 -0
  72. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/observe.py +0 -0
  73. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/parse.py +0 -0
  74. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/patterns.py +0 -0
  75. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/plan/__init__.py +0 -0
  76. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/plan/planner.py +0 -0
  77. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/plugins/__init__.py +0 -0
  78. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/plugins/builtin/__init__.py +0 -0
  79. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/plugins/builtin/browser_reload.py +0 -0
  80. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/plugins/builtin/notify.py +0 -0
  81. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/plugins/builtin/systemd_reload.py +0 -0
  82. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/spec_loader.py +0 -0
  83. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/ssh.py +0 -0
  84. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/tests/__init__.py +0 -0
  85. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/tests/test_audit.py +0 -0
  86. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/tests/test_cli.py +0 -0
  87. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/tests/test_data_sync.py +0 -0
  88. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/tests/test_discovery.py +0 -0
  89. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/tests/test_dsl.py +0 -0
  90. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/tests/test_examples.py +0 -0
  91. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/tests/test_executor.py +0 -0
  92. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/tests/test_fleet.py +0 -0
  93. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/tests/test_git_integration.py +0 -0
  94. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/tests/test_iac.py +0 -0
  95. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/tests/test_manifest.py +0 -0
  96. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/tests/test_markpact_compiler.py +0 -0
  97. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/tests/test_markpact_parser.py +0 -0
  98. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/tests/test_models.py +0 -0
  99. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/tests/test_observe.py +0 -0
  100. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/tests/test_parse.py +0 -0
  101. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/tests/test_patterns.py +0 -0
  102. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/tests/test_planner_edge.py +0 -0
  103. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/tests/test_plugins.py +0 -0
  104. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/tests/test_probes.py +0 -0
  105. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/tests/test_public_api.py +0 -0
  106. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/tests/test_resume.py +0 -0
  107. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/tests/test_spec_loader.py +0 -0
  108. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/tests/test_ssh.py +0 -0
  109. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/tests/test_templates.py +0 -0
  110. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/tests/test_verify.py +0 -0
  111. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/tests/test_version.py +0 -0
  112. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/tests/test_version_changelog.py +0 -0
  113. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/tests/test_version_cli.py +0 -0
  114. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/tests/test_version_manifest.py +0 -0
  115. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/tests/test_version_sources.py +0 -0
  116. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/tests/test_workflow.py +0 -0
  117. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/verify.py +0 -0
  118. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/version/__init__.py +0 -0
  119. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/version/bump.py +0 -0
  120. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/version/changelog.py +0 -0
  121. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/version/commits.py +0 -0
  122. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/version/diff.py +0 -0
  123. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/version/git_integration.py +0 -0
  124. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/version/git_transaction.py +0 -0
  125. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/version/manifest.py +0 -0
  126. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/version/sources/__init__.py +0 -0
  127. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/version/sources/base.py +0 -0
  128. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/version/sources/json_.py +0 -0
  129. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/version/sources/plain.py +0 -0
  130. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/version/sources/regex.py +0 -0
  131. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/version/sources/toml_.py +0 -0
  132. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/version/sources/yaml_.py +0 -0
  133. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/version/transaction.py +0 -0
  134. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy/version.py +0 -0
  135. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy.egg-info/dependency_links.txt +0 -0
  136. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy.egg-info/entry_points.txt +0 -0
  137. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy.egg-info/requires.txt +0 -0
  138. {redeploy-0.2.30 → redeploy-0.2.32}/redeploy.egg-info/top_level.txt +0 -0
  139. {redeploy-0.2.30 → redeploy-0.2.32}/setup.cfg +0 -0
  140. {redeploy-0.2.30 → redeploy-0.2.32}/tests/test_iac.py +0 -0
  141. {redeploy-0.2.30 → redeploy-0.2.32}/tests/test_parse.py +0 -0
  142. {redeploy-0.2.30 → redeploy-0.2.32}/tests/test_redeploy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redeploy
3
- Version: 0.2.30
3
+ Version: 0.2.32
4
4
  Summary: Infrastructure migration toolkit: detect → plan → apply
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -27,17 +27,17 @@ Requires-Dist: pfix>=0.1.60; extra == "dev"
27
27
 
28
28
  ## AI Cost Tracking
29
29
 
30
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.30-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
31
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-15.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
30
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.32-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
31
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-17.5h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
32
32
 
33
- - 🤖 **LLM usage:** $7.5000 (50 commits)
34
- - 👤 **Human dev:** ~$1505 (15.0h @ $100/h, 30min dedup)
33
+ - 🤖 **LLM usage:** $7.5000 (52 commits)
34
+ - 👤 **Human dev:** ~$1755 (17.5h @ $100/h, 30min dedup)
35
35
 
36
36
  Generated on 2026-04-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
37
37
 
38
38
  ---
39
39
 
40
- ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.30-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
40
+ ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.32-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
41
41
 
42
42
  Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
43
43
 
@@ -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.30-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-15.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.32-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-17.5h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $7.5000 (50 commits)
10
- - 👤 **Human dev:** ~$1505 (15.0h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $7.5000 (52 commits)
10
+ - 👤 **Human dev:** ~$1755 (17.5h @ $100/h, 30min dedup)
11
11
 
12
12
  Generated on 2026-04-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
13
13
 
14
14
  ---
15
15
 
16
- ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.30-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.32-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.30"
7
+ version = "0.2.32"
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.30"
10
+ __version__ = "0.2.32"
11
11
 
12
12
  # ── Core models ───────────────────────────────────────────────────────────────
13
13
  from .models import ( # noqa: F401
@@ -120,6 +120,92 @@ class AppHealthInfo(BaseModel):
120
120
  response_ms: Optional[float] = None
121
121
 
122
122
 
123
+ # ── Hardware (output of hardware probe) ───────────────────────────────────────
124
+
125
+ class DrmOutput(BaseModel):
126
+ """One DRM connector (e.g. card1-DSI-2, card2-HDMI-A-1)."""
127
+ name: str # e.g. "card1-DSI-2"
128
+ connector: str # e.g. "DSI-2", "HDMI-A-1"
129
+ status: str = "unknown" # connected / disconnected
130
+ enabled: str = "unknown" # enabled / disabled
131
+ modes: list[str] = Field(default_factory=list) # e.g. ["1280x800@60"]
132
+ transform: str = "normal"
133
+ position: str = "0,0"
134
+ scale: str = "1.0"
135
+
136
+
137
+ class BacklightInfo(BaseModel):
138
+ """Sysfs backlight device."""
139
+ name: str # e.g. "11-0045"
140
+ brightness: int = 0
141
+ max_brightness: int = 255
142
+ bl_power: int = 0 # 0=on, 4=off
143
+ display_name: Optional[str] = None # e.g. "DSI-2"
144
+
145
+
146
+ class I2CBusInfo(BaseModel):
147
+ bus: int
148
+ devices: list[str] = Field(default_factory=list) # hex addresses found
149
+
150
+
151
+ class HardwareDiagnostic(BaseModel):
152
+ """Problem found during hardware probe."""
153
+ component: str # "dsi", "backlight", "i2c", "gpio", "overlay"
154
+ severity: str = "warning" # info / warning / error / critical
155
+ message: str
156
+ fix: Optional[str] = None # suggested fix command or action
157
+
158
+
159
+ class HardwareInfo(BaseModel):
160
+ """Hardware state produced by hardware probe."""
161
+ board: Optional[str] = None # e.g. "Raspberry Pi 5 Model B Rev 1.0"
162
+ kernel: Optional[str] = None # uname -r
163
+ config_txt: str = "" # full /boot/firmware/config.txt
164
+
165
+ # Display
166
+ drm_outputs: list[DrmOutput] = Field(default_factory=list)
167
+ backlights: list[BacklightInfo] = Field(default_factory=list)
168
+ framebuffers: list[str] = Field(default_factory=list) # /dev/fb* names
169
+ wlr_outputs: list[dict] = Field(default_factory=list) # raw wlr-randr output
170
+
171
+ # DSI specific
172
+ dsi_overlays: list[str] = Field(default_factory=list) # active dtoverlay lines
173
+ dsi_dmesg: list[str] = Field(default_factory=list) # relevant dmesg lines
174
+
175
+ # I2C
176
+ i2c_buses: list[I2CBusInfo] = Field(default_factory=list)
177
+
178
+ # GPIO header
179
+ gpio_4pin_detected: Optional[bool] = None # 5V/GND/SDA/SCL header present
180
+
181
+ # Diagnostics
182
+ diagnostics: list[HardwareDiagnostic] = Field(default_factory=list)
183
+
184
+ @property
185
+ def has_dsi(self) -> bool:
186
+ return any("DSI" in o.name for o in self.drm_outputs)
187
+
188
+ @property
189
+ def dsi_connected(self) -> bool:
190
+ return any("DSI" in o.name and o.status == "connected" for o in self.drm_outputs)
191
+
192
+ @property
193
+ def dsi_enabled(self) -> bool:
194
+ return any("DSI" in o.name and o.enabled == "enabled" for o in self.drm_outputs)
195
+
196
+ @property
197
+ def backlight_on(self) -> bool:
198
+ return any(b.bl_power == 0 and b.brightness > 0 for b in self.backlights)
199
+
200
+ @property
201
+ def errors(self) -> list[HardwareDiagnostic]:
202
+ return [d for d in self.diagnostics if d.severity in ("error", "critical")]
203
+
204
+ @property
205
+ def warnings(self) -> list[HardwareDiagnostic]:
206
+ return [d for d in self.diagnostics if d.severity == "warning"]
207
+
208
+
123
209
  class InfraState(BaseModel):
124
210
  """Full detected state of infrastructure — output of `detect`."""
125
211
  host: str # "root@1.2.3.4" or "local"
@@ -0,0 +1,282 @@
1
+ """Hardware diagnostic plugin for redeploy.
2
+
3
+ Analyzes system hardware and provides configuration recommendations.
4
+ Supports Raspberry Pi variants and desktop platforms.
5
+
6
+ Usage in deployment.yaml:
7
+ extra_steps:
8
+ - id: analyze_hardware
9
+ action: plugin
10
+ plugin_type: hardware_diagnostic
11
+ plugin_params:
12
+ checks: ["platform", "cpu", "memory", "storage", "gpio", "usb", "audio"]
13
+ platform: "auto" # or "rpi3", "rpi4", "rpi5", "desktop"
14
+ verbose: true
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import os
19
+ import subprocess
20
+ from dataclasses import dataclass
21
+ from typing import Any, Dict, List, Optional
22
+
23
+ from loguru import logger
24
+
25
+ from redeploy.plugins import PluginContext, register_plugin
26
+
27
+
28
+ @dataclass
29
+ class HardwareInfo:
30
+ """Hardware diagnostic information."""
31
+ platform: str
32
+ cpu_model: str
33
+ cpu_cores: int
34
+ memory_mb: int
35
+ storage_gb: float
36
+ gpio_available: bool
37
+ usb_devices: List[str]
38
+ audio_devices: List[str]
39
+ network_interfaces: List[str]
40
+ issues: List[str]
41
+ recommendations: List[str]
42
+
43
+
44
+ def _run_command(cmd: str) -> str:
45
+ """Run shell command and return output."""
46
+ try:
47
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=10)
48
+ return result.stdout.strip()
49
+ except Exception as e:
50
+ logger.debug(f"Command failed: {cmd} - {e}")
51
+ return ""
52
+
53
+
54
+ def _detect_platform() -> str:
55
+ """Detect the platform type."""
56
+ # Check for Raspberry Pi
57
+ model_file = "/proc/device-tree/model"
58
+ if os.path.exists(model_file):
59
+ try:
60
+ with open(model_file, "r") as f:
61
+ model = f.read().strip("\x00")
62
+ if "Raspberry Pi 5" in model:
63
+ return "rpi5"
64
+ elif "Raspberry Pi 4" in model:
65
+ return "rpi4"
66
+ elif "Raspberry Pi 3" in model:
67
+ return "rpi3"
68
+ except Exception:
69
+ pass
70
+
71
+ # Check CPU info
72
+ cpuinfo = _run_command("cat /proc/cpuinfo")
73
+ if "Raspberry Pi" in cpuinfo or "BCM" in cpuinfo:
74
+ return "rpi_unknown"
75
+
76
+ # Check architecture
77
+ arch = _run_command("uname -m")
78
+ if arch == "aarch64":
79
+ return "arm64_unknown"
80
+ elif arch == "x86_64":
81
+ return "desktop"
82
+
83
+ return "unknown"
84
+
85
+
86
+ def _get_cpu_info() -> tuple[str, int]:
87
+ """Get CPU model and core count."""
88
+ cpuinfo = _run_command("cat /proc/cpuinfo")
89
+ model = "unknown"
90
+ cores = 0
91
+
92
+ for line in cpuinfo.split("\n"):
93
+ if line.startswith("model name"):
94
+ model = line.split(":", 1)[1].strip()
95
+ elif line.startswith("Hardware"):
96
+ model = line.split(":", 1)[1].strip()
97
+ elif line.startswith("processor"):
98
+ cores += 1
99
+
100
+ return model, max(cores, 1)
101
+
102
+
103
+ def _get_memory_mb() -> int:
104
+ """Get total memory in MB."""
105
+ meminfo = _run_command("cat /proc/meminfo")
106
+ for line in meminfo.split("\n"):
107
+ if line.startswith("MemTotal"):
108
+ kb = int(line.split()[1])
109
+ return kb // 1024
110
+ return 0
111
+
112
+
113
+ def _get_storage_gb() -> float:
114
+ """Get available storage in GB."""
115
+ output = _run_command("df -h /")
116
+ if output:
117
+ lines = output.split("\n")
118
+ if len(lines) > 1:
119
+ parts = lines[1].split()
120
+ if len(parts) > 3:
121
+ available = parts[3]
122
+ if available.endswith("G"):
123
+ return float(available.rstrip("G"))
124
+ elif available.endswith("M"):
125
+ return float(available.rstrip("M")) / 1024
126
+ return 0.0
127
+
128
+
129
+ def _check_gpio() -> bool:
130
+ """Check if GPIO is available."""
131
+ # Check for GPIO character device
132
+ return os.path.exists("/dev/gpiochip0") or os.path.exists("/dev/gpiomem")
133
+
134
+
135
+ def _get_usb_devices() -> List[str]:
136
+ """Get list of USB devices."""
137
+ output = _run_command("lsusb")
138
+ devices = []
139
+ for line in output.split("\n"):
140
+ if line:
141
+ parts = line.split(" ", 6)
142
+ if len(parts) > 6:
143
+ devices.append(parts[6])
144
+ return devices
145
+
146
+
147
+ def _get_audio_devices() -> List[str]:
148
+ """Get list of audio devices."""
149
+ output = _run_command("aplay -l")
150
+ devices = []
151
+ for line in output.split("\n"):
152
+ if line.startswith("card"):
153
+ devices.append(line.strip())
154
+ return devices
155
+
156
+
157
+ def _get_network_interfaces() -> List[str]:
158
+ """Get list of network interfaces."""
159
+ output = _run_command("ip -o link show")
160
+ interfaces = []
161
+ for line in output.split("\n"):
162
+ if line:
163
+ parts = line.split(":")
164
+ if len(parts) > 1:
165
+ iface = parts[1].strip()
166
+ if iface != "lo":
167
+ interfaces.append(iface)
168
+ return interfaces
169
+
170
+
171
+ def _analyze_hardware(platform: str, verbose: bool = False) -> HardwareInfo:
172
+ """Perform comprehensive hardware analysis."""
173
+ platform = platform if platform != "auto" else _detect_platform()
174
+ cpu_model, cpu_cores = _get_cpu_info()
175
+ memory_mb = _get_memory_mb()
176
+ storage_gb = _get_storage_gb()
177
+ gpio_available = _check_gpio()
178
+ usb_devices = _get_usb_devices()
179
+ audio_devices = _get_audio_devices()
180
+ network_interfaces = _get_network_interfaces()
181
+
182
+ issues = []
183
+ recommendations = []
184
+
185
+ # Platform-specific checks
186
+ if platform.startswith("rpi"):
187
+ if memory_mb < 1024:
188
+ issues.append(f"Low memory: {memory_mb}MB (recommended: >= 2GB for RPi)")
189
+ recommendations.append("Consider upgrading to RPi4/5 with 2GB+ RAM")
190
+ if storage_gb < 5:
191
+ issues.append(f"Low storage: {storage_gb}GB (recommended: >= 8GB)")
192
+ recommendations.append("Free up disk space or use larger SD card")
193
+
194
+ if platform == "rpi5":
195
+ if not gpio_available:
196
+ issues.append("GPIO not available on RPi5")
197
+ recommendations.append("Check kernel configuration for GPIO support")
198
+ elif platform in ["rpi3", "rpi4"]:
199
+ if not gpio_available:
200
+ issues.append("GPIO not available")
201
+ recommendations.append("Enable GPIO in config.txt or check permissions")
202
+
203
+ elif platform == "desktop":
204
+ if memory_mb < 4096:
205
+ issues.append(f"Low memory: {memory_mb}MB (recommended: >= 4GB for desktop)")
206
+ recommendations.append("Consider upgrading RAM for better performance")
207
+
208
+ # General checks
209
+ if not network_interfaces:
210
+ issues.append("No network interfaces detected")
211
+ recommendations.append("Check network configuration")
212
+
213
+ if verbose:
214
+ logger.info(f"Platform: {platform}")
215
+ logger.info(f"CPU: {cpu_model} ({cpu_cores} cores)")
216
+ logger.info(f"Memory: {memory_mb}MB")
217
+ logger.info(f"Storage: {storage_gb}GB available")
218
+ logger.info(f"GPIO: {'Available' if gpio_available else 'Not available'}")
219
+ logger.info(f"USB devices: {len(usb_devices)}")
220
+ logger.info(f"Audio devices: {len(audio_devices)}")
221
+ logger.info(f"Network interfaces: {network_interfaces}")
222
+
223
+ return HardwareInfo(
224
+ platform=platform,
225
+ cpu_model=cpu_model,
226
+ cpu_cores=cpu_cores,
227
+ memory_mb=memory_mb,
228
+ storage_gb=storage_gb,
229
+ gpio_available=gpio_available,
230
+ usb_devices=usb_devices,
231
+ audio_devices=audio_devices,
232
+ network_interfaces=network_interfaces,
233
+ issues=issues,
234
+ recommendations=recommendations,
235
+ )
236
+
237
+
238
+ @register_plugin("hardware_diagnostic")
239
+ def hardware_diagnostic(ctx: PluginContext) -> None:
240
+ """Perform hardware diagnostics and provide recommendations."""
241
+ checks = ctx.params.get("checks", ["platform", "cpu", "memory", "storage"])
242
+ platform = ctx.params.get("platform", "auto")
243
+ verbose = ctx.params.get("verbose", True)
244
+ dry_run = ctx.dry_run
245
+
246
+ if dry_run:
247
+ logger.info("[DRY RUN] Would analyze hardware")
248
+ ctx.step.status = "done"
249
+ ctx.step.result = "dry_run"
250
+ return
251
+
252
+ logger.info("Starting hardware diagnostic...")
253
+
254
+ hw_info = _analyze_hardware(platform, verbose)
255
+
256
+ # Build result message
257
+ result_parts = []
258
+ result_parts.append(f"Platform: {hw_info.platform}")
259
+ result_parts.append(f"CPU: {hw_info.cpu_model} ({hw_info.cpu_cores} cores)")
260
+ result_parts.append(f"Memory: {hw_info.memory_mb}MB")
261
+ result_parts.append(f"Storage: {hw_info.storage_gb}GB")
262
+ result_parts.append(f"GPIO: {'OK' if hw_info.gpio_available else 'N/A'}")
263
+
264
+ if hw_info.issues:
265
+ result_parts.append(f"Issues: {len(hw_info.issues)}")
266
+ for issue in hw_info.issues:
267
+ logger.warning(f" - {issue}")
268
+
269
+ if hw_info.recommendations:
270
+ result_parts.append(f"Recommendations: {len(hw_info.recommendations)}")
271
+ for rec in hw_info.recommendations:
272
+ logger.info(f" - {rec}")
273
+
274
+ ctx.step.result = "; ".join(result_parts)
275
+ ctx.step.description = f"Hardware diagnostic: {hw_info.platform}, {hw_info.memory_mb}MB RAM"
276
+
277
+ if hw_info.issues:
278
+ ctx.step.status = "done" # Still done, but with warnings
279
+ logger.warning(f"Hardware diagnostic completed with {len(hw_info.issues)} issues")
280
+ else:
281
+ ctx.step.status = "done"
282
+ logger.info("Hardware diagnostic completed successfully")
@@ -0,0 +1,123 @@
1
+ """Process control plugin for redeploy.
2
+
3
+ Automatically kills processes on specified ports before deployment.
4
+ Supports port ranges, process name filtering, and graceful vs force kill.
5
+
6
+ Usage in deployment.yaml:
7
+ extra_steps:
8
+ - id: kill_processes_on_ports
9
+ action: plugin
10
+ plugin_type: process_control
11
+ plugin_params:
12
+ ports: [8100, 8101, 8202]
13
+ strategy: graceful # or 'force'
14
+ timeout: 10
15
+ notify: true
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import socket
20
+ import time
21
+ from typing import Optional
22
+
23
+ from loguru import logger
24
+
25
+ from redeploy.models import ConflictSeverity, MigrationStep
26
+ from redeploy.plugins import PluginContext, register_plugin
27
+
28
+
29
+ def _find_pid_on_port(port: int) -> Optional[int]:
30
+ """Find PID of process listening on port (Linux only)."""
31
+ try:
32
+ with open("/proc/net/tcp", "r") as f:
33
+ for line in f:
34
+ parts = line.strip().split()
35
+ if len(parts) >= 10:
36
+ # Format: local_addr rem_addr st tx_queue rx_queue ...
37
+ # local_addr format: hex_ip:hex_port
38
+ local_addr = parts[1].split(":")[-1]
39
+ hex_port = int(local_addr, 16)
40
+ if hex_port == port:
41
+ inode = parts[9]
42
+ # Find PID by inode in /proc/*/fd
43
+ import os
44
+ for pid_dir in os.listdir("/proc"):
45
+ if pid_dir.isdigit():
46
+ fd_dir = f"/proc/{pid_dir}/fd"
47
+ if os.path.exists(fd_dir):
48
+ for fd in os.listdir(fd_dir):
49
+ try:
50
+ link = os.readlink(os.path.join(fd_dir, fd))
51
+ if f"socket:[{inode}]" in link:
52
+ return int(pid_dir)
53
+ except (OSError, ValueError):
54
+ continue
55
+ except Exception as e:
56
+ logger.debug(f"Could not find PID on port {port}: {e}")
57
+ return None
58
+
59
+
60
+ def _kill_process(pid: int, force: bool = False) -> bool:
61
+ """Kill a process by PID. Returns True if successful."""
62
+ try:
63
+ import os
64
+ import signal
65
+ sig = signal.SIGKILL if force else signal.SIGTERM
66
+ os.kill(pid, sig)
67
+ return True
68
+ except ProcessLookupError:
69
+ return False
70
+ except PermissionError:
71
+ logger.warning(f"Permission denied to kill PID {pid}")
72
+ return False
73
+
74
+
75
+ @register_plugin("process_control")
76
+ def process_control(ctx: PluginContext) -> None:
77
+ """Kill processes on specified ports."""
78
+ ports = ctx.params.get("ports", [])
79
+ strategy = ctx.params.get("strategy", "graceful")
80
+ timeout = ctx.params.get("timeout", 10)
81
+ notify = ctx.params.get("notify", True)
82
+ dry_run = ctx.dry_run
83
+
84
+ if not ports:
85
+ logger.warning("process_control: no ports specified")
86
+ ctx.step.status = "done"
87
+ ctx.step.result = "no_ports"
88
+ return
89
+
90
+ if dry_run:
91
+ logger.info(f"[DRY RUN] Would kill processes on ports: {ports}")
92
+ ctx.step.status = "done"
93
+ ctx.step.result = "dry_run"
94
+ return
95
+
96
+ killed = []
97
+ for port in ports:
98
+ pid = _find_pid_on_port(port)
99
+ if pid:
100
+ logger.info(f"Found process PID {pid} on port {port}")
101
+ if strategy == "graceful":
102
+ _kill_process(pid, force=False)
103
+ # Wait for graceful shutdown
104
+ time.sleep(2)
105
+ # Check if still running
106
+ if _find_pid_on_port(port) == pid:
107
+ logger.warning(f"Process {pid} on port {port} did not stop gracefully, force killing")
108
+ _kill_process(pid, force=True)
109
+ else:
110
+ _kill_process(pid, force=True)
111
+ killed.append((port, pid))
112
+ else:
113
+ logger.debug(f"No process found on port {port}")
114
+
115
+ if killed:
116
+ ctx.step.result = f"killed:{len(killed)}_processes"
117
+ if notify:
118
+ ctx.step.description = f"Killed {len(killed)} process(es) on ports {ports}"
119
+ else:
120
+ ctx.step.result = "no_processes_to_kill"
121
+ ctx.step.description = f"No processes found on ports {ports}"
122
+
123
+ ctx.step.status = "done"
@@ -218,6 +218,37 @@ _GIT_PULL = _step(
218
218
  rollback_command="git -C ~/app reset --hard HEAD@{1}",
219
219
  )
220
220
 
221
+ # ── process control ─────────────────────────────────────────────────────────────
222
+
223
+ _KILL_PROCESSES_ON_PORTS = _step(
224
+ id="kill_processes_on_ports",
225
+ action=StepAction.PLUGIN,
226
+ plugin_type="process_control",
227
+ description="Kill processes on specified ports (auto-detects PIDs)",
228
+ plugin_params={"ports": [], "strategy": "graceful", "timeout": 10, "notify": True},
229
+ risk=ConflictSeverity.LOW,
230
+ )
231
+
232
+ _KILL_DEV_PROCESSES = _step(
233
+ id="kill_dev_processes",
234
+ action=StepAction.PLUGIN,
235
+ plugin_type="process_control",
236
+ description="Kill common dev server processes (vite, uvicorn, python)",
237
+ plugin_params={"ports": [8100, 8101, 8202, 3000, 5173], "strategy": "graceful", "timeout": 5, "notify": True},
238
+ risk=ConflictSeverity.LOW,
239
+ )
240
+
241
+ # ── hardware diagnostic ─────────────────────────────────────────────────────────
242
+
243
+ _HARDWARE_DIAGNOSTIC = _step(
244
+ id="hardware_diagnostic",
245
+ action=StepAction.PLUGIN,
246
+ plugin_type="hardware_diagnostic",
247
+ description="Analyze system hardware and provide configuration recommendations",
248
+ plugin_params={"checks": ["platform", "cpu", "memory", "storage"], "platform": "auto", "verbose": True},
249
+ risk=ConflictSeverity.LOW,
250
+ )
251
+
221
252
  # ── registry ──────────────────────────────────────────────────────────────────
222
253
 
223
254
  _LIBRARY: dict[str, MigrationStep] = {
@@ -241,6 +272,9 @@ _LIBRARY: dict[str, MigrationStep] = {
241
272
  _SYSTEMCTL_RESTART,
242
273
  _SYSTEMCTL_DAEMON_RELOAD,
243
274
  _GIT_PULL,
275
+ _KILL_PROCESSES_ON_PORTS,
276
+ _KILL_DEV_PROCESSES,
277
+ _HARDWARE_DIAGNOSTIC,
244
278
  ]
245
279
  }
246
280
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redeploy
3
- Version: 0.2.30
3
+ Version: 0.2.32
4
4
  Summary: Infrastructure migration toolkit: detect → plan → apply
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -27,17 +27,17 @@ Requires-Dist: pfix>=0.1.60; extra == "dev"
27
27
 
28
28
  ## AI Cost Tracking
29
29
 
30
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.30-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
31
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-15.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
30
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.32-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
31
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-17.5h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
32
32
 
33
- - 🤖 **LLM usage:** $7.5000 (50 commits)
34
- - 👤 **Human dev:** ~$1505 (15.0h @ $100/h, 30min dedup)
33
+ - 🤖 **LLM usage:** $7.5000 (52 commits)
34
+ - 👤 **Human dev:** ~$1755 (17.5h @ $100/h, 30min dedup)
35
35
 
36
36
  Generated on 2026-04-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
37
37
 
38
38
  ---
39
39
 
40
- ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.30-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
40
+ ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.32-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
41
41
 
42
42
  Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
43
43
 
@@ -82,7 +82,9 @@ redeploy/plan/planner.py
82
82
  redeploy/plugins/__init__.py
83
83
  redeploy/plugins/builtin/__init__.py
84
84
  redeploy/plugins/builtin/browser_reload.py
85
+ redeploy/plugins/builtin/hardware_diagnostic.py
85
86
  redeploy/plugins/builtin/notify.py
87
+ redeploy/plugins/builtin/process_control.py
86
88
  redeploy/plugins/builtin/systemd_reload.py
87
89
  redeploy/tests/__init__.py
88
90
  redeploy/tests/test_audit.py
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes