redeploy 0.2.35__tar.gz → 0.2.37__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.
- {redeploy-0.2.35 → redeploy-0.2.37}/PKG-INFO +6 -6
- {redeploy-0.2.35 → redeploy-0.2.37}/README.md +5 -5
- {redeploy-0.2.35 → redeploy-0.2.37}/pyproject.toml +1 -1
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/__init__.py +1 -1
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/apply/executor.py +3 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/apply/handlers.py +64 -90
- redeploy-0.2.37/redeploy/apply/utils/__init__.py +6 -0
- redeploy-0.2.37/redeploy/apply/utils/run_container_build.py +69 -0
- redeploy-0.2.37/redeploy/cli/commands/hardware.py +314 -0
- redeploy-0.2.37/redeploy/cli/commands/version/__init__.py +6 -0
- redeploy-0.2.37/redeploy/cli/commands/version/commands.py +594 -0
- redeploy-0.2.37/redeploy/cli/commands/version/helpers.py +349 -0
- redeploy-0.2.37/redeploy/cli/commands/version/monorepo.py +139 -0
- redeploy-0.2.37/redeploy/cli/commands/version/release.py +153 -0
- redeploy-0.2.37/redeploy/cli/commands/version/scanner.py +262 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/display.py +114 -74
- redeploy-0.2.37/redeploy/detect/builtin/__init__.py +2 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/detect/hardware.py +12 -171
- redeploy-0.2.37/redeploy/detect/hardware_rules.py +262 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/detect/templates.py +84 -170
- redeploy-0.2.37/redeploy/hardware/__init__.py +8 -0
- redeploy-0.2.37/redeploy/hardware/config_txt.py +88 -0
- redeploy-0.2.37/redeploy/hardware/data/__init__.py +4 -0
- redeploy-0.2.37/redeploy/hardware/data/hyperpixel.py +41 -0
- redeploy-0.2.37/redeploy/hardware/data/official.py +18 -0
- redeploy-0.2.37/redeploy/hardware/data/waveshare.py +48 -0
- redeploy-0.2.37/redeploy/hardware/fixes.py +203 -0
- redeploy-0.2.37/redeploy/hardware/panels.py +51 -0
- redeploy-0.2.37/redeploy/hardware/raspi_config.py +32 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/models.py +47 -32
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/version/sources/base.py +5 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/version/sources/json_.py +0 -4
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/version/sources/plain.py +0 -4
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/version/sources/regex.py +0 -4
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/version/sources/toml_.py +0 -4
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/version/sources/yaml_.py +0 -4
- redeploy-0.2.37/redeploy/version.py +23 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy.egg-info/PKG-INFO +6 -6
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy.egg-info/SOURCES.txt +21 -1
- redeploy-0.2.37/tests/test_hardware_config.py +162 -0
- redeploy-0.2.37/tests/test_hardware_rules.py +189 -0
- redeploy-0.2.35/redeploy/cli/commands/hardware.py +0 -230
- redeploy-0.2.35/redeploy/cli/commands/version.py +0 -1405
- redeploy-0.2.35/redeploy/version.py +0 -96
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/apply/__init__.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/apply/exceptions.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/apply/progress.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/apply/rollback.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/apply/state.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/audit.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/blueprint/__init__.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/blueprint/extractor.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/blueprint/generators/__init__.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/blueprint/generators/docker_compose.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/blueprint/generators/migration.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/__init__.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/__init__.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/audit.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/blueprint.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/detect.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/device_map.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/devices.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/diagnose.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/diff.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/exec_.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/export.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/import_.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/init.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/inspect.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/patterns.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/plan_apply.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/plugin.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/probe.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/state.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/status.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/target.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/commands/workflow.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli/core.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/cli.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/data_sync.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/detect/__init__.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/detect/detector.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/detect/probes.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/detect/remote.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/detect/workflow.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/discovery.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/dsl/__init__.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/dsl/loader.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/dsl/parser.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/dsl_python/__init__.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/dsl_python/context.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/dsl_python/decorators.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/dsl_python/docker_steps.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/dsl_python/exceptions.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/dsl_python/runner.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/dsl_python/steps.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/fleet.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/iac/__init__.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/iac/base.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/iac/docker_compose.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/iac/parsers/__init__.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/iac/parsers/compose.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/iac/registry.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/markpact/__init__.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/markpact/compiler.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/markpact/models.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/markpact/parser.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/observe.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/parse.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/patterns.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/plan/__init__.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/plan/planner.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/plugins/__init__.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/plugins/builtin/__init__.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/plugins/builtin/browser_reload.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/plugins/builtin/hardware_diagnostic.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/plugins/builtin/notify.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/plugins/builtin/process_control.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/plugins/builtin/systemd_reload.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/spec_loader.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/ssh.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/steps.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/__init__.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_audit.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_cli.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_data_sync.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_discovery.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_dsl.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_examples.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_executor.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_fleet.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_git_integration.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_iac.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_manifest.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_markpact_compiler.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_markpact_parser.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_models.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_observe.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_parse.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_patterns.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_planner_edge.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_plugins.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_probes.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_public_api.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_resume.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_spec_loader.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_ssh.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_templates.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_verify.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_version.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_version_changelog.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_version_cli.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_version_manifest.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_version_sources.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/tests/test_workflow.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/verify.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/version/__init__.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/version/bump.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/version/changelog.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/version/commits.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/version/diff.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/version/git_integration.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/version/git_transaction.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/version/manifest.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/version/sources/__init__.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy/version/transaction.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy.egg-info/dependency_links.txt +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy.egg-info/entry_points.txt +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy.egg-info/requires.txt +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/redeploy.egg-info/top_level.txt +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/setup.cfg +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/tests/test_iac.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/tests/test_parse.py +0 -0
- {redeploy-0.2.35 → redeploy-0.2.37}/tests/test_redeploy.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: redeploy
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.37
|
|
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
|
-
     
|
|
31
|
+
  
|
|
32
32
|
|
|
33
|
-
- 🤖 **LLM usage:** $7.5000 (
|
|
34
|
-
- 👤 **Human dev:** ~$
|
|
33
|
+
- 🤖 **LLM usage:** $7.5000 (58 commits)
|
|
34
|
+
- 👤 **Human dev:** ~$1859 (18.6h @ $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
|
-
    
|
|
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
|
-
     
|
|
7
|
+
  
|
|
8
8
|
|
|
9
|
-
- 🤖 **LLM usage:** $7.5000 (
|
|
10
|
-
- 👤 **Human dev:** ~$
|
|
9
|
+
- 🤖 **LLM usage:** $7.5000 (58 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$1859 (18.6h @ $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
|
-
    
|
|
17
17
|
|
|
18
18
|
Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
|
|
19
19
|
|
|
@@ -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.
|
|
10
|
+
__version__ = "0.2.37"
|
|
11
11
|
|
|
12
12
|
# ── Core models ───────────────────────────────────────────────────────────────
|
|
13
13
|
from .models import ( # noqa: F401
|
|
@@ -15,6 +15,7 @@ from .handlers import (
|
|
|
15
15
|
run_ssh, run_scp, run_rsync, run_docker_build, run_podman_build,
|
|
16
16
|
run_docker_health_wait, run_container_log_tail, run_http_check,
|
|
17
17
|
run_version_check, run_plugin, run_wait, run_inline_script,
|
|
18
|
+
run_ensure_config_line, run_raspi_config,
|
|
18
19
|
)
|
|
19
20
|
from .progress import ProgressEmitter
|
|
20
21
|
from .rollback import rollback_steps
|
|
@@ -227,6 +228,8 @@ class Executor:
|
|
|
227
228
|
StepAction.WAIT: lambda s: run_wait(s),
|
|
228
229
|
StepAction.PLUGIN: lambda s: run_plugin(s, self.probe, self.plan, self._emitter, self.dry_run),
|
|
229
230
|
StepAction.INLINE_SCRIPT: lambda s: run_inline_script(s, self.probe, self.plan),
|
|
231
|
+
StepAction.ENSURE_CONFIG_LINE: lambda s: run_ensure_config_line(s, self.probe),
|
|
232
|
+
StepAction.RASPI_CONFIG: lambda s: run_raspi_config(s, self.probe),
|
|
230
233
|
}
|
|
231
234
|
|
|
232
235
|
handler = dispatch.get(step.action)
|
|
@@ -77,51 +77,8 @@ def run_docker_build(
|
|
|
77
77
|
emitter: ProgressEmitter | None,
|
|
78
78
|
) -> None:
|
|
79
79
|
"""Run docker compose build on remote with periodic progress polling."""
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
raise StepError(step, "No command specified")
|
|
83
|
-
|
|
84
|
-
timeout = step.timeout or 1800 # 30 min default for first ARM64 build
|
|
85
|
-
poll_interval = 15 # seconds between progress snapshots
|
|
86
|
-
done_event = threading.Event()
|
|
87
|
-
result_holder: list = []
|
|
88
|
-
|
|
89
|
-
def _ssh_build() -> None:
|
|
90
|
-
r = probe.run(cmd, timeout=timeout)
|
|
91
|
-
result_holder.append(r)
|
|
92
|
-
done_event.set()
|
|
93
|
-
|
|
94
|
-
thread = threading.Thread(target=_ssh_build, daemon=True)
|
|
95
|
-
thread.start()
|
|
96
|
-
|
|
97
|
-
elapsed = 0
|
|
98
|
-
while not done_event.wait(timeout=poll_interval):
|
|
99
|
-
elapsed += poll_interval
|
|
100
|
-
snap = probe.run(
|
|
101
|
-
"docker system df 2>/dev/null | grep -E 'Image|Cache' | "
|
|
102
|
-
"awk '{print $1, $3, $4}'",
|
|
103
|
-
timeout=10,
|
|
104
|
-
)
|
|
105
|
-
if snap.ok and snap.out.strip():
|
|
106
|
-
lines = " | ".join(snap.out.strip().splitlines())
|
|
107
|
-
msg = f"[{elapsed}s] build cache: {lines}"
|
|
108
|
-
logger.debug(f" {msg}")
|
|
109
|
-
if emitter:
|
|
110
|
-
emitter.progress(step.id, msg)
|
|
111
|
-
else:
|
|
112
|
-
msg = f"[{elapsed}s] build in progress..."
|
|
113
|
-
logger.debug(f" {msg}")
|
|
114
|
-
if emitter:
|
|
115
|
-
emitter.progress(step.id, msg)
|
|
116
|
-
|
|
117
|
-
if not result_holder:
|
|
118
|
-
raise StepError(step, "Build thread did not return a result")
|
|
119
|
-
r = result_holder[0]
|
|
120
|
-
step.result = r.out[:500]
|
|
121
|
-
if not r.ok:
|
|
122
|
-
raise StepError(step, f"exit={r.exit_code}: {r.stderr[:300]}")
|
|
123
|
-
logger.debug(f" build completed in {elapsed + poll_interval}s")
|
|
124
|
-
step.status = StepStatus.DONE
|
|
80
|
+
from .utils import run_container_build
|
|
81
|
+
run_container_build(step, probe, emitter, engine="docker")
|
|
125
82
|
|
|
126
83
|
|
|
127
84
|
def run_podman_build(
|
|
@@ -130,51 +87,8 @@ def run_podman_build(
|
|
|
130
87
|
emitter: ProgressEmitter | None,
|
|
131
88
|
) -> None:
|
|
132
89
|
"""Run podman build on remote with periodic progress polling."""
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
raise StepError(step, "No command specified")
|
|
136
|
-
|
|
137
|
-
timeout = step.timeout or 1800 # 30 min default for ARM64 builds
|
|
138
|
-
poll_interval = 15 # seconds between progress snapshots
|
|
139
|
-
done_event = threading.Event()
|
|
140
|
-
result_holder: list = []
|
|
141
|
-
|
|
142
|
-
def _ssh_build() -> None:
|
|
143
|
-
r = probe.run(cmd, timeout=timeout)
|
|
144
|
-
result_holder.append(r)
|
|
145
|
-
done_event.set()
|
|
146
|
-
|
|
147
|
-
thread = threading.Thread(target=_ssh_build, daemon=True)
|
|
148
|
-
thread.start()
|
|
149
|
-
|
|
150
|
-
elapsed = 0
|
|
151
|
-
while not done_event.wait(timeout=poll_interval):
|
|
152
|
-
elapsed += poll_interval
|
|
153
|
-
snap = probe.run(
|
|
154
|
-
"podman system df 2>/dev/null | grep -E 'Image|Cache' | "
|
|
155
|
-
"awk '{print $1, $3, $4}'",
|
|
156
|
-
timeout=10,
|
|
157
|
-
)
|
|
158
|
-
if snap.ok and snap.out.strip():
|
|
159
|
-
lines = " | ".join(snap.out.strip().splitlines())
|
|
160
|
-
msg = f"[{elapsed}s] podman cache: {lines}"
|
|
161
|
-
logger.debug(f" {msg}")
|
|
162
|
-
if emitter:
|
|
163
|
-
emitter.progress(step.id, msg)
|
|
164
|
-
else:
|
|
165
|
-
msg = f"[{elapsed}s] podman build in progress..."
|
|
166
|
-
logger.debug(f" {msg}")
|
|
167
|
-
if emitter:
|
|
168
|
-
emitter.progress(step.id, msg)
|
|
169
|
-
|
|
170
|
-
if not result_holder:
|
|
171
|
-
raise StepError(step, "Podman build thread did not return a result")
|
|
172
|
-
r = result_holder[0]
|
|
173
|
-
step.result = r.out[:500]
|
|
174
|
-
if not r.ok:
|
|
175
|
-
raise StepError(step, f"exit={r.exit_code}: {r.stderr[:300]}")
|
|
176
|
-
logger.debug(f" podman build completed in {elapsed + poll_interval}s")
|
|
177
|
-
step.status = StepStatus.DONE
|
|
90
|
+
from .utils import run_container_build
|
|
91
|
+
run_container_build(step, probe, emitter, engine="podman")
|
|
178
92
|
|
|
179
93
|
|
|
180
94
|
def run_docker_health_wait(
|
|
@@ -424,3 +338,63 @@ def _resolve_command_ref(command_ref: str, step: MigrationStep, plan: MigrationP
|
|
|
424
338
|
raise StepError(step, f"Could not find bash script for ref '{section_id}' in {file_path} (tried markpact:ref and section heading)")
|
|
425
339
|
|
|
426
340
|
return script
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
# ── hardware-specific handlers ────────────────────────────────────────────────
|
|
344
|
+
|
|
345
|
+
def run_ensure_config_line(step: MigrationStep, probe: "RemoteProbe") -> None:
|
|
346
|
+
"""Idempotent add/replace a line in a remote config.txt."""
|
|
347
|
+
from ..hardware.config_txt import ensure_line
|
|
348
|
+
|
|
349
|
+
if not step.config_file or not step.config_line:
|
|
350
|
+
raise StepError(step, "ensure_config_line requires config_file and config_line")
|
|
351
|
+
|
|
352
|
+
config_path = step.config_file
|
|
353
|
+
r = probe.run(f"sudo cat {config_path}", timeout=10)
|
|
354
|
+
if not r.ok:
|
|
355
|
+
raise StepError(step, f"Cannot read {config_path}: {r.stderr[:200]}")
|
|
356
|
+
|
|
357
|
+
edit = ensure_line(
|
|
358
|
+
r.out,
|
|
359
|
+
step.config_line,
|
|
360
|
+
section=step.config_section or "all",
|
|
361
|
+
replaces_pattern=step.config_replaces_pattern,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
if not edit.changed:
|
|
365
|
+
step.status = StepStatus.DONE
|
|
366
|
+
step.result = f"no-op: {edit.diff_summary}"
|
|
367
|
+
return
|
|
368
|
+
|
|
369
|
+
# Write atomically: base64-encode to avoid shell quoting issues
|
|
370
|
+
encoded = base64.b64encode(edit.new_content.encode()).decode()
|
|
371
|
+
tmp = f"/tmp/redeploy-cfg-{step.id}.txt"
|
|
372
|
+
write_r = probe.run(
|
|
373
|
+
f"echo '{encoded}' | base64 -d | sudo tee {tmp} > /dev/null && sudo mv {tmp} {config_path}",
|
|
374
|
+
timeout=15,
|
|
375
|
+
)
|
|
376
|
+
if not write_r.ok:
|
|
377
|
+
raise StepError(step, f"Cannot write {config_path}: {write_r.stderr[:200]}")
|
|
378
|
+
|
|
379
|
+
step.status = StepStatus.DONE
|
|
380
|
+
step.result = edit.diff_summary
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def run_raspi_config(step: MigrationStep, probe: "RemoteProbe") -> None:
|
|
384
|
+
"""Run raspi-config nonint to enable/disable an interface."""
|
|
385
|
+
from ..hardware.raspi_config import build_raspi_config_command
|
|
386
|
+
|
|
387
|
+
if not step.raspi_interface or not step.raspi_state:
|
|
388
|
+
raise StepError(step, "raspi_config requires raspi_interface and raspi_state")
|
|
389
|
+
|
|
390
|
+
try:
|
|
391
|
+
cmd = build_raspi_config_command(step.raspi_interface, step.raspi_state)
|
|
392
|
+
except ValueError as exc:
|
|
393
|
+
raise StepError(step, str(exc))
|
|
394
|
+
|
|
395
|
+
r = probe.run(cmd, timeout=30)
|
|
396
|
+
if not r.ok:
|
|
397
|
+
raise StepError(step, f"raspi-config failed: {r.stderr[:200]}")
|
|
398
|
+
|
|
399
|
+
step.status = StepStatus.DONE
|
|
400
|
+
step.result = f"applied: {cmd}"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Container build handler with progress polling (docker/podman)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
from typing import TYPE_CHECKING, Literal
|
|
7
|
+
|
|
8
|
+
from loguru import logger
|
|
9
|
+
|
|
10
|
+
from ...models import StepStatus
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ..models import MigrationStep
|
|
14
|
+
from ..progress import ProgressEmitter
|
|
15
|
+
from ...detect.remote import RemoteProbe
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def run_container_build(
|
|
19
|
+
step: "MigrationStep",
|
|
20
|
+
probe: "RemoteProbe",
|
|
21
|
+
emitter: "ProgressEmitter | None",
|
|
22
|
+
engine: Literal["docker", "podman"],
|
|
23
|
+
) -> None:
|
|
24
|
+
"""Run container build on remote with periodic progress polling."""
|
|
25
|
+
cmd = step.command
|
|
26
|
+
if not cmd:
|
|
27
|
+
from ..exceptions import StepError
|
|
28
|
+
raise StepError(step, "No command specified")
|
|
29
|
+
|
|
30
|
+
timeout = step.timeout or 1800 # 30 min default for ARM64 builds
|
|
31
|
+
poll_interval = 15
|
|
32
|
+
done_event = threading.Event()
|
|
33
|
+
result_holder: list = []
|
|
34
|
+
|
|
35
|
+
def _ssh_build() -> None:
|
|
36
|
+
r = probe.run(cmd, timeout=timeout)
|
|
37
|
+
result_holder.append(r)
|
|
38
|
+
done_event.set()
|
|
39
|
+
|
|
40
|
+
thread = threading.Thread(target=_ssh_build, daemon=True)
|
|
41
|
+
thread.start()
|
|
42
|
+
|
|
43
|
+
elapsed = 0
|
|
44
|
+
while not done_event.wait(timeout=poll_interval):
|
|
45
|
+
elapsed += poll_interval
|
|
46
|
+
cache_cmd = f"{engine} system df 2>/dev/null | grep -E 'Image|Cache' | awk '{{print $1, $3, $4}}'"
|
|
47
|
+
snap = probe.run(cache_cmd, timeout=10)
|
|
48
|
+
if snap.ok and snap.out.strip():
|
|
49
|
+
lines = " | ".join(snap.out.strip().splitlines())
|
|
50
|
+
msg = f"[{elapsed}s] {engine} cache: {lines}"
|
|
51
|
+
logger.debug(f" {msg}")
|
|
52
|
+
if emitter:
|
|
53
|
+
emitter.progress(step.id, msg)
|
|
54
|
+
else:
|
|
55
|
+
msg = f"[{elapsed}s] {engine} build in progress..."
|
|
56
|
+
logger.debug(f" {msg}")
|
|
57
|
+
if emitter:
|
|
58
|
+
emitter.progress(step.id, msg)
|
|
59
|
+
|
|
60
|
+
if not result_holder:
|
|
61
|
+
from ..exceptions import StepError
|
|
62
|
+
raise StepError(step, f"{engine.capitalize()} build thread did not return a result")
|
|
63
|
+
r = result_holder[0]
|
|
64
|
+
step.result = r.out[:500]
|
|
65
|
+
if not r.ok:
|
|
66
|
+
from ..exceptions import StepError
|
|
67
|
+
raise StepError(step, f"exit={r.exit_code}: {r.stderr[:300]}")
|
|
68
|
+
logger.debug(f" {engine} build completed in {elapsed + poll_interval}s")
|
|
69
|
+
step.status = StepStatus.DONE
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""hardware command — Probe and diagnose hardware on a remote host.
|
|
2
|
+
|
|
3
|
+
Detects DSI displays, DRM connectors, backlight, I2C buses,
|
|
4
|
+
GPIO, config.txt overlays and reports problems with fix suggestions.
|
|
5
|
+
|
|
6
|
+
Examples::
|
|
7
|
+
|
|
8
|
+
redeploy hardware pi@192.168.188.109
|
|
9
|
+
redeploy hardware pi@192.168.188.109 --format json
|
|
10
|
+
redeploy hardware pi@192.168.188.109 --fix # print all fix commands
|
|
11
|
+
redeploy hardware pi@192.168.188.109 --apply-fix dsi # run fix for component
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
import click
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
from rich.panel import Panel
|
|
20
|
+
from rich.table import Table
|
|
21
|
+
from rich import box
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_SEVERITY_COLOR = {
|
|
25
|
+
"info": "green",
|
|
26
|
+
"warning": "yellow",
|
|
27
|
+
"error": "red",
|
|
28
|
+
"critical": "bold red",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_SEVERITY_ICON = {
|
|
32
|
+
"info": "✓",
|
|
33
|
+
"warning": "⚠",
|
|
34
|
+
"error": "✗",
|
|
35
|
+
"critical": "✗✗",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _probe_hardware(host, ssh_key, console):
|
|
40
|
+
"""Probe hardware on remote host."""
|
|
41
|
+
from ...detect.hardware import probe_hardware
|
|
42
|
+
from ...detect.remote import RemoteProbe
|
|
43
|
+
|
|
44
|
+
p = RemoteProbe(host, ssh_key=ssh_key) if ssh_key else RemoteProbe(host)
|
|
45
|
+
|
|
46
|
+
with console.status(f"[cyan]Probing hardware on {host}…[/cyan]"):
|
|
47
|
+
try:
|
|
48
|
+
hw = probe_hardware(p)
|
|
49
|
+
except ConnectionError as exc:
|
|
50
|
+
console.print(f"[red]✗ {exc}[/red]")
|
|
51
|
+
sys.exit(2)
|
|
52
|
+
except Exception as exc:
|
|
53
|
+
console.print(f"[red]✗ hardware probe failed: {exc}[/red]")
|
|
54
|
+
raise
|
|
55
|
+
|
|
56
|
+
return hw, p
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _format_output(hw, output_fmt):
|
|
60
|
+
"""Format output as yaml/json, return True if formatted."""
|
|
61
|
+
if output_fmt == "yaml":
|
|
62
|
+
import yaml
|
|
63
|
+
click.echo(yaml.safe_dump(hw.model_dump(mode="json"), sort_keys=False))
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
if output_fmt == "json":
|
|
67
|
+
import json as _json
|
|
68
|
+
click.echo(_json.dumps(hw.model_dump(mode="json"), indent=2))
|
|
69
|
+
return True
|
|
70
|
+
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _print_drm_connectors(console, hw):
|
|
75
|
+
"""Print DRM connectors table."""
|
|
76
|
+
if not hw.drm_outputs:
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
t = Table(title="DRM Connectors", show_header=True, box=box.SIMPLE, padding=(0, 1))
|
|
80
|
+
t.add_column("Connector")
|
|
81
|
+
t.add_column("Status")
|
|
82
|
+
t.add_column("Enabled")
|
|
83
|
+
t.add_column("Mode")
|
|
84
|
+
t.add_column("Transform")
|
|
85
|
+
for o in hw.drm_outputs:
|
|
86
|
+
status_color = "green" if o.status == "connected" else "red"
|
|
87
|
+
mode = o.modes[0] if o.modes else "—"
|
|
88
|
+
t.add_row(
|
|
89
|
+
o.connector,
|
|
90
|
+
f"[{status_color}]{o.status}[/{status_color}]",
|
|
91
|
+
o.enabled,
|
|
92
|
+
mode,
|
|
93
|
+
o.transform,
|
|
94
|
+
)
|
|
95
|
+
console.print(t)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _print_backlights(console, hw):
|
|
99
|
+
"""Print backlights table."""
|
|
100
|
+
if not hw.backlights:
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
t = Table(title="Backlight", show_header=True, box=box.SIMPLE, padding=(0, 1))
|
|
104
|
+
t.add_column("Device")
|
|
105
|
+
t.add_column("Brightness")
|
|
106
|
+
t.add_column("Max")
|
|
107
|
+
t.add_column("Power")
|
|
108
|
+
t.add_column("Display")
|
|
109
|
+
for b in hw.backlights:
|
|
110
|
+
pw_color = "green" if b.bl_power == 0 else "red"
|
|
111
|
+
br_color = "green" if b.brightness > 0 else "red"
|
|
112
|
+
t.add_row(
|
|
113
|
+
b.name,
|
|
114
|
+
f"[{br_color}]{b.brightness}[/{br_color}]",
|
|
115
|
+
str(b.max_brightness),
|
|
116
|
+
f"[{pw_color}]{'ON' if b.bl_power == 0 else 'OFF'}[/{pw_color}]",
|
|
117
|
+
b.display_name or "—",
|
|
118
|
+
)
|
|
119
|
+
console.print(t)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _print_dsi_overlays(console, hw):
|
|
123
|
+
"""Print DSI overlays."""
|
|
124
|
+
if hw.dsi_overlays:
|
|
125
|
+
console.print(f"[dim]DSI overlays:[/dim] {', '.join(hw.dsi_overlays)}")
|
|
126
|
+
else:
|
|
127
|
+
console.print("[red]DSI overlays: none found[/red]")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _print_i2c_buses(console, hw):
|
|
131
|
+
"""Print I2C buses summary."""
|
|
132
|
+
if not hw.i2c_buses:
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
bus_summary = ", ".join(
|
|
136
|
+
f"i2c-{b.bus}" + (f" [{','.join(b.devices)}]" if b.devices else "")
|
|
137
|
+
for b in hw.i2c_buses
|
|
138
|
+
)
|
|
139
|
+
console.print(f"[dim]I2C buses:[/dim] {bus_summary}")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _print_diagnostics(console, hw, show_fix):
|
|
143
|
+
"""Print diagnostics table and fix suggestions."""
|
|
144
|
+
if not hw.diagnostics:
|
|
145
|
+
console.print("[green]✓ No hardware issues detected[/green]")
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
t = Table(title="Diagnostics", show_header=True, box=box.SIMPLE, padding=(0, 1))
|
|
149
|
+
t.add_column("", width=3)
|
|
150
|
+
t.add_column("Component", style="bold")
|
|
151
|
+
t.add_column("Message")
|
|
152
|
+
|
|
153
|
+
for d in hw.diagnostics:
|
|
154
|
+
color = _SEVERITY_COLOR.get(d.severity, "white")
|
|
155
|
+
icon = _SEVERITY_ICON.get(d.severity, "?")
|
|
156
|
+
t.add_row(
|
|
157
|
+
f"[{color}]{icon}[/{color}]",
|
|
158
|
+
f"[{color}]{d.component}[/{color}]",
|
|
159
|
+
d.message,
|
|
160
|
+
)
|
|
161
|
+
console.print(t)
|
|
162
|
+
|
|
163
|
+
# Fix hints
|
|
164
|
+
fixable = [d for d in hw.diagnostics if d.fix]
|
|
165
|
+
if fixable and show_fix:
|
|
166
|
+
console.print()
|
|
167
|
+
console.print("[bold]Fix suggestions:[/bold]")
|
|
168
|
+
for d in fixable:
|
|
169
|
+
color = _SEVERITY_COLOR.get(d.severity, "white")
|
|
170
|
+
console.print(
|
|
171
|
+
f"\n [{color}][{d.component}][/{color}] {d.message}\n"
|
|
172
|
+
+ "\n".join(f" [dim]{line}[/dim]" for line in (d.fix or "").splitlines())
|
|
173
|
+
)
|
|
174
|
+
elif fixable and not show_fix:
|
|
175
|
+
console.print(
|
|
176
|
+
f"\n[dim] {len(fixable)} fix suggestion(s) available — rerun with --fix[/dim]"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _apply_fix(console, p, hw, apply_fix_component, panel_id=None):
|
|
181
|
+
"""Apply fix for a specific component using fix plan generator."""
|
|
182
|
+
from ...hardware.fixes import generate_fix_plan
|
|
183
|
+
from ...hardware.panels import get
|
|
184
|
+
|
|
185
|
+
# Get panel if specified
|
|
186
|
+
panel = get(panel_id) if panel_id else None
|
|
187
|
+
|
|
188
|
+
# Generate fix plan
|
|
189
|
+
steps = generate_fix_plan(hw, apply_fix_component, panel)
|
|
190
|
+
if not steps:
|
|
191
|
+
console.print(f"[yellow]No fix available for component: {apply_fix_component}[/yellow]")
|
|
192
|
+
if panel_id:
|
|
193
|
+
console.print(f"[dim]Panel: {panel_id}[/dim]")
|
|
194
|
+
sys.exit(1)
|
|
195
|
+
|
|
196
|
+
console.print(f"[bold]Fix plan for {apply_fix_component}[/bold]")
|
|
197
|
+
if panel:
|
|
198
|
+
console.print(f" Panel: {panel.name}")
|
|
199
|
+
|
|
200
|
+
# Execute steps via Executor (simplified version)
|
|
201
|
+
from ...apply.executor import Executor
|
|
202
|
+
|
|
203
|
+
executor = Executor(probe=p, console=console, dry_run=False)
|
|
204
|
+
for step in steps:
|
|
205
|
+
console.print(f"[cyan]→ {step.description}[/cyan]")
|
|
206
|
+
try:
|
|
207
|
+
executor._run_single_step(step)
|
|
208
|
+
console.print(f"[green] ✓ done[/green]")
|
|
209
|
+
except Exception as exc:
|
|
210
|
+
console.print(f"[red] ✗ failed: {exc}[/red]")
|
|
211
|
+
sys.exit(1)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@click.command()
|
|
215
|
+
@click.argument("host", required=False, default=None)
|
|
216
|
+
@click.option(
|
|
217
|
+
"--format", "output_fmt",
|
|
218
|
+
default="rich",
|
|
219
|
+
type=click.Choice(["rich", "yaml", "json"]),
|
|
220
|
+
help="Output format",
|
|
221
|
+
)
|
|
222
|
+
@click.option(
|
|
223
|
+
"--fix", "show_fix",
|
|
224
|
+
is_flag=True,
|
|
225
|
+
help="Print fix commands for all issues found",
|
|
226
|
+
)
|
|
227
|
+
@click.option(
|
|
228
|
+
"--apply-fix", "apply_fix_component",
|
|
229
|
+
default=None,
|
|
230
|
+
metavar="COMPONENT",
|
|
231
|
+
help=(
|
|
232
|
+
"Run the fix for a specific component via SSH "
|
|
233
|
+
"(e.g. 'backlight', 'overlay'). Requires sudo on target."
|
|
234
|
+
),
|
|
235
|
+
)
|
|
236
|
+
@click.option(
|
|
237
|
+
"--panel", "panel_id",
|
|
238
|
+
default=None,
|
|
239
|
+
metavar="PANEL_ID",
|
|
240
|
+
help="Specify panel ID explicitly (use --list-panels to see available)",
|
|
241
|
+
)
|
|
242
|
+
@click.option(
|
|
243
|
+
"--list-panels",
|
|
244
|
+
is_flag=True,
|
|
245
|
+
help="List available panel definitions and exit",
|
|
246
|
+
)
|
|
247
|
+
@click.option("--ssh-key", default=None, type=click.Path(), help="SSH private key path")
|
|
248
|
+
def hardware(host, output_fmt, show_fix, apply_fix_component, panel_id, list_panels, ssh_key):
|
|
249
|
+
"""Probe and diagnose hardware on a remote host.
|
|
250
|
+
|
|
251
|
+
Checks DSI display, DRM connectors, backlight controller, I2C buses,
|
|
252
|
+
config.txt overlays and Wayland compositor — then reports issues with
|
|
253
|
+
actionable fix suggestions.
|
|
254
|
+
|
|
255
|
+
\b
|
|
256
|
+
Examples:
|
|
257
|
+
redeploy hardware pi@192.168.188.109
|
|
258
|
+
redeploy hardware pi@192.168.188.109 --fix
|
|
259
|
+
redeploy hardware pi@192.168.188.109 --format yaml
|
|
260
|
+
redeploy hardware --list-panels
|
|
261
|
+
"""
|
|
262
|
+
console = Console()
|
|
263
|
+
|
|
264
|
+
# Handle --list-panels
|
|
265
|
+
if list_panels:
|
|
266
|
+
from ...hardware.panels import all_panels
|
|
267
|
+
panels = all_panels()
|
|
268
|
+
t = Table(show_header=True, box=box.ROUNDED)
|
|
269
|
+
t.add_column("ID", style="cyan")
|
|
270
|
+
t.add_column("Name", style="bold")
|
|
271
|
+
t.add_column("Vendor", style="dim")
|
|
272
|
+
t.add_column("Resolution", style="dim")
|
|
273
|
+
t.add_column("Overlay", style="dim")
|
|
274
|
+
for p in panels:
|
|
275
|
+
res = f"{p.resolution[0]}×{p.resolution[1]}" if p.resolution else "—"
|
|
276
|
+
t.add_row(p.id, p.name, p.vendor, res, p.overlay)
|
|
277
|
+
console.print(t)
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
if not host:
|
|
281
|
+
raise click.UsageError("HOST argument is required unless --list-panels is used.")
|
|
282
|
+
|
|
283
|
+
hw, p = _probe_hardware(host, ssh_key, console)
|
|
284
|
+
|
|
285
|
+
if _format_output(hw, output_fmt):
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
# ── rich output ─────────────────────────────────────────────────────────
|
|
289
|
+
|
|
290
|
+
console.print()
|
|
291
|
+
console.print(Panel(
|
|
292
|
+
f"[bold]hardware probe[/bold] [cyan]{host}[/cyan]\n"
|
|
293
|
+
f"Board: [white]{hw.board or 'unknown'}[/white] "
|
|
294
|
+
f"Kernel: [white]{hw.kernel or 'unknown'}[/white]",
|
|
295
|
+
box=box.ROUNDED,
|
|
296
|
+
expand=False,
|
|
297
|
+
))
|
|
298
|
+
|
|
299
|
+
_print_drm_connectors(console, hw)
|
|
300
|
+
_print_backlights(console, hw)
|
|
301
|
+
_print_dsi_overlays(console, hw)
|
|
302
|
+
_print_i2c_buses(console, hw)
|
|
303
|
+
|
|
304
|
+
console.print()
|
|
305
|
+
|
|
306
|
+
_print_diagnostics(console, hw, show_fix)
|
|
307
|
+
|
|
308
|
+
if apply_fix_component:
|
|
309
|
+
_apply_fix(console, p, hw, apply_fix_component, panel_id)
|
|
310
|
+
|
|
311
|
+
# Exit code based on errors
|
|
312
|
+
errors = hw.errors
|
|
313
|
+
if errors:
|
|
314
|
+
sys.exit(1)
|