redeploy 0.2.45__tar.gz → 0.2.46__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.45 → redeploy-0.2.46}/PKG-INFO +6 -6
- {redeploy-0.2.45 → redeploy-0.2.46}/README.md +5 -5
- {redeploy-0.2.45 → redeploy-0.2.46}/pyproject.toml +1 -1
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/__init__.py +1 -1
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/apply/__init__.py +10 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/apply/handlers.py +109 -27
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/blueprint.py +3 -63
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/device_map.py +6 -250
- redeploy-0.2.46/redeploy/cli/commands/device_map_renderers.py +170 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/hardware.py +41 -2
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/models.py +9 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy.egg-info/PKG-INFO +6 -6
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy.egg-info/SOURCES.txt +1 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/apply/exceptions.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/apply/executor.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/apply/progress.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/apply/rollback.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/apply/state.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/apply/state_apply.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/apply/utils/__init__.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/apply/utils/run_container_build.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/audit.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/blueprint/__init__.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/blueprint/extractor.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/blueprint/generators/__init__.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/blueprint/generators/docker_compose.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/blueprint/generators/migration.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/__init__.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/__init__.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/audit.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/detect.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/devices.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/diagnose.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/diff.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/exec_.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/export.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/import_.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/init.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/inspect.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/patterns.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/plan_apply.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/plugin.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/probe.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/push.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/state.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/status.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/target.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/version/__init__.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/version/commands.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/version/helpers.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/version/monorepo.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/version/release.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/version/scanner.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/commands/workflow.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/core.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli/display.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/cli.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/config_apply/__init__.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/config_apply/applier.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/config_apply/loader.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/data_sync.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/detect/__init__.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/detect/builtin/__init__.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/detect/detector.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/detect/hardware.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/detect/hardware_rules.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/detect/probes.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/detect/remote.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/detect/templates.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/detect/workflow.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/discovery.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/dsl/__init__.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/dsl/loader.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/dsl/parser.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/dsl_python/__init__.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/dsl_python/context.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/dsl_python/decorators.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/dsl_python/docker_steps.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/dsl_python/exceptions.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/dsl_python/runner.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/dsl_python/steps.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/fleet.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/hardware/__init__.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/hardware/config_txt.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/hardware/data/__init__.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/hardware/data/hyperpixel.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/hardware/data/official.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/hardware/data/waveshare.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/hardware/fixes.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/hardware/kiosk/__init__.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/hardware/kiosk/autostart.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/hardware/kiosk/browsers.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/hardware/kiosk/compositors.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/hardware/kiosk/output_profiles.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/hardware/panels.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/hardware/raspi_config.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/iac/__init__.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/iac/base.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/iac/docker_compose.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/iac/parsers/__init__.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/iac/parsers/compose.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/iac/registry.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/integrations/__init__.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/integrations/op3_bridge.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/markpact/__init__.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/markpact/compiler.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/markpact/models.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/markpact/parser.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/observe.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/parse.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/patterns.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/plan/__init__.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/plan/planner.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/plugins/__init__.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/plugins/builtin/__init__.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/plugins/builtin/browser_reload.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/plugins/builtin/hardware_diagnostic.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/plugins/builtin/notify.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/plugins/builtin/process_control.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/plugins/builtin/systemd_reload.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/spec_loader.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/ssh.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/steps.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/__init__.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_audit.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_cli.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_data_sync.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_discovery.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_dsl.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_examples.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_executor.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_fleet.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_git_integration.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_iac.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_manifest.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_markpact_compiler.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_markpact_parser.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_models.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_observe.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_parse.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_patterns.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_planner_edge.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_plugins.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_probes.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_public_api.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_resume.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_spec_loader.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_ssh.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_templates.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_verify.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_version.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_version_changelog.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_version_cli.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_version_manifest.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_version_sources.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/tests/test_workflow.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/verify.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/version/__init__.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/version/bump.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/version/changelog.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/version/commits.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/version/diff.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/version/git_integration.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/version/git_transaction.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/version/manifest.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/version/sources/__init__.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/version/sources/base.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/version/sources/json_.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/version/sources/plain.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/version/sources/regex.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/version/sources/toml_.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/version/sources/yaml_.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/version/transaction.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy/version.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy.egg-info/dependency_links.txt +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy.egg-info/entry_points.txt +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy.egg-info/requires.txt +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/redeploy.egg-info/top_level.txt +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/setup.cfg +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/tests/test_hardware_config.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/tests/test_hardware_kiosk.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/tests/test_hardware_rules.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/tests/test_iac.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/tests/test_parse.py +0 -0
- {redeploy-0.2.45 → redeploy-0.2.46}/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.46
|
|
4
4
|
Summary: Infrastructure migration toolkit: detect → plan → apply
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -30,17 +30,17 @@ Requires-Dist: op3>=0.1.4; extra == "op3"
|
|
|
30
30
|
|
|
31
31
|
## AI Cost Tracking
|
|
32
32
|
|
|
33
|
-
     
|
|
34
|
+
  
|
|
35
35
|
|
|
36
|
-
- 🤖 **LLM usage:** $7.5000 (
|
|
37
|
-
- 👤 **Human dev:** ~$
|
|
36
|
+
- 🤖 **LLM usage:** $7.5000 (67 commits)
|
|
37
|
+
- 👤 **Human dev:** ~$2360 (23.6h @ $100/h, 30min dedup)
|
|
38
38
|
|
|
39
39
|
Generated on 2026-04-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
40
40
|
|
|
41
41
|
---
|
|
42
42
|
|
|
43
|
-
    
|
|
44
44
|
|
|
45
45
|
Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
|
|
46
46
|
|
|
@@ -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 (67 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$2360 (23.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.46"
|
|
11
11
|
|
|
12
12
|
# ── Core models ───────────────────────────────────────────────────────────────
|
|
13
13
|
from .models import ( # noqa: F401
|
|
@@ -5,10 +5,15 @@ from .handlers import (
|
|
|
5
5
|
run_container_log_tail,
|
|
6
6
|
run_docker_build,
|
|
7
7
|
run_docker_health_wait,
|
|
8
|
+
run_ensure_autostart_entry,
|
|
9
|
+
run_ensure_browser_kiosk_script,
|
|
10
|
+
run_ensure_config_line,
|
|
11
|
+
run_ensure_kanshi_profile,
|
|
8
12
|
run_http_check,
|
|
9
13
|
run_inline_script,
|
|
10
14
|
run_plugin,
|
|
11
15
|
run_podman_build,
|
|
16
|
+
run_raspi_config,
|
|
12
17
|
run_rsync,
|
|
13
18
|
run_scp,
|
|
14
19
|
run_ssh,
|
|
@@ -37,10 +42,15 @@ __all__ = [
|
|
|
37
42
|
"run_container_log_tail",
|
|
38
43
|
"run_docker_build",
|
|
39
44
|
"run_docker_health_wait",
|
|
45
|
+
"run_ensure_autostart_entry",
|
|
46
|
+
"run_ensure_browser_kiosk_script",
|
|
47
|
+
"run_ensure_config_line",
|
|
48
|
+
"run_ensure_kanshi_profile",
|
|
40
49
|
"run_http_check",
|
|
41
50
|
"run_inline_script",
|
|
42
51
|
"run_plugin",
|
|
43
52
|
"run_podman_build",
|
|
53
|
+
"run_raspi_config",
|
|
44
54
|
"run_rsync",
|
|
45
55
|
"run_scp",
|
|
46
56
|
"run_ssh",
|
|
@@ -405,14 +405,36 @@ def run_raspi_config(step: MigrationStep, probe: "RemoteProbe") -> None:
|
|
|
405
405
|
def run_ensure_kanshi_profile(step: MigrationStep, probe: "RemoteProbe") -> None:
|
|
406
406
|
"""Idempotently write or replace a named kanshi output profile.
|
|
407
407
|
|
|
408
|
-
|
|
409
|
-
|
|
408
|
+
Declarative mode (preferred)::
|
|
409
|
+
|
|
410
|
+
profile_name: waveshare-only
|
|
411
|
+
outputs_on: [DSI-2]
|
|
412
|
+
outputs_off: [HDMI-A-2]
|
|
413
|
+
|
|
414
|
+
Legacy mode (pre-rendered block)::
|
|
415
|
+
|
|
416
|
+
step.command — the profile block to write (rendered kanshi syntax)
|
|
417
|
+
step.config_file — kanshi config path (default: ~/.config/kanshi/config)
|
|
410
418
|
"""
|
|
411
419
|
from ..hardware.kiosk.output_profiles import OutputProfile
|
|
412
420
|
|
|
413
|
-
|
|
421
|
+
# Declarative mode: build profile from semantic fields
|
|
422
|
+
if step.profile_name:
|
|
423
|
+
profile = OutputProfile(
|
|
424
|
+
name=step.profile_name,
|
|
425
|
+
enabled=list(step.outputs_on),
|
|
426
|
+
disabled=list(step.outputs_off),
|
|
427
|
+
)
|
|
428
|
+
profile_block = profile.to_kanshi_config()
|
|
429
|
+
else:
|
|
430
|
+
profile_block = step.command
|
|
431
|
+
|
|
414
432
|
if not profile_block:
|
|
415
|
-
raise StepError(
|
|
433
|
+
raise StepError(
|
|
434
|
+
step,
|
|
435
|
+
"ensure_kanshi_profile requires profile_name+outputs_on/outputs_off "
|
|
436
|
+
"or step.command with pre-rendered profile block",
|
|
437
|
+
)
|
|
416
438
|
|
|
417
439
|
config_path = step.config_file or "~/.config/kanshi/config"
|
|
418
440
|
mkdir_cmd = f"mkdir -p $(dirname {config_path})"
|
|
@@ -462,56 +484,116 @@ def run_ensure_kanshi_profile(step: MigrationStep, probe: "RemoteProbe") -> None
|
|
|
462
484
|
|
|
463
485
|
|
|
464
486
|
def run_ensure_autostart_entry(step: MigrationStep, probe: "RemoteProbe") -> None:
|
|
465
|
-
"""Idempotently add or replace
|
|
487
|
+
"""Idempotently add or replace keyed entries in a compositor autostart file.
|
|
466
488
|
|
|
467
|
-
|
|
468
|
-
step.config_line — the line to write (the entry body)
|
|
469
|
-
step.config_section — used as the entry key for idempotent marker
|
|
470
|
-
"""
|
|
471
|
-
from ..hardware.kiosk.autostart import AutostartEntry, ensure_autostart_entry
|
|
489
|
+
Declarative mode (preferred)::
|
|
472
490
|
|
|
473
|
-
|
|
474
|
-
|
|
491
|
+
compositor: labwc
|
|
492
|
+
entries:
|
|
493
|
+
- "kanshid &"
|
|
494
|
+
- "sleep 3"
|
|
495
|
+
- "bash /home/pi/c2004/scripts/kiosk-launch.sh &"
|
|
475
496
|
|
|
476
|
-
|
|
477
|
-
entry = AutostartEntry(key=key, line=step.config_line)
|
|
497
|
+
Legacy mode (single entry)::
|
|
478
498
|
|
|
479
|
-
|
|
499
|
+
step.config_file — path to autostart file
|
|
500
|
+
step.config_line — the line to write (the entry body)
|
|
501
|
+
step.config_section — used as the entry key for idempotent marker
|
|
502
|
+
"""
|
|
503
|
+
from ..hardware.kiosk.autostart import AutostartEntry, ensure_autostart_entry
|
|
504
|
+
from ..hardware.kiosk.compositors import COMPOSITORS
|
|
505
|
+
|
|
506
|
+
# Determine autostart path
|
|
507
|
+
config_file = step.config_file
|
|
508
|
+
if not config_file and step.compositor:
|
|
509
|
+
comp = COMPOSITORS.get(step.compositor)
|
|
510
|
+
if not comp:
|
|
511
|
+
raise StepError(step, f"Unknown compositor '{step.compositor}'")
|
|
512
|
+
config_file = comp.autostart_abs()
|
|
513
|
+
if not config_file:
|
|
514
|
+
raise StepError(step, "ensure_autostart_entry requires config_file or compositor")
|
|
515
|
+
|
|
516
|
+
# Collect entries to apply
|
|
517
|
+
entries: list[AutostartEntry] = []
|
|
518
|
+
if step.entries:
|
|
519
|
+
for i, line in enumerate(step.entries):
|
|
520
|
+
key = f"redeploy-{i}-{line.split()[0] if line else 'entry'}"
|
|
521
|
+
entries.append(AutostartEntry(key=key, line=line))
|
|
522
|
+
elif step.config_line:
|
|
523
|
+
key = step.config_section or "redeploy"
|
|
524
|
+
entries.append(AutostartEntry(key=key, line=step.config_line))
|
|
525
|
+
|
|
526
|
+
if not entries:
|
|
527
|
+
raise StepError(step, "ensure_autostart_entry requires entries or config_line")
|
|
528
|
+
|
|
529
|
+
r = probe.run(f"cat {config_file} 2>/dev/null || echo ''", timeout=10)
|
|
480
530
|
existing = r.out if r.ok else ""
|
|
481
531
|
|
|
482
|
-
|
|
483
|
-
|
|
532
|
+
changed_any = False
|
|
533
|
+
new_content = existing
|
|
534
|
+
for entry in entries:
|
|
535
|
+
new_content, changed = ensure_autostart_entry(new_content, entry)
|
|
536
|
+
if changed:
|
|
537
|
+
changed_any = True
|
|
538
|
+
|
|
539
|
+
if not changed_any:
|
|
484
540
|
step.status = StepStatus.DONE
|
|
485
|
-
step.result = f"no-op:
|
|
541
|
+
step.result = f"no-op: {len(entries)} autostart entry(ies) already correct"
|
|
486
542
|
return
|
|
487
543
|
|
|
488
|
-
mkdir_cmd = f"mkdir -p $(dirname {
|
|
544
|
+
mkdir_cmd = f"mkdir -p $(dirname {config_file})"
|
|
489
545
|
probe.run(mkdir_cmd, timeout=10)
|
|
490
546
|
|
|
491
547
|
encoded = base64.b64encode(new_content.encode()).decode()
|
|
492
548
|
tmp = f"/tmp/redeploy-autostart-{step.id}.txt"
|
|
493
549
|
write_r = probe.run(
|
|
494
|
-
f"echo '{encoded}' | base64 -d | tee {tmp} > /dev/null && mv {tmp} {
|
|
550
|
+
f"echo '{encoded}' | base64 -d | tee {tmp} > /dev/null && mv {tmp} {config_file}",
|
|
495
551
|
timeout=15,
|
|
496
552
|
)
|
|
497
553
|
if not write_r.ok:
|
|
498
|
-
raise StepError(step, f"Cannot write {
|
|
554
|
+
raise StepError(step, f"Cannot write {config_file}: {write_r.stderr[:200]}")
|
|
499
555
|
|
|
500
556
|
step.status = StepStatus.DONE
|
|
501
|
-
step.result = f"
|
|
557
|
+
step.result = f"{len(entries)} autostart entry(ies) written to {config_file}"
|
|
502
558
|
|
|
503
559
|
|
|
504
560
|
def run_ensure_browser_kiosk_script(step: MigrationStep, probe: "RemoteProbe") -> None:
|
|
505
561
|
"""Write a kiosk-launch.sh script to the remote device.
|
|
506
562
|
|
|
507
|
-
|
|
508
|
-
|
|
563
|
+
Declarative mode (preferred)::
|
|
564
|
+
|
|
565
|
+
browser_profile: chromium_wayland_kiosk
|
|
566
|
+
url: "http://localhost:8100/connect-id?font=xlarge&theme=dark"
|
|
567
|
+
kiosk_script_path: /home/pi/c2004/scripts/kiosk-launch.sh
|
|
568
|
+
|
|
569
|
+
Legacy mode (pre-rendered script)::
|
|
570
|
+
|
|
571
|
+
step.command — the full script content to write
|
|
572
|
+
step.dst — destination path (default: ~/kiosk-launch.sh)
|
|
509
573
|
"""
|
|
510
|
-
|
|
574
|
+
from ..hardware.kiosk.browsers import CHROMIUM_WAYLAND_KIOSK
|
|
575
|
+
|
|
576
|
+
# Declarative mode: build script from profile + URL
|
|
577
|
+
if step.browser_profile:
|
|
578
|
+
# Registry lookup (extend when more profiles exist)
|
|
579
|
+
if step.browser_profile == "chromium_wayland_kiosk":
|
|
580
|
+
profile = CHROMIUM_WAYLAND_KIOSK
|
|
581
|
+
else:
|
|
582
|
+
raise StepError(step, f"Unknown browser_profile '{step.browser_profile}'")
|
|
583
|
+
if not step.url:
|
|
584
|
+
raise StepError(step, "browser_profile mode requires 'url' field")
|
|
585
|
+
script_content = profile.build_launch_cmd(step.url)
|
|
586
|
+
else:
|
|
587
|
+
script_content = step.command
|
|
588
|
+
|
|
511
589
|
if not script_content:
|
|
512
|
-
raise StepError(
|
|
590
|
+
raise StepError(
|
|
591
|
+
step,
|
|
592
|
+
"ensure_browser_kiosk_script requires browser_profile+url "
|
|
593
|
+
"or step.command with pre-rendered script body",
|
|
594
|
+
)
|
|
513
595
|
|
|
514
|
-
dst = step.dst or "~/kiosk-launch.sh"
|
|
596
|
+
dst = step.kiosk_script_path or step.dst or "~/kiosk-launch.sh"
|
|
515
597
|
|
|
516
598
|
r = probe.run(f"cat {dst} 2>/dev/null || echo ''", timeout=10)
|
|
517
599
|
existing = r.out if r.ok else ""
|
|
@@ -95,68 +95,6 @@ def capture(host, name, compose_files, migration_file, device_map_file,
|
|
|
95
95
|
console.print(f"\n[green]✓ saved:[/green] {saved}")
|
|
96
96
|
|
|
97
97
|
|
|
98
|
-
def _apply_blueprint_config(bp, config_path):
|
|
99
|
-
"""Apply blueprint settings from YAML config file to the remote host.
|
|
100
|
-
|
|
101
|
-
Reads blueprint YAML/JSON and applies hardware settings (transforms, backlight, etc.).
|
|
102
|
-
"""
|
|
103
|
-
import yaml
|
|
104
|
-
import json as _json
|
|
105
|
-
|
|
106
|
-
from ...remote import Remote
|
|
107
|
-
|
|
108
|
-
with open(config_path) as f:
|
|
109
|
-
raw = f.read()
|
|
110
|
-
|
|
111
|
-
try:
|
|
112
|
-
cfg = yaml.safe_load(raw)
|
|
113
|
-
except Exception:
|
|
114
|
-
cfg = _json.loads(raw)
|
|
115
|
-
|
|
116
|
-
host = cfg.get("host") or getattr(bp, "host", None)
|
|
117
|
-
if not host:
|
|
118
|
-
print("[red]✗ No 'host' field in config file[/red]")
|
|
119
|
-
return
|
|
120
|
-
|
|
121
|
-
print(f"[cyan]→ Applying blueprint config from {config_path} to {host}[/cyan]")
|
|
122
|
-
|
|
123
|
-
p = Remote(host)
|
|
124
|
-
|
|
125
|
-
# Apply hardware settings (reuse logic from device-map)
|
|
126
|
-
hardware = cfg.get("hardware", {})
|
|
127
|
-
applied = 0
|
|
128
|
-
|
|
129
|
-
for output in hardware.get("drm_outputs", []):
|
|
130
|
-
connector = output.get("connector", "")
|
|
131
|
-
transform = output.get("transform", "normal")
|
|
132
|
-
if not connector or not ("DSI" in connector or "HDMI" in connector):
|
|
133
|
-
continue
|
|
134
|
-
|
|
135
|
-
if transform != "normal":
|
|
136
|
-
wlr_cmd = (
|
|
137
|
-
f"WAYLAND_DISPLAY=wayland-0 XDG_RUNTIME_DIR=/run/user/$(id -u) "
|
|
138
|
-
f"wlr-randr --output {connector} --transform {transform} 2>&1"
|
|
139
|
-
)
|
|
140
|
-
r = p.run(wlr_cmd)
|
|
141
|
-
if r.ok:
|
|
142
|
-
print(f"[green] ✓ {connector}: transform={transform}[/green]")
|
|
143
|
-
applied += 1
|
|
144
|
-
|
|
145
|
-
for bl in hardware.get("backlights", []):
|
|
146
|
-
name = bl.get("name", "")
|
|
147
|
-
brightness = bl.get("brightness")
|
|
148
|
-
if name and brightness is not None:
|
|
149
|
-
r = p.run(f"echo {brightness} | sudo tee /sys/class/backlight/{name}/brightness > /dev/null")
|
|
150
|
-
if r.ok:
|
|
151
|
-
print(f"[green] ✓ backlight {name}: brightness={brightness}[/green]")
|
|
152
|
-
applied += 1
|
|
153
|
-
|
|
154
|
-
if applied > 0:
|
|
155
|
-
print(f"[bold green]✓ Blueprint config applied from {config_path}[/bold green]")
|
|
156
|
-
else:
|
|
157
|
-
print("[yellow]Nothing to apply[/yellow]")
|
|
158
|
-
|
|
159
|
-
|
|
160
98
|
def _execute_query_blueprint(bp, query_expr, output_fmt):
|
|
161
99
|
"""Execute JMESPath query on DeviceBlueprint model and output result."""
|
|
162
100
|
import jmespath
|
|
@@ -299,7 +237,9 @@ def show(blueprint_file, fmt, apply_config, query_expr):
|
|
|
299
237
|
click.echo(bp.to_yaml())
|
|
300
238
|
|
|
301
239
|
if apply_config:
|
|
302
|
-
|
|
240
|
+
from ...config_apply import apply_config_file
|
|
241
|
+
from rich.console import Console
|
|
242
|
+
apply_config_file(apply_config, host=bp.host, console=Console())
|
|
303
243
|
|
|
304
244
|
|
|
305
245
|
# ── list ──────────────────────────────────────────────────────────────────────
|
|
@@ -29,7 +29,6 @@ from pathlib import Path
|
|
|
29
29
|
import click
|
|
30
30
|
from rich import box
|
|
31
31
|
from rich.console import Console
|
|
32
|
-
from rich.panel import Panel
|
|
33
32
|
from rich.table import Table
|
|
34
33
|
|
|
35
34
|
|
|
@@ -196,143 +195,6 @@ def device_map_cmd(
|
|
|
196
195
|
sys.exit(1)
|
|
197
196
|
|
|
198
197
|
|
|
199
|
-
def _apply_config_from_file(console, config_path, ssh_key):
|
|
200
|
-
"""Apply hardware/infra settings from YAML/JSON config file to the remote host.
|
|
201
|
-
|
|
202
|
-
Reads DeviceMap YAML/JSON and applies:
|
|
203
|
-
- Display transforms via wlr-randr + kanshi
|
|
204
|
-
- Backlight settings
|
|
205
|
-
- Other configurable hardware parameters
|
|
206
|
-
"""
|
|
207
|
-
import yaml
|
|
208
|
-
import json as _json
|
|
209
|
-
|
|
210
|
-
from ...detect.remote import RemoteProbe
|
|
211
|
-
|
|
212
|
-
with open(config_path) as f:
|
|
213
|
-
raw = f.read()
|
|
214
|
-
|
|
215
|
-
try:
|
|
216
|
-
cfg = yaml.safe_load(raw)
|
|
217
|
-
except Exception:
|
|
218
|
-
cfg = _json.loads(raw)
|
|
219
|
-
|
|
220
|
-
# Extract host from config or prompt
|
|
221
|
-
host = cfg.get("host")
|
|
222
|
-
if not host:
|
|
223
|
-
console.print("[red]✗ No 'host' field in config file[/red]")
|
|
224
|
-
sys.exit(1)
|
|
225
|
-
|
|
226
|
-
console.print(f"[cyan]→ Applying config from {config_path} to {host}[/cyan]")
|
|
227
|
-
|
|
228
|
-
# Connect to remote
|
|
229
|
-
p = RemoteProbe(host, ssh_key=ssh_key)
|
|
230
|
-
|
|
231
|
-
applied = 0
|
|
232
|
-
|
|
233
|
-
# ── display transforms ────────────────────────────────────────────────────
|
|
234
|
-
hardware = cfg.get("hardware", {})
|
|
235
|
-
for output in hardware.get("drm_outputs", []):
|
|
236
|
-
connector = output.get("connector", "")
|
|
237
|
-
transform = output.get("transform", "normal")
|
|
238
|
-
if not connector or not ("DSI" in connector or "HDMI" in connector):
|
|
239
|
-
continue
|
|
240
|
-
|
|
241
|
-
if transform == "normal":
|
|
242
|
-
console.print(f"[dim] skip {connector}: transform=normal (default)[/dim]")
|
|
243
|
-
continue
|
|
244
|
-
|
|
245
|
-
console.print(f"[cyan]→ {connector}: transform={transform}[/cyan]")
|
|
246
|
-
|
|
247
|
-
# Apply via wlr-randr
|
|
248
|
-
wlr_cmd = (
|
|
249
|
-
f"WAYLAND_DISPLAY=wayland-0 XDG_RUNTIME_DIR=/run/user/$(id -u) "
|
|
250
|
-
f"wlr-randr --output {connector} --transform {transform} 2>&1"
|
|
251
|
-
)
|
|
252
|
-
r = p.run(wlr_cmd)
|
|
253
|
-
if r.ok:
|
|
254
|
-
console.print(f"[green] ✓ wlr-randr applied[/green]")
|
|
255
|
-
applied += 1
|
|
256
|
-
else:
|
|
257
|
-
console.print(f"[yellow] ⚠ wlr-randr: {r.out.strip() or 'no output'}[/yellow]")
|
|
258
|
-
|
|
259
|
-
# ── persist DSI transforms in kanshi ─────────────────────────────────────────
|
|
260
|
-
dsi_outputs = [
|
|
261
|
-
o for o in hardware.get("drm_outputs", [])
|
|
262
|
-
if "DSI" in o.get("connector", "")
|
|
263
|
-
]
|
|
264
|
-
if dsi_outputs:
|
|
265
|
-
_update_kanshi_from_cfg(console, p, dsi_outputs)
|
|
266
|
-
|
|
267
|
-
# ── backlight ─────────────────────────────────────────────────────────────
|
|
268
|
-
for bl in hardware.get("backlights", []):
|
|
269
|
-
name = bl.get("name", "")
|
|
270
|
-
brightness = bl.get("brightness")
|
|
271
|
-
bl_power = bl.get("bl_power")
|
|
272
|
-
if not name:
|
|
273
|
-
continue
|
|
274
|
-
|
|
275
|
-
if brightness is not None:
|
|
276
|
-
r = p.run(f"echo {brightness} | sudo tee /sys/class/backlight/{name}/brightness > /dev/null")
|
|
277
|
-
if r.ok:
|
|
278
|
-
console.print(f"[green] ✓ backlight {name}: brightness={brightness}[/green]")
|
|
279
|
-
applied += 1
|
|
280
|
-
|
|
281
|
-
if bl_power is not None:
|
|
282
|
-
r = p.run(f"echo {bl_power} | sudo tee /sys/class/backlight/{name}/bl_power > /dev/null")
|
|
283
|
-
if r.ok:
|
|
284
|
-
console.print(f"[green] ✓ backlight {name}: bl_power={bl_power}[/green]")
|
|
285
|
-
applied += 1
|
|
286
|
-
|
|
287
|
-
if applied == 0:
|
|
288
|
-
console.print("[yellow]Nothing to apply — no relevant settings found in config[/yellow]")
|
|
289
|
-
else:
|
|
290
|
-
console.print(f"\n[bold green]✓ Config applied from {config_path}[/bold green]")
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
def _update_kanshi_from_cfg(console, p, dsi_outputs: list):
|
|
294
|
-
"""Rebuild kanshi profile from drm_outputs list and write to ~/.config/kanshi/config."""
|
|
295
|
-
import re as _re
|
|
296
|
-
|
|
297
|
-
kanshi_cfg_path = "~/.config/kanshi/config"
|
|
298
|
-
read_r = p.run(f"cat {kanshi_cfg_path} 2>/dev/null")
|
|
299
|
-
current = read_r.out if read_r.ok else ""
|
|
300
|
-
|
|
301
|
-
for output in dsi_outputs:
|
|
302
|
-
connector = output.get("connector", "")
|
|
303
|
-
transform = output.get("transform", "normal")
|
|
304
|
-
|
|
305
|
-
output_line_pat = _re.compile(
|
|
306
|
-
rf'(\s*output\s+{_re.escape(connector)}\s+enable)(\s+transform\s+\S+)?'
|
|
307
|
-
)
|
|
308
|
-
if _re.search(output_line_pat, current):
|
|
309
|
-
if transform == "normal":
|
|
310
|
-
current = _re.sub(output_line_pat, r'\1', current)
|
|
311
|
-
else:
|
|
312
|
-
current = _re.sub(output_line_pat, rf'\1 transform {transform}', current)
|
|
313
|
-
elif current.strip():
|
|
314
|
-
current = _re.sub(
|
|
315
|
-
rf'(\s*output\s+{_re.escape(connector)}\b)',
|
|
316
|
-
rf'\1 transform {transform}' if transform != "normal" else r'\1',
|
|
317
|
-
current,
|
|
318
|
-
)
|
|
319
|
-
else:
|
|
320
|
-
current = (
|
|
321
|
-
f"profile waveshare-only {{\n"
|
|
322
|
-
f" output {connector} enable"
|
|
323
|
-
+ (f" transform {transform}" if transform != "normal" else "")
|
|
324
|
-
+ "\n"
|
|
325
|
-
+ " output HDMI-A-2 disable\n"
|
|
326
|
-
+ "}\n"
|
|
327
|
-
)
|
|
328
|
-
|
|
329
|
-
escaped = current.replace("'", "'\\''")
|
|
330
|
-
write_r = p.run(f"mkdir -p ~/.config/kanshi && printf '%s' '{escaped}' > {kanshi_cfg_path}")
|
|
331
|
-
if write_r.ok:
|
|
332
|
-
console.print(f"[green] ✓ kanshi config updated ({kanshi_cfg_path})[/green]")
|
|
333
|
-
p.run("pkill -SIGUSR1 kanshi 2>/dev/null || true")
|
|
334
|
-
|
|
335
|
-
|
|
336
198
|
def _execute_query_device_map(console, dm, query_expr, output_fmt):
|
|
337
199
|
"""Execute JMESPath query on DeviceMap model and output result."""
|
|
338
200
|
import jmespath
|
|
@@ -360,122 +222,16 @@ def _execute_query_device_map(console, dm, query_expr, output_fmt):
|
|
|
360
222
|
# ── rendering helpers ─────────────────────────────────────────────────────────
|
|
361
223
|
|
|
362
224
|
|
|
363
|
-
_SEV_COLOR = {"info": "green", "warning": "yellow", "error": "red", "critical": "bold red"}
|
|
364
|
-
_SEV_ICON = {"info": "✓", "warning": "⚠", "error": "✗", "critical": "✗✗"}
|
|
365
|
-
|
|
366
|
-
|
|
367
225
|
def _render(console: Console, dm: "DeviceMap", fmt: str) -> None:
|
|
368
|
-
|
|
226
|
+
"""Dispatch to the appropriate renderer for *fmt*."""
|
|
227
|
+
from .device_map_renderers import render_json, render_rich, render_yaml
|
|
369
228
|
|
|
370
229
|
if fmt == "yaml":
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
import json as _json
|
|
375
|
-
click.echo(_json.dumps(dm.model_dump(mode="json"), indent=2))
|
|
376
|
-
return
|
|
377
|
-
|
|
378
|
-
# Rich
|
|
379
|
-
console.print()
|
|
380
|
-
tags_str = " ".join(f"[dim cyan]{t}[/dim cyan]" for t in dm.tags) or "[dim]-[/dim]"
|
|
381
|
-
console.print(Panel(
|
|
382
|
-
f"[bold]device-map[/bold] [cyan]{dm.host}[/cyan]\n"
|
|
383
|
-
f"Name: [white]{dm.name or '—'}[/white] "
|
|
384
|
-
f"Tags: {tags_str} "
|
|
385
|
-
f"Scanned: [dim]{dm.scanned_at.strftime('%Y-%m-%d %H:%M UTC')}[/dim]",
|
|
386
|
-
box=box.ROUNDED,
|
|
387
|
-
expand=False,
|
|
388
|
-
))
|
|
389
|
-
|
|
390
|
-
# Hardware section
|
|
391
|
-
hw = dm.hardware
|
|
392
|
-
if hw:
|
|
393
|
-
console.print(f"\n[bold]Board[/bold] {hw.board or '?'} Kernel: {hw.kernel or '?'}")
|
|
394
|
-
|
|
395
|
-
if hw.drm_outputs:
|
|
396
|
-
t = Table(title="Display (DRM)", show_header=True, box=box.SIMPLE, padding=(0, 1))
|
|
397
|
-
t.add_column("Connector")
|
|
398
|
-
t.add_column("Status")
|
|
399
|
-
t.add_column("Mode")
|
|
400
|
-
t.add_column("Transform")
|
|
401
|
-
t.add_column("Backlight")
|
|
402
|
-
for o in hw.drm_outputs:
|
|
403
|
-
sc = "green" if o.status == "connected" else "dim"
|
|
404
|
-
bl = next((b for b in hw.backlights if b.display_name == o.connector), None)
|
|
405
|
-
bl_str = f"{bl.brightness}/{bl.max_brightness}" if bl else "—"
|
|
406
|
-
t.add_row(
|
|
407
|
-
o.connector,
|
|
408
|
-
f"[{sc}]{o.status}[/{sc}]",
|
|
409
|
-
o.modes[0] if o.modes else "—",
|
|
410
|
-
o.transform,
|
|
411
|
-
bl_str,
|
|
412
|
-
)
|
|
413
|
-
console.print(t)
|
|
414
|
-
|
|
415
|
-
if hw.dsi_overlays:
|
|
416
|
-
console.print(f"[dim]Overlays:[/dim] {', '.join(hw.dsi_overlays)}")
|
|
417
|
-
if hw.i2c_buses:
|
|
418
|
-
buses = ", ".join(
|
|
419
|
-
f"i2c-{b.bus}" + (f"[{','.join(b.devices)}]" if b.devices else "")
|
|
420
|
-
for b in hw.i2c_buses
|
|
421
|
-
)
|
|
422
|
-
console.print(f"[dim]I2C:[/dim] {buses}")
|
|
423
|
-
|
|
424
|
-
# Infra section
|
|
425
|
-
infra = dm.infra
|
|
426
|
-
if infra:
|
|
427
|
-
rt = infra.runtime
|
|
428
|
-
runtimes = [
|
|
429
|
-
f"podman {rt.podman}" if rt.podman else None,
|
|
430
|
-
f"docker {rt.docker}" if rt.docker else None,
|
|
431
|
-
f"k3s {rt.k3s}" if rt.k3s else None,
|
|
432
|
-
]
|
|
433
|
-
rt_str = " ".join(r for r in runtimes if r) or "—"
|
|
434
|
-
console.print(f"\n[bold]Infra[/bold] strategy=[cyan]{infra.detected_strategy.value}[/cyan]"
|
|
435
|
-
f" version={infra.current_version or '?'} {rt_str}")
|
|
436
|
-
|
|
437
|
-
# Running services
|
|
438
|
-
podman_svcs = infra.services.get("podman", [])
|
|
439
|
-
systemd_svcs = infra.services.get("systemd", [])
|
|
440
|
-
all_svcs = podman_svcs + systemd_svcs
|
|
441
|
-
if all_svcs:
|
|
442
|
-
t = Table(title="Services", show_header=True, box=box.SIMPLE, padding=(0, 1))
|
|
443
|
-
t.add_column("Name")
|
|
444
|
-
t.add_column("Status")
|
|
445
|
-
t.add_column("Type")
|
|
446
|
-
for s in all_svcs:
|
|
447
|
-
sc = "green" if "running" in s.status.lower() or "active" in s.status.lower() else "red"
|
|
448
|
-
kind = "podman" if s in podman_svcs else "systemd"
|
|
449
|
-
t.add_row(s.name, f"[{sc}]{s.status}[/{sc}]", kind)
|
|
450
|
-
console.print(t)
|
|
451
|
-
|
|
452
|
-
# Issues
|
|
453
|
-
console.print()
|
|
454
|
-
if not dm.issues:
|
|
455
|
-
console.print("[green]✓ No issues detected[/green]")
|
|
230
|
+
render_yaml(dm)
|
|
231
|
+
elif fmt == "json":
|
|
232
|
+
render_json(dm)
|
|
456
233
|
else:
|
|
457
|
-
|
|
458
|
-
t.add_column("", width=3)
|
|
459
|
-
t.add_column("Source", style="dim")
|
|
460
|
-
t.add_column("Component")
|
|
461
|
-
t.add_column("Message")
|
|
462
|
-
|
|
463
|
-
for issue in dm.issues:
|
|
464
|
-
sev = issue.get("severity", "info")
|
|
465
|
-
color = _SEV_COLOR.get(sev, "white")
|
|
466
|
-
icon = _SEV_ICON.get(sev, "?")
|
|
467
|
-
t.add_row(
|
|
468
|
-
f"[{color}]{icon}[/{color}]",
|
|
469
|
-
issue.get("source", ""),
|
|
470
|
-
f"[{color}]{issue.get('component', '')}[/{color}]",
|
|
471
|
-
issue.get("message", ""),
|
|
472
|
-
)
|
|
473
|
-
console.print(t)
|
|
474
|
-
|
|
475
|
-
fixes = [i for i in dm.issues if i.get("fix")]
|
|
476
|
-
if fixes:
|
|
477
|
-
console.print(f"[dim] {len(fixes)} fix suggestion(s) — rerun with:[/dim] "
|
|
478
|
-
f"[cyan]redeploy hardware {dm.host} --fix[/cyan]")
|
|
234
|
+
render_rich(console, dm)
|
|
479
235
|
|
|
480
236
|
|
|
481
237
|
def _print_list(console: Console) -> None:
|