redeploy 0.2.42__tar.gz → 0.2.43__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.42 → redeploy-0.2.43}/PKG-INFO +6 -6
- {redeploy-0.2.42 → redeploy-0.2.43}/README.md +5 -5
- {redeploy-0.2.42 → redeploy-0.2.43}/pyproject.toml +1 -1
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/__init__.py +1 -1
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/apply/executor.py +4 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/apply/handlers.py +134 -0
- redeploy-0.2.43/redeploy/hardware/kiosk/__init__.py +18 -0
- redeploy-0.2.43/redeploy/hardware/kiosk/autostart.py +84 -0
- redeploy-0.2.43/redeploy/hardware/kiosk/browsers.py +78 -0
- redeploy-0.2.43/redeploy/hardware/kiosk/compositors.py +80 -0
- redeploy-0.2.43/redeploy/hardware/kiosk/output_profiles.py +68 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/models.py +3 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy.egg-info/PKG-INFO +6 -6
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy.egg-info/SOURCES.txt +6 -0
- redeploy-0.2.43/tests/test_hardware_kiosk.py +230 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/apply/__init__.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/apply/exceptions.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/apply/progress.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/apply/rollback.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/apply/state.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/apply/state_apply.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/apply/utils/__init__.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/apply/utils/run_container_build.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/audit.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/blueprint/__init__.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/blueprint/extractor.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/blueprint/generators/__init__.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/blueprint/generators/docker_compose.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/blueprint/generators/migration.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/cli/__init__.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/cli/commands/__init__.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/cli/commands/audit.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/cli/commands/blueprint.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/cli/commands/detect.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/cli/commands/device_map.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/cli/commands/devices.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/cli/commands/diagnose.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/cli/commands/diff.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/cli/commands/exec_.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/cli/commands/export.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/cli/commands/hardware.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/cli/commands/import_.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/cli/commands/init.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/cli/commands/inspect.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/cli/commands/patterns.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/cli/commands/plan_apply.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/cli/commands/plugin.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/cli/commands/probe.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/cli/commands/push.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/cli/commands/state.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/cli/commands/status.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/cli/commands/target.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/cli/commands/version/__init__.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/cli/commands/version/commands.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/cli/commands/version/helpers.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/cli/commands/version/monorepo.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/cli/commands/version/release.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/cli/commands/version/scanner.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/cli/commands/workflow.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/cli/core.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/cli/display.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/cli.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/data_sync.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/detect/__init__.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/detect/builtin/__init__.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/detect/detector.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/detect/hardware.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/detect/hardware_rules.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/detect/probes.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/detect/remote.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/detect/templates.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/detect/workflow.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/discovery.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/dsl/__init__.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/dsl/loader.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/dsl/parser.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/dsl_python/__init__.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/dsl_python/context.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/dsl_python/decorators.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/dsl_python/docker_steps.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/dsl_python/exceptions.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/dsl_python/runner.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/dsl_python/steps.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/fleet.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/hardware/__init__.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/hardware/config_txt.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/hardware/data/__init__.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/hardware/data/hyperpixel.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/hardware/data/official.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/hardware/data/waveshare.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/hardware/fixes.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/hardware/panels.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/hardware/raspi_config.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/iac/__init__.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/iac/base.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/iac/docker_compose.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/iac/parsers/__init__.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/iac/parsers/compose.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/iac/registry.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/markpact/__init__.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/markpact/compiler.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/markpact/models.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/markpact/parser.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/observe.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/parse.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/patterns.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/plan/__init__.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/plan/planner.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/plugins/__init__.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/plugins/builtin/__init__.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/plugins/builtin/browser_reload.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/plugins/builtin/hardware_diagnostic.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/plugins/builtin/notify.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/plugins/builtin/process_control.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/plugins/builtin/systemd_reload.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/spec_loader.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/ssh.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/steps.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/tests/__init__.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/tests/test_audit.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/tests/test_cli.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/tests/test_data_sync.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/tests/test_discovery.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/tests/test_dsl.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/tests/test_examples.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/tests/test_executor.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/tests/test_fleet.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/tests/test_git_integration.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/tests/test_iac.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/tests/test_manifest.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/tests/test_markpact_compiler.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/tests/test_markpact_parser.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/tests/test_models.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/tests/test_observe.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/tests/test_parse.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/tests/test_patterns.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/tests/test_planner_edge.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/tests/test_plugins.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/tests/test_probes.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/tests/test_public_api.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/tests/test_resume.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/tests/test_spec_loader.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/tests/test_ssh.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/tests/test_templates.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/tests/test_verify.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/tests/test_version.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/tests/test_version_changelog.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/tests/test_version_cli.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/tests/test_version_manifest.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/tests/test_version_sources.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/tests/test_workflow.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/verify.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/version/__init__.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/version/bump.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/version/changelog.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/version/commits.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/version/diff.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/version/git_integration.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/version/git_transaction.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/version/manifest.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/version/sources/__init__.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/version/sources/base.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/version/sources/json_.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/version/sources/plain.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/version/sources/regex.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/version/sources/toml_.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/version/sources/yaml_.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/version/transaction.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy/version.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy.egg-info/dependency_links.txt +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy.egg-info/entry_points.txt +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy.egg-info/requires.txt +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/redeploy.egg-info/top_level.txt +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/setup.cfg +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/tests/test_hardware_config.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/tests/test_hardware_rules.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/tests/test_iac.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/tests/test_parse.py +0 -0
- {redeploy-0.2.42 → redeploy-0.2.43}/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.43
|
|
4
4
|
Summary: Infrastructure migration toolkit: detect → plan → apply
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -28,17 +28,17 @@ Requires-Dist: pfix>=0.1.60; extra == "dev"
|
|
|
28
28
|
|
|
29
29
|
## AI Cost Tracking
|
|
30
30
|
|
|
31
|
-
     
|
|
32
|
+
  
|
|
33
33
|
|
|
34
|
-
- 🤖 **LLM usage:** $7.5000 (
|
|
35
|
-
- 👤 **Human dev:** ~$
|
|
34
|
+
- 🤖 **LLM usage:** $7.5000 (64 commits)
|
|
35
|
+
- 👤 **Human dev:** ~$2099 (21.0h @ $100/h, 30min dedup)
|
|
36
36
|
|
|
37
37
|
Generated on 2026-04-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
38
38
|
|
|
39
39
|
---
|
|
40
40
|
|
|
41
|
-
    
|
|
42
42
|
|
|
43
43
|
Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
|
|
44
44
|
|
|
@@ -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 (64 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$2099 (21.0h @ $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.43"
|
|
11
11
|
|
|
12
12
|
# ── Core models ───────────────────────────────────────────────────────────────
|
|
13
13
|
from .models import ( # noqa: F401
|
|
@@ -16,6 +16,7 @@ from .handlers import (
|
|
|
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
18
|
run_ensure_config_line, run_raspi_config,
|
|
19
|
+
run_ensure_kanshi_profile, run_ensure_autostart_entry, run_ensure_browser_kiosk_script,
|
|
19
20
|
)
|
|
20
21
|
from .progress import ProgressEmitter
|
|
21
22
|
from .rollback import rollback_steps
|
|
@@ -230,6 +231,9 @@ class Executor:
|
|
|
230
231
|
StepAction.INLINE_SCRIPT: lambda s: run_inline_script(s, self.probe, self.plan),
|
|
231
232
|
StepAction.ENSURE_CONFIG_LINE: lambda s: run_ensure_config_line(s, self.probe),
|
|
232
233
|
StepAction.RASPI_CONFIG: lambda s: run_raspi_config(s, self.probe),
|
|
234
|
+
StepAction.ENSURE_KANSHI_PROFILE: lambda s: run_ensure_kanshi_profile(s, self.probe),
|
|
235
|
+
StepAction.ENSURE_AUTOSTART_ENTRY: lambda s: run_ensure_autostart_entry(s, self.probe),
|
|
236
|
+
StepAction.ENSURE_BROWSER_KIOSK_SCRIPT: lambda s: run_ensure_browser_kiosk_script(s, self.probe),
|
|
233
237
|
}
|
|
234
238
|
|
|
235
239
|
handler = dispatch.get(step.action)
|
|
@@ -398,3 +398,137 @@ def run_raspi_config(step: MigrationStep, probe: "RemoteProbe") -> None:
|
|
|
398
398
|
|
|
399
399
|
step.status = StepStatus.DONE
|
|
400
400
|
step.result = f"applied: {cmd}"
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
# ── kiosk handlers ────────────────────────────────────────────────────────────
|
|
404
|
+
|
|
405
|
+
def run_ensure_kanshi_profile(step: MigrationStep, probe: "RemoteProbe") -> None:
|
|
406
|
+
"""Idempotently write or replace a named kanshi output profile.
|
|
407
|
+
|
|
408
|
+
step.command — the profile block to write (rendered kanshi syntax)
|
|
409
|
+
step.config_file — kanshi config path (default: ~/.config/kanshi/config)
|
|
410
|
+
"""
|
|
411
|
+
from ..hardware.kiosk.output_profiles import OutputProfile
|
|
412
|
+
|
|
413
|
+
profile_block = step.command
|
|
414
|
+
if not profile_block:
|
|
415
|
+
raise StepError(step, "ensure_kanshi_profile requires step.command with profile block")
|
|
416
|
+
|
|
417
|
+
config_path = step.config_file or "~/.config/kanshi/config"
|
|
418
|
+
mkdir_cmd = f"mkdir -p $(dirname {config_path})"
|
|
419
|
+
probe.run(mkdir_cmd, timeout=10)
|
|
420
|
+
|
|
421
|
+
r = probe.run(f"cat {config_path} 2>/dev/null || echo ''", timeout=10)
|
|
422
|
+
existing = r.out if r.ok else ""
|
|
423
|
+
|
|
424
|
+
# Extract profile name from block (first line: "profile <name> {")
|
|
425
|
+
first_line = profile_block.strip().splitlines()[0]
|
|
426
|
+
import re
|
|
427
|
+
m = re.match(r"profile\s+(\S+)\s*\{", first_line)
|
|
428
|
+
if not m:
|
|
429
|
+
raise StepError(step, f"Cannot parse profile name from block: {first_line!r}")
|
|
430
|
+
profile_name = m.group(1)
|
|
431
|
+
|
|
432
|
+
# Replace existing profile block or append
|
|
433
|
+
pattern = re.compile(
|
|
434
|
+
rf"^profile\s+{re.escape(profile_name)}\s*\{{[^}}]*\}}", re.MULTILINE | re.DOTALL
|
|
435
|
+
)
|
|
436
|
+
if pattern.search(existing):
|
|
437
|
+
if profile_block.strip() in existing:
|
|
438
|
+
step.status = StepStatus.DONE
|
|
439
|
+
step.result = f"no-op: profile '{profile_name}' already correct"
|
|
440
|
+
return
|
|
441
|
+
new_content = pattern.sub(profile_block.strip(), existing)
|
|
442
|
+
changed = True
|
|
443
|
+
else:
|
|
444
|
+
sep = "\n" if existing and not existing.endswith("\n") else ""
|
|
445
|
+
new_content = existing + sep + profile_block.strip() + "\n"
|
|
446
|
+
changed = True
|
|
447
|
+
|
|
448
|
+
encoded = base64.b64encode(new_content.encode()).decode()
|
|
449
|
+
tmp = f"/tmp/redeploy-kanshi-{step.id}.conf"
|
|
450
|
+
write_r = probe.run(
|
|
451
|
+
f"echo '{encoded}' | base64 -d | tee {tmp} > /dev/null && mv {tmp} {config_path}",
|
|
452
|
+
timeout=15,
|
|
453
|
+
)
|
|
454
|
+
if not write_r.ok:
|
|
455
|
+
raise StepError(step, f"Cannot write {config_path}: {write_r.stderr[:200]}")
|
|
456
|
+
|
|
457
|
+
# Reload kanshi if running
|
|
458
|
+
probe.run("pkill -SIGUSR1 kanshi 2>/dev/null || true", timeout=5)
|
|
459
|
+
|
|
460
|
+
step.status = StepStatus.DONE
|
|
461
|
+
step.result = f"profile '{profile_name}' written to {config_path}"
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def run_ensure_autostart_entry(step: MigrationStep, probe: "RemoteProbe") -> None:
|
|
465
|
+
"""Idempotently add or replace a keyed entry in a compositor autostart file.
|
|
466
|
+
|
|
467
|
+
step.config_file — path to autostart file
|
|
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
|
|
472
|
+
|
|
473
|
+
if not step.config_file or not step.config_line:
|
|
474
|
+
raise StepError(step, "ensure_autostart_entry requires config_file and config_line")
|
|
475
|
+
|
|
476
|
+
key = step.config_section or "redeploy"
|
|
477
|
+
entry = AutostartEntry(key=key, line=step.config_line)
|
|
478
|
+
|
|
479
|
+
r = probe.run(f"cat {step.config_file} 2>/dev/null || echo ''", timeout=10)
|
|
480
|
+
existing = r.out if r.ok else ""
|
|
481
|
+
|
|
482
|
+
new_content, changed = ensure_autostart_entry(existing, entry)
|
|
483
|
+
if not changed:
|
|
484
|
+
step.status = StepStatus.DONE
|
|
485
|
+
step.result = f"no-op: entry [{key}] already correct"
|
|
486
|
+
return
|
|
487
|
+
|
|
488
|
+
mkdir_cmd = f"mkdir -p $(dirname {step.config_file})"
|
|
489
|
+
probe.run(mkdir_cmd, timeout=10)
|
|
490
|
+
|
|
491
|
+
encoded = base64.b64encode(new_content.encode()).decode()
|
|
492
|
+
tmp = f"/tmp/redeploy-autostart-{step.id}.txt"
|
|
493
|
+
write_r = probe.run(
|
|
494
|
+
f"echo '{encoded}' | base64 -d | tee {tmp} > /dev/null && mv {tmp} {step.config_file}",
|
|
495
|
+
timeout=15,
|
|
496
|
+
)
|
|
497
|
+
if not write_r.ok:
|
|
498
|
+
raise StepError(step, f"Cannot write {step.config_file}: {write_r.stderr[:200]}")
|
|
499
|
+
|
|
500
|
+
step.status = StepStatus.DONE
|
|
501
|
+
step.result = f"entry [{key}] written to {step.config_file}"
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def run_ensure_browser_kiosk_script(step: MigrationStep, probe: "RemoteProbe") -> None:
|
|
505
|
+
"""Write a kiosk-launch.sh script to the remote device.
|
|
506
|
+
|
|
507
|
+
step.command — the full script content to write
|
|
508
|
+
step.dst — destination path (default: ~/kiosk-launch.sh)
|
|
509
|
+
"""
|
|
510
|
+
script_content = step.command
|
|
511
|
+
if not script_content:
|
|
512
|
+
raise StepError(step, "ensure_browser_kiosk_script requires step.command with script body")
|
|
513
|
+
|
|
514
|
+
dst = step.dst or "~/kiosk-launch.sh"
|
|
515
|
+
|
|
516
|
+
r = probe.run(f"cat {dst} 2>/dev/null || echo ''", timeout=10)
|
|
517
|
+
existing = r.out if r.ok else ""
|
|
518
|
+
if existing.strip() == script_content.strip():
|
|
519
|
+
step.status = StepStatus.DONE
|
|
520
|
+
step.result = f"no-op: {dst} already correct"
|
|
521
|
+
return
|
|
522
|
+
|
|
523
|
+
encoded = base64.b64encode(script_content.encode()).decode()
|
|
524
|
+
tmp = f"/tmp/redeploy-kiosk-{step.id}.sh"
|
|
525
|
+
write_r = probe.run(
|
|
526
|
+
f"echo '{encoded}' | base64 -d | tee {tmp} > /dev/null"
|
|
527
|
+
f" && chmod +x {tmp} && mv {tmp} {dst}",
|
|
528
|
+
timeout=15,
|
|
529
|
+
)
|
|
530
|
+
if not write_r.ok:
|
|
531
|
+
raise StepError(step, f"Cannot write {dst}: {write_r.stderr[:200]}")
|
|
532
|
+
|
|
533
|
+
step.status = StepStatus.DONE
|
|
534
|
+
step.result = f"kiosk script written to {dst}"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Kiosk display configuration helpers — encoding pi109 session knowledge."""
|
|
2
|
+
|
|
3
|
+
from .autostart import AutostartEntry, ensure_autostart_entry, generate_labwc_autostart
|
|
4
|
+
from .browsers import BrowserKioskProfile, CHROMIUM_WAYLAND_KIOSK
|
|
5
|
+
from .compositors import CompositorDefinition, LABWC
|
|
6
|
+
from .output_profiles import OutputProfile, dsi_only_profile
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"AutostartEntry",
|
|
10
|
+
"ensure_autostart_entry",
|
|
11
|
+
"generate_labwc_autostart",
|
|
12
|
+
"BrowserKioskProfile",
|
|
13
|
+
"CHROMIUM_WAYLAND_KIOSK",
|
|
14
|
+
"CompositorDefinition",
|
|
15
|
+
"LABWC",
|
|
16
|
+
"OutputProfile",
|
|
17
|
+
"dsi_only_profile",
|
|
18
|
+
]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Autostart file management for kiosk compositors.
|
|
2
|
+
|
|
3
|
+
Encodes the pi109 lesson: labwc autostart order is critical.
|
|
4
|
+
1. kanshid & — start output manager
|
|
5
|
+
2. sleep 3 — wait for kanshi to disable HDMI-A-2
|
|
6
|
+
3. bash ~/kiosk-launch.sh & — start browser AFTER output is configured
|
|
7
|
+
|
|
8
|
+
If Chromium starts before kanshi applies the DSI-only profile, it may render
|
|
9
|
+
on HDMI-A-2 (which would be blank since no monitor is connected).
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class AutostartEntry:
|
|
19
|
+
"""One entry in a compositor autostart file."""
|
|
20
|
+
|
|
21
|
+
key: str
|
|
22
|
+
"""Unique identifier used for idempotent replacement, e.g. 'kiosk-browser'."""
|
|
23
|
+
|
|
24
|
+
line: str
|
|
25
|
+
"""The shell line to write, e.g. 'bash ~/kiosk-launch.sh &'."""
|
|
26
|
+
|
|
27
|
+
comment: str = ""
|
|
28
|
+
"""Optional inline comment explaining the entry."""
|
|
29
|
+
|
|
30
|
+
def render(self) -> str:
|
|
31
|
+
"""Render as a single autostart file line."""
|
|
32
|
+
if self.comment:
|
|
33
|
+
return f"{self.line} # [{self.key}] {self.comment}"
|
|
34
|
+
return f"{self.line} # [{self.key}]"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def ensure_autostart_entry(content: str, entry: AutostartEntry) -> tuple[str, bool]:
|
|
38
|
+
"""Idempotently add or replace an entry in an autostart file.
|
|
39
|
+
|
|
40
|
+
Looks for a line containing ``[key]`` marker. If found, replaces it.
|
|
41
|
+
If not, appends it. Returns (new_content, changed).
|
|
42
|
+
"""
|
|
43
|
+
marker = f"[{entry.key}]"
|
|
44
|
+
rendered = entry.render()
|
|
45
|
+
lines = content.splitlines(keepends=True)
|
|
46
|
+
for i, line in enumerate(lines):
|
|
47
|
+
if marker in line:
|
|
48
|
+
if line.rstrip("\n") == rendered:
|
|
49
|
+
return content, False # already correct
|
|
50
|
+
lines[i] = rendered + "\n"
|
|
51
|
+
return "".join(lines), True
|
|
52
|
+
# Not found — append
|
|
53
|
+
sep = "\n" if content and not content.endswith("\n") else ""
|
|
54
|
+
return content + sep + rendered + "\n", True
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def generate_labwc_autostart(
|
|
58
|
+
kiosk_script: str = "~/kiosk-launch.sh",
|
|
59
|
+
kanshi_settle_secs: int = 3,
|
|
60
|
+
extra_entries: list[AutostartEntry] | None = None,
|
|
61
|
+
) -> str:
|
|
62
|
+
"""Generate a complete labwc autostart file for a kiosk deployment.
|
|
63
|
+
|
|
64
|
+
Encodes the pi109 ordering: kanshi → sleep → browser.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
kiosk_script: Path to the kiosk launch shell script.
|
|
68
|
+
kanshi_settle_secs: Seconds to wait after starting kanshi.
|
|
69
|
+
Pi109 used sleep 3 for the Waveshare 8" panel.
|
|
70
|
+
extra_entries: Additional autostart entries appended at the end.
|
|
71
|
+
"""
|
|
72
|
+
lines = [
|
|
73
|
+
"# Generated by redeploy hardware/kiosk — do not edit manually",
|
|
74
|
+
"# Pi109 ordering: kanshi first, then browser after settle time",
|
|
75
|
+
"",
|
|
76
|
+
"kanshid & # [kanshi] output manager (disables HDMI before browser starts)",
|
|
77
|
+
f"sleep {kanshi_settle_secs} # [kanshi-settle] wait for kanshi DSI-only profile",
|
|
78
|
+
f"bash {kiosk_script} & # [kiosk-browser] launch browser in kiosk mode",
|
|
79
|
+
]
|
|
80
|
+
if extra_entries:
|
|
81
|
+
lines.append("")
|
|
82
|
+
for e in extra_entries:
|
|
83
|
+
lines.append(e.render())
|
|
84
|
+
return "\n".join(lines) + "\n"
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Browser kiosk profiles for Wayland kiosk deployments.
|
|
2
|
+
|
|
3
|
+
Encodes browser flag knowledge from pi109 debugging session.
|
|
4
|
+
|
|
5
|
+
Pi109 critical lessons:
|
|
6
|
+
- GNOME Keyring intercepts first Chromium launch without --password-store=basic
|
|
7
|
+
causing a blocking dialog that freezes the kiosk silently on RPi OS.
|
|
8
|
+
- --windowed is incompatible with --kiosk under labwc; it must be removed.
|
|
9
|
+
- --noerrdialogs prevents crash dialogs on recoverable GPU errors.
|
|
10
|
+
- --disable-infobars removes the "Chrome is being controlled" banner.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class BrowserKioskProfile:
|
|
19
|
+
"""Static definition of a browser kiosk launch profile."""
|
|
20
|
+
|
|
21
|
+
id: str
|
|
22
|
+
"""Short identifier, e.g. 'chromium-wayland'."""
|
|
23
|
+
|
|
24
|
+
binary: str
|
|
25
|
+
"""Binary name as found in PATH, e.g. 'chromium-browser'."""
|
|
26
|
+
|
|
27
|
+
required_flags: list[str] = field(default_factory=list)
|
|
28
|
+
"""Flags that MUST be present for a functional kiosk session."""
|
|
29
|
+
|
|
30
|
+
incompatible_flags: list[str] = field(default_factory=list)
|
|
31
|
+
"""Flags that break kiosk mode; raise if detected in a spec."""
|
|
32
|
+
|
|
33
|
+
optional_flags: list[str] = field(default_factory=list)
|
|
34
|
+
"""Recommended but not strictly required."""
|
|
35
|
+
|
|
36
|
+
notes: list[str] = field(default_factory=list)
|
|
37
|
+
"""Human-readable caveats from real debugging."""
|
|
38
|
+
|
|
39
|
+
def build_launch_cmd(self, url: str, extra_flags: list[str] | None = None) -> str:
|
|
40
|
+
"""Build the Chromium launch command string for a kiosk-launch.sh."""
|
|
41
|
+
flags = list(self.required_flags)
|
|
42
|
+
flags += extra_flags or []
|
|
43
|
+
bad = [f for f in flags if f in self.incompatible_flags]
|
|
44
|
+
if bad:
|
|
45
|
+
raise ValueError(
|
|
46
|
+
f"Incompatible flags detected for {self.id!r}: {bad}\n"
|
|
47
|
+
f"Remove them: {self.incompatible_flags}"
|
|
48
|
+
)
|
|
49
|
+
return f"{self.binary} {' '.join(flags)} {url}"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
CHROMIUM_WAYLAND_KIOSK = BrowserKioskProfile(
|
|
53
|
+
id="chromium-wayland",
|
|
54
|
+
binary="chromium-browser",
|
|
55
|
+
required_flags=[
|
|
56
|
+
"--kiosk",
|
|
57
|
+
"--password-store=basic", # Pi109: GNOME Keyring blocks without this
|
|
58
|
+
"--noerrdialogs",
|
|
59
|
+
"--disable-infobars",
|
|
60
|
+
"--disable-session-crashed-bubble",
|
|
61
|
+
"--disable-features=TranslateUI",
|
|
62
|
+
"--ozone-platform=wayland", # explicit; prevents X11 fallback
|
|
63
|
+
"--enable-features=UseOzonePlatform",
|
|
64
|
+
],
|
|
65
|
+
incompatible_flags=[
|
|
66
|
+
"--windowed", # Pi109: conflicts with --kiosk under labwc
|
|
67
|
+
"--start-maximized", # overrides kiosk geometry
|
|
68
|
+
],
|
|
69
|
+
optional_flags=[
|
|
70
|
+
"--check-for-update-interval=1", # disable auto-update check noise
|
|
71
|
+
"--incognito", # stateless sessions
|
|
72
|
+
],
|
|
73
|
+
notes=[
|
|
74
|
+
"Pi109: --password-store=basic is MANDATORY on RPi OS with GNOME Keyring",
|
|
75
|
+
"--windowed + --kiosk causes transparent/non-fullscreen window under labwc",
|
|
76
|
+
"Set DISPLAY unset; XDG_SESSION_TYPE=wayland must be exported before launch",
|
|
77
|
+
],
|
|
78
|
+
)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Compositor definitions for kiosk deployments.
|
|
2
|
+
|
|
3
|
+
Encodes compositor-specific knowledge: where autostart lives, which output
|
|
4
|
+
manager is used, what environment variables are required.
|
|
5
|
+
|
|
6
|
+
Pi109 lesson: labwc uses kanshi for output management. The compositor must be
|
|
7
|
+
started BEFORE kanshi, which must settle BEFORE Chromium starts.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class CompositorDefinition:
|
|
16
|
+
"""Static definition of a Wayland compositor for kiosk use."""
|
|
17
|
+
|
|
18
|
+
id: str
|
|
19
|
+
"""Short identifier, e.g. 'labwc'."""
|
|
20
|
+
|
|
21
|
+
autostart_path: str
|
|
22
|
+
"""Path to the autostart file (relative to $HOME or absolute)."""
|
|
23
|
+
|
|
24
|
+
output_manager: str
|
|
25
|
+
"""Tool used to manage outputs: 'kanshi', 'wlr-randr', 'none'."""
|
|
26
|
+
|
|
27
|
+
required_packages: list[str] = field(default_factory=list)
|
|
28
|
+
"""Packages that must be present on the target device."""
|
|
29
|
+
|
|
30
|
+
required_env: list[str] = field(default_factory=list)
|
|
31
|
+
"""Environment variables a kiosk launcher script must export."""
|
|
32
|
+
|
|
33
|
+
notes: list[str] = field(default_factory=list)
|
|
34
|
+
"""Human-readable caveats encoded from real debugging sessions."""
|
|
35
|
+
|
|
36
|
+
def autostart_abs(self, home: str = "/home/pi") -> str:
|
|
37
|
+
path = self.autostart_path
|
|
38
|
+
if path.startswith("~"):
|
|
39
|
+
path = home + path[1:]
|
|
40
|
+
return path
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
LABWC = CompositorDefinition(
|
|
44
|
+
id="labwc",
|
|
45
|
+
autostart_path="~/.config/labwc/autostart",
|
|
46
|
+
output_manager="kanshi",
|
|
47
|
+
required_packages=["labwc", "kanshi", "wlr-randr"],
|
|
48
|
+
required_env=["WAYLAND_DISPLAY", "XDG_RUNTIME_DIR", "XDG_SESSION_TYPE"],
|
|
49
|
+
notes=[
|
|
50
|
+
# Pi109 session knowledge:
|
|
51
|
+
"kanshi must be started BEFORE Chromium (it disables HDMI-A-2)",
|
|
52
|
+
"sleep 3 between kanshid and browser start allows kanshi to apply profile",
|
|
53
|
+
"labwc autostart order: kanshid & → sleep 3 → bash ~/kiosk-launch.sh &",
|
|
54
|
+
"--windowed flag conflicts with --kiosk under labwc; remove it",
|
|
55
|
+
"XDG_SESSION_TYPE=wayland must be set or Chromium falls back to X11",
|
|
56
|
+
],
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
WESTON = CompositorDefinition(
|
|
60
|
+
id="weston",
|
|
61
|
+
autostart_path="~/.config/weston.ini",
|
|
62
|
+
output_manager="weston",
|
|
63
|
+
required_packages=["weston"],
|
|
64
|
+
required_env=["WAYLAND_DISPLAY", "XDG_RUNTIME_DIR"],
|
|
65
|
+
notes=[],
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
SWAY = CompositorDefinition(
|
|
69
|
+
id="sway",
|
|
70
|
+
autostart_path="~/.config/sway/config",
|
|
71
|
+
output_manager="sway",
|
|
72
|
+
required_packages=["sway", "swaybar"],
|
|
73
|
+
required_env=["WAYLAND_DISPLAY", "XDG_RUNTIME_DIR", "XDG_SESSION_TYPE"],
|
|
74
|
+
notes=[],
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
#: registry: compositor id → definition
|
|
78
|
+
COMPOSITORS: dict[str, CompositorDefinition] = {
|
|
79
|
+
c.id: c for c in [LABWC, WESTON, SWAY]
|
|
80
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Kanshi output profile generation for kiosk deployments.
|
|
2
|
+
|
|
3
|
+
Encodes the pi109 lesson: HDMI outputs must be explicitly disabled in the
|
|
4
|
+
kanshi profile so the DSI panel is the only active output BEFORE Chromium is
|
|
5
|
+
launched. If HDMI-A-2 is enabled, Chromium may open on it instead of DSI-2.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class OutputProfile:
|
|
14
|
+
"""A kanshi output profile definition."""
|
|
15
|
+
|
|
16
|
+
name: str
|
|
17
|
+
"""Profile name used in kanshi config, e.g. 'waveshare-only'."""
|
|
18
|
+
|
|
19
|
+
enabled: list[str] = field(default_factory=list)
|
|
20
|
+
"""Connector names to enable, e.g. ['DSI-2']."""
|
|
21
|
+
|
|
22
|
+
disabled: list[str] = field(default_factory=list)
|
|
23
|
+
"""Connector names to explicitly disable, e.g. ['HDMI-A-2']."""
|
|
24
|
+
|
|
25
|
+
transforms: dict[str, str] = field(default_factory=dict)
|
|
26
|
+
"""Optional per-output transform, e.g. {'DSI-2': '90'}."""
|
|
27
|
+
|
|
28
|
+
modes: dict[str, str] = field(default_factory=dict)
|
|
29
|
+
"""Optional per-output mode override, e.g. {'DSI-2': '800x480'}."""
|
|
30
|
+
|
|
31
|
+
def to_kanshi_config(self) -> str:
|
|
32
|
+
"""Render the kanshi profile block as a string."""
|
|
33
|
+
lines = [f"profile {self.name} {{"]
|
|
34
|
+
for conn in self.enabled:
|
|
35
|
+
parts = [f" output {conn} enable"]
|
|
36
|
+
if conn in self.modes:
|
|
37
|
+
parts.append(f"mode {self.modes[conn]}")
|
|
38
|
+
if conn in self.transforms:
|
|
39
|
+
parts.append(f"transform {self.transforms[conn]}")
|
|
40
|
+
lines.append(" ".join(parts))
|
|
41
|
+
for conn in self.disabled:
|
|
42
|
+
lines.append(f" output {conn} disable")
|
|
43
|
+
lines.append("}")
|
|
44
|
+
return "\n".join(lines)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def dsi_only_profile(
|
|
48
|
+
dsi_connector: str = "DSI-2",
|
|
49
|
+
hdmi_connectors: list[str] | None = None,
|
|
50
|
+
profile_name: str = "waveshare-only",
|
|
51
|
+
transform: str | None = None,
|
|
52
|
+
) -> OutputProfile:
|
|
53
|
+
"""Factory: DSI panel enabled, all HDMI outputs disabled.
|
|
54
|
+
|
|
55
|
+
Pi109 knowledge: RPi 5 exposes HDMI-A-1 and HDMI-A-2. Both must be
|
|
56
|
+
disabled in kanshi so Chromium opens on DSI-2.
|
|
57
|
+
"""
|
|
58
|
+
if hdmi_connectors is None:
|
|
59
|
+
hdmi_connectors = ["HDMI-A-1", "HDMI-A-2"]
|
|
60
|
+
transforms = {}
|
|
61
|
+
if transform:
|
|
62
|
+
transforms[dsi_connector] = transform
|
|
63
|
+
return OutputProfile(
|
|
64
|
+
name=profile_name,
|
|
65
|
+
enabled=[dsi_connector],
|
|
66
|
+
disabled=hdmi_connectors,
|
|
67
|
+
transforms=transforms,
|
|
68
|
+
)
|
|
@@ -41,6 +41,9 @@ class StepAction(str, Enum):
|
|
|
41
41
|
INLINE_SCRIPT = "inline_script" # multiline bash script from YAML
|
|
42
42
|
ENSURE_CONFIG_LINE = "ensure_config_line" # idempotent config.txt editing
|
|
43
43
|
RASPI_CONFIG = "raspi_config" # raspi-config nonint wrapper
|
|
44
|
+
ENSURE_KANSHI_PROFILE = "ensure_kanshi_profile" # write/replace kanshi output profile
|
|
45
|
+
ENSURE_AUTOSTART_ENTRY = "ensure_autostart_entry" # idempotent autostart line
|
|
46
|
+
ENSURE_BROWSER_KIOSK_SCRIPT = "ensure_browser_kiosk_script" # write kiosk-launch.sh
|
|
44
47
|
|
|
45
48
|
|
|
46
49
|
class StepStatus(str, Enum):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: redeploy
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.43
|
|
4
4
|
Summary: Infrastructure migration toolkit: detect → plan → apply
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -28,17 +28,17 @@ Requires-Dist: pfix>=0.1.60; extra == "dev"
|
|
|
28
28
|
|
|
29
29
|
## AI Cost Tracking
|
|
30
30
|
|
|
31
|
-
     
|
|
32
|
+
  
|
|
33
33
|
|
|
34
|
-
- 🤖 **LLM usage:** $7.5000 (
|
|
35
|
-
- 👤 **Human dev:** ~$
|
|
34
|
+
- 🤖 **LLM usage:** $7.5000 (64 commits)
|
|
35
|
+
- 👤 **Human dev:** ~$2099 (21.0h @ $100/h, 30min dedup)
|
|
36
36
|
|
|
37
37
|
Generated on 2026-04-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
38
38
|
|
|
39
39
|
---
|
|
40
40
|
|
|
41
|
-
    
|
|
42
42
|
|
|
43
43
|
Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
|
|
44
44
|
|
|
@@ -96,6 +96,11 @@ redeploy/hardware/data/__init__.py
|
|
|
96
96
|
redeploy/hardware/data/hyperpixel.py
|
|
97
97
|
redeploy/hardware/data/official.py
|
|
98
98
|
redeploy/hardware/data/waveshare.py
|
|
99
|
+
redeploy/hardware/kiosk/__init__.py
|
|
100
|
+
redeploy/hardware/kiosk/autostart.py
|
|
101
|
+
redeploy/hardware/kiosk/browsers.py
|
|
102
|
+
redeploy/hardware/kiosk/compositors.py
|
|
103
|
+
redeploy/hardware/kiosk/output_profiles.py
|
|
99
104
|
redeploy/iac/__init__.py
|
|
100
105
|
redeploy/iac/base.py
|
|
101
106
|
redeploy/iac/docker_compose.py
|
|
@@ -165,6 +170,7 @@ redeploy/version/sources/regex.py
|
|
|
165
170
|
redeploy/version/sources/toml_.py
|
|
166
171
|
redeploy/version/sources/yaml_.py
|
|
167
172
|
tests/test_hardware_config.py
|
|
173
|
+
tests/test_hardware_kiosk.py
|
|
168
174
|
tests/test_hardware_rules.py
|
|
169
175
|
tests/test_iac.py
|
|
170
176
|
tests/test_parse.py
|