redeploy 0.2.40__tar.gz → 0.2.41__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.40 → redeploy-0.2.41}/PKG-INFO +6 -6
- {redeploy-0.2.40 → redeploy-0.2.41}/README.md +5 -5
- {redeploy-0.2.40 → redeploy-0.2.41}/pyproject.toml +1 -1
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/__init__.py +1 -1
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/blueprint.py +89 -6
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/device_map.py +154 -16
- redeploy-0.2.41/redeploy/cli/commands/diagnose.py +47 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/diff.py +1 -5
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/export.py +10 -10
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/hardware.py +241 -8
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/detect/hardware.py +71 -1
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/detect/hardware_rules.py +227 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/models.py +20 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy.egg-info/PKG-INFO +6 -6
- {redeploy-0.2.40 → redeploy-0.2.41}/tests/test_hardware_rules.py +3 -3
- redeploy-0.2.40/redeploy/cli/commands/diagnose.py +0 -88
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/apply/__init__.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/apply/exceptions.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/apply/executor.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/apply/handlers.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/apply/progress.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/apply/rollback.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/apply/state.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/apply/utils/__init__.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/apply/utils/run_container_build.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/audit.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/blueprint/__init__.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/blueprint/extractor.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/blueprint/generators/__init__.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/blueprint/generators/docker_compose.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/blueprint/generators/migration.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/__init__.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/__init__.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/audit.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/detect.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/devices.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/exec_.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/import_.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/init.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/inspect.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/patterns.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/plan_apply.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/plugin.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/probe.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/state.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/status.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/target.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/version/__init__.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/version/commands.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/version/helpers.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/version/monorepo.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/version/release.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/version/scanner.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/commands/workflow.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/core.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli/display.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/cli.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/data_sync.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/detect/__init__.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/detect/builtin/__init__.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/detect/detector.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/detect/probes.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/detect/remote.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/detect/templates.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/detect/workflow.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/discovery.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/dsl/__init__.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/dsl/loader.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/dsl/parser.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/dsl_python/__init__.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/dsl_python/context.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/dsl_python/decorators.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/dsl_python/docker_steps.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/dsl_python/exceptions.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/dsl_python/runner.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/dsl_python/steps.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/fleet.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/hardware/__init__.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/hardware/config_txt.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/hardware/data/__init__.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/hardware/data/hyperpixel.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/hardware/data/official.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/hardware/data/waveshare.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/hardware/fixes.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/hardware/panels.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/hardware/raspi_config.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/iac/__init__.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/iac/base.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/iac/docker_compose.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/iac/parsers/__init__.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/iac/parsers/compose.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/iac/registry.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/markpact/__init__.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/markpact/compiler.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/markpact/models.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/markpact/parser.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/observe.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/parse.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/patterns.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/plan/__init__.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/plan/planner.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/plugins/__init__.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/plugins/builtin/__init__.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/plugins/builtin/browser_reload.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/plugins/builtin/hardware_diagnostic.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/plugins/builtin/notify.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/plugins/builtin/process_control.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/plugins/builtin/systemd_reload.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/spec_loader.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/ssh.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/steps.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/__init__.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_audit.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_cli.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_data_sync.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_discovery.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_dsl.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_examples.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_executor.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_fleet.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_git_integration.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_iac.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_manifest.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_markpact_compiler.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_markpact_parser.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_models.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_observe.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_parse.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_patterns.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_planner_edge.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_plugins.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_probes.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_public_api.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_resume.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_spec_loader.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_ssh.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_templates.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_verify.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_version.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_version_changelog.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_version_cli.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_version_manifest.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_version_sources.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/tests/test_workflow.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/verify.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/version/__init__.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/version/bump.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/version/changelog.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/version/commits.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/version/diff.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/version/git_integration.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/version/git_transaction.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/version/manifest.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/version/sources/__init__.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/version/sources/base.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/version/sources/json_.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/version/sources/plain.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/version/sources/regex.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/version/sources/toml_.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/version/sources/yaml_.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/version/transaction.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy/version.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy.egg-info/SOURCES.txt +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy.egg-info/dependency_links.txt +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy.egg-info/entry_points.txt +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy.egg-info/requires.txt +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/redeploy.egg-info/top_level.txt +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/setup.cfg +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/tests/test_hardware_config.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/tests/test_iac.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/tests/test_parse.py +0 -0
- {redeploy-0.2.40 → redeploy-0.2.41}/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.41
|
|
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 (62 commits)
|
|
34
|
+
- 👤 **Human dev:** ~$2044 (20.4h @ $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 (62 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$2044 (20.4h @ $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.41"
|
|
11
11
|
|
|
12
12
|
# ── Core models ───────────────────────────────────────────────────────────────
|
|
13
13
|
from .models import ( # noqa: F401
|
|
@@ -53,7 +53,7 @@ def blueprint_cmd():
|
|
|
53
53
|
@click.option("--save", is_flag=True, help="Save to ~/.config/redeploy/blueprints/")
|
|
54
54
|
@click.option("--out", "out_path", default=None, type=click.Path(),
|
|
55
55
|
help="Save to specific file instead")
|
|
56
|
-
@click.option("--format", "fmt", default="yaml", type=click.Choice(["
|
|
56
|
+
@click.option("--format", "fmt", default="yaml", type=click.Choice(["yaml", "json"]))
|
|
57
57
|
@click.option("--tag", "tags", multiple=True)
|
|
58
58
|
def capture(host, name, compose_files, migration_file, device_map_file,
|
|
59
59
|
live, save, out_path, fmt, tags):
|
|
@@ -83,7 +83,11 @@ def capture(host, name, compose_files, migration_file, device_map_file,
|
|
|
83
83
|
host=host,
|
|
84
84
|
)
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
if fmt == "json":
|
|
87
|
+
import json as _json
|
|
88
|
+
click.echo(_json.dumps(bp.model_dump(mode="json"), indent=2))
|
|
89
|
+
else:
|
|
90
|
+
click.echo(bp.to_yaml())
|
|
87
91
|
|
|
88
92
|
if save or out_path:
|
|
89
93
|
path = Path(out_path) if out_path else None
|
|
@@ -91,6 +95,68 @@ def capture(host, name, compose_files, migration_file, device_map_file,
|
|
|
91
95
|
console.print(f"\n[green]✓ saved:[/green] {saved}")
|
|
92
96
|
|
|
93
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
|
+
|
|
94
160
|
# ── twin ──────────────────────────────────────────────────────────────────────
|
|
95
161
|
|
|
96
162
|
@blueprint_cmd.command("twin")
|
|
@@ -180,13 +246,30 @@ def deploy(blueprint_file, target_host, out_path, remote_dir, no_transfer,
|
|
|
180
246
|
|
|
181
247
|
@blueprint_cmd.command("show")
|
|
182
248
|
@click.argument("blueprint_file", type=click.Path(exists=True))
|
|
183
|
-
@click.option("--format", "fmt", default="yaml", type=click.Choice(["
|
|
184
|
-
|
|
185
|
-
|
|
249
|
+
@click.option("--format", "fmt", default="yaml", type=click.Choice(["yaml", "json"]))
|
|
250
|
+
@click.option("--apply-config", "apply_config", default=None,
|
|
251
|
+
type=click.Path(exists=True, dir_okay=False),
|
|
252
|
+
help="Apply blueprint settings from YAML config file to the remote host")
|
|
253
|
+
def show(blueprint_file, fmt, apply_config):
|
|
254
|
+
"""Display a saved DeviceBlueprint.
|
|
255
|
+
|
|
256
|
+
\b
|
|
257
|
+
Config-file workflow (scan → edit → apply):
|
|
258
|
+
redeploy blueprint capture pi@192.168.188.109 > blueprint.yaml
|
|
259
|
+
# edit blueprint.yaml: set hardware.drm_outputs[0].transform: '270'
|
|
260
|
+
redeploy blueprint show blueprint.yaml --apply-config blueprint.yaml
|
|
261
|
+
"""
|
|
186
262
|
from ...models import DeviceBlueprint
|
|
187
263
|
|
|
188
264
|
bp = DeviceBlueprint.load(Path(blueprint_file))
|
|
189
|
-
|
|
265
|
+
if fmt == "json":
|
|
266
|
+
import json as _json
|
|
267
|
+
click.echo(_json.dumps(bp.model_dump(mode="json"), indent=2))
|
|
268
|
+
else:
|
|
269
|
+
click.echo(bp.to_yaml())
|
|
270
|
+
|
|
271
|
+
if apply_config:
|
|
272
|
+
_apply_blueprint_config(bp, apply_config)
|
|
190
273
|
|
|
191
274
|
|
|
192
275
|
# ── list ──────────────────────────────────────────────────────────────────────
|
|
@@ -39,22 +39,21 @@ from rich.table import Table
|
|
|
39
39
|
@click.option("--tag", "tags", multiple=True, help="Tag(s) to attach (repeatable)")
|
|
40
40
|
@click.option("--save", is_flag=True, help="Persist map to ~/.config/redeploy/device-maps/")
|
|
41
41
|
@click.option("--out", "out_path", default=None, type=click.Path(), help="Save to specific file")
|
|
42
|
-
@click.option(
|
|
43
|
-
|
|
44
|
-
default="yaml",
|
|
45
|
-
type=click.Choice(["rich", "yaml", "json"]),
|
|
46
|
-
help="Output format",
|
|
47
|
-
)
|
|
42
|
+
@click.option("--format", "output_fmt", default="yaml",
|
|
43
|
+
type=click.Choice(["yaml", "json"]), help="Output format (default: yaml)")
|
|
48
44
|
@click.option("--no-infra", is_flag=True, help="Skip infra probe (faster — hardware only)")
|
|
49
45
|
@click.option("--list", "list_saved", is_flag=True, help="List saved device maps")
|
|
50
46
|
@click.option("--show", "show_file", default=None, type=click.Path(exists=True),
|
|
51
47
|
help="Load and display a saved device-map file")
|
|
52
48
|
@click.option("--diff", "diff_files", nargs=2, type=click.Path(exists=True),
|
|
53
49
|
help="Diff two saved device-map files")
|
|
50
|
+
@click.option("--apply-config", "apply_config", default=None,
|
|
51
|
+
type=click.Path(exists=True, dir_okay=False),
|
|
52
|
+
help="Apply hardware/infra settings from YAML config file to the remote host")
|
|
54
53
|
@click.option("--ssh-key", default=None, type=click.Path(), help="SSH private key path")
|
|
55
54
|
def device_map_cmd(
|
|
56
55
|
host, name, tags, save, out_path, output_fmt,
|
|
57
|
-
no_infra, list_saved, show_file, diff_files, ssh_key,
|
|
56
|
+
no_infra, list_saved, show_file, diff_files, ssh_key, apply_config,
|
|
58
57
|
):
|
|
59
58
|
"""Generate a full standardized device snapshot (hardware + infra + diagnostics).
|
|
60
59
|
|
|
@@ -65,9 +64,14 @@ def device_map_cmd(
|
|
|
65
64
|
Examples:
|
|
66
65
|
redeploy device-map pi@192.168.188.109
|
|
67
66
|
redeploy device-map pi@192.168.188.109 --save --name "kiosk-lab"
|
|
68
|
-
redeploy device-map pi@192.168.188.109 --format yaml > rpi5.yaml
|
|
69
67
|
redeploy device-map --list
|
|
70
68
|
redeploy device-map --show ~/.config/redeploy/device-maps/pi_at_192.168.188.109.yaml
|
|
69
|
+
|
|
70
|
+
\b
|
|
71
|
+
Config-file workflow (scan → edit → apply):
|
|
72
|
+
redeploy device-map pi@192.168.188.109 > device-map.yaml
|
|
73
|
+
# edit device-map.yaml: set hardware.drm_outputs[0].transform: '270'
|
|
74
|
+
redeploy device-map pi@192.168.188.109 --apply-config device-map.yaml
|
|
71
75
|
"""
|
|
72
76
|
from ...models import DeviceMap
|
|
73
77
|
|
|
@@ -81,7 +85,7 @@ def device_map_cmd(
|
|
|
81
85
|
# ── --show ────────────────────────────────────────────────────────────────
|
|
82
86
|
if show_file:
|
|
83
87
|
dm = DeviceMap.load(Path(show_file))
|
|
84
|
-
|
|
88
|
+
click.echo(dm.to_yaml())
|
|
85
89
|
return
|
|
86
90
|
|
|
87
91
|
# ── --diff ────────────────────────────────────────────────────────────────
|
|
@@ -153,14 +157,11 @@ def device_map_cmd(
|
|
|
153
157
|
)
|
|
154
158
|
|
|
155
159
|
# ── output ────────────────────────────────────────────────────────────────
|
|
156
|
-
if output_fmt
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
else:
|
|
160
|
-
import json as _json
|
|
161
|
-
click.echo(_json.dumps(dm.model_dump(mode="json"), indent=2))
|
|
160
|
+
if output_fmt == "json":
|
|
161
|
+
import json as _json
|
|
162
|
+
click.echo(_json.dumps(dm.model_dump(mode="json"), indent=2))
|
|
162
163
|
else:
|
|
163
|
-
|
|
164
|
+
click.echo(dm.to_yaml())
|
|
164
165
|
|
|
165
166
|
# ── save ──────────────────────────────────────────────────────────────────
|
|
166
167
|
if save or out_path:
|
|
@@ -172,6 +173,143 @@ def device_map_cmd(
|
|
|
172
173
|
sys.exit(1)
|
|
173
174
|
|
|
174
175
|
|
|
176
|
+
def _apply_config_from_file(console, config_path, ssh_key):
|
|
177
|
+
"""Apply hardware/infra settings from YAML/JSON config file to the remote host.
|
|
178
|
+
|
|
179
|
+
Reads DeviceMap YAML/JSON and applies:
|
|
180
|
+
- Display transforms via wlr-randr + kanshi
|
|
181
|
+
- Backlight settings
|
|
182
|
+
- Other configurable hardware parameters
|
|
183
|
+
"""
|
|
184
|
+
import yaml
|
|
185
|
+
import json as _json
|
|
186
|
+
|
|
187
|
+
from ...remote import Remote
|
|
188
|
+
|
|
189
|
+
with open(config_path) as f:
|
|
190
|
+
raw = f.read()
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
cfg = yaml.safe_load(raw)
|
|
194
|
+
except Exception:
|
|
195
|
+
cfg = _json.loads(raw)
|
|
196
|
+
|
|
197
|
+
# Extract host from config or prompt
|
|
198
|
+
host = cfg.get("host")
|
|
199
|
+
if not host:
|
|
200
|
+
console.print("[red]✗ No 'host' field in config file[/red]")
|
|
201
|
+
sys.exit(1)
|
|
202
|
+
|
|
203
|
+
console.print(f"[cyan]→ Applying config from {config_path} to {host}[/cyan]")
|
|
204
|
+
|
|
205
|
+
# Connect to remote
|
|
206
|
+
p = Remote(host, ssh_key=ssh_key)
|
|
207
|
+
|
|
208
|
+
applied = 0
|
|
209
|
+
|
|
210
|
+
# ── display transforms ────────────────────────────────────────────────────
|
|
211
|
+
hardware = cfg.get("hardware", {})
|
|
212
|
+
for output in hardware.get("drm_outputs", []):
|
|
213
|
+
connector = output.get("connector", "")
|
|
214
|
+
transform = output.get("transform", "normal")
|
|
215
|
+
if not connector or not ("DSI" in connector or "HDMI" in connector):
|
|
216
|
+
continue
|
|
217
|
+
|
|
218
|
+
if transform == "normal":
|
|
219
|
+
console.print(f"[dim] skip {connector}: transform=normal (default)[/dim]")
|
|
220
|
+
continue
|
|
221
|
+
|
|
222
|
+
console.print(f"[cyan]→ {connector}: transform={transform}[/cyan]")
|
|
223
|
+
|
|
224
|
+
# Apply via wlr-randr
|
|
225
|
+
wlr_cmd = (
|
|
226
|
+
f"WAYLAND_DISPLAY=wayland-0 XDG_RUNTIME_DIR=/run/user/$(id -u) "
|
|
227
|
+
f"wlr-randr --output {connector} --transform {transform} 2>&1"
|
|
228
|
+
)
|
|
229
|
+
r = p.run(wlr_cmd)
|
|
230
|
+
if r.ok:
|
|
231
|
+
console.print(f"[green] ✓ wlr-randr applied[/green]")
|
|
232
|
+
applied += 1
|
|
233
|
+
else:
|
|
234
|
+
console.print(f"[yellow] ⚠ wlr-randr: {r.out.strip() or 'no output'}[/yellow]")
|
|
235
|
+
|
|
236
|
+
# ── persist DSI transforms in kanshi ─────────────────────────────────────────
|
|
237
|
+
dsi_outputs = [
|
|
238
|
+
o for o in hardware.get("drm_outputs", [])
|
|
239
|
+
if "DSI" in o.get("connector", "")
|
|
240
|
+
]
|
|
241
|
+
if dsi_outputs:
|
|
242
|
+
_update_kanshi_from_cfg(console, p, dsi_outputs)
|
|
243
|
+
|
|
244
|
+
# ── backlight ─────────────────────────────────────────────────────────────
|
|
245
|
+
for bl in hardware.get("backlights", []):
|
|
246
|
+
name = bl.get("name", "")
|
|
247
|
+
brightness = bl.get("brightness")
|
|
248
|
+
bl_power = bl.get("bl_power")
|
|
249
|
+
if not name:
|
|
250
|
+
continue
|
|
251
|
+
|
|
252
|
+
if brightness is not None:
|
|
253
|
+
r = p.run(f"echo {brightness} | sudo tee /sys/class/backlight/{name}/brightness > /dev/null")
|
|
254
|
+
if r.ok:
|
|
255
|
+
console.print(f"[green] ✓ backlight {name}: brightness={brightness}[/green]")
|
|
256
|
+
applied += 1
|
|
257
|
+
|
|
258
|
+
if bl_power is not None:
|
|
259
|
+
r = p.run(f"echo {bl_power} | sudo tee /sys/class/backlight/{name}/bl_power > /dev/null")
|
|
260
|
+
if r.ok:
|
|
261
|
+
console.print(f"[green] ✓ backlight {name}: bl_power={bl_power}[/green]")
|
|
262
|
+
applied += 1
|
|
263
|
+
|
|
264
|
+
if applied == 0:
|
|
265
|
+
console.print("[yellow]Nothing to apply — no relevant settings found in config[/yellow]")
|
|
266
|
+
else:
|
|
267
|
+
console.print(f"\n[bold green]✓ Config applied from {config_path}[/bold green]")
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _update_kanshi_from_cfg(console, p, dsi_outputs: list):
|
|
271
|
+
"""Rebuild kanshi profile from drm_outputs list and write to ~/.config/kanshi/config."""
|
|
272
|
+
import re as _re
|
|
273
|
+
|
|
274
|
+
kanshi_cfg_path = "~/.config/kanshi/config"
|
|
275
|
+
read_r = p.run(f"cat {kanshi_cfg_path} 2>/dev/null")
|
|
276
|
+
current = read_r.out if read_r.ok else ""
|
|
277
|
+
|
|
278
|
+
for output in dsi_outputs:
|
|
279
|
+
connector = output.get("connector", "")
|
|
280
|
+
transform = output.get("transform", "normal")
|
|
281
|
+
|
|
282
|
+
output_line_pat = _re.compile(
|
|
283
|
+
rf'(\s*output\s+{_re.escape(connector)}\s+enable)(\s+transform\s+\S+)?'
|
|
284
|
+
)
|
|
285
|
+
if _re.search(output_line_pat, current):
|
|
286
|
+
if transform == "normal":
|
|
287
|
+
current = _re.sub(output_line_pat, r'\1', current)
|
|
288
|
+
else:
|
|
289
|
+
current = _re.sub(output_line_pat, rf'\1 transform {transform}', current)
|
|
290
|
+
elif current.strip():
|
|
291
|
+
current = _re.sub(
|
|
292
|
+
rf'(\s*output\s+{_re.escape(connector)}\b)',
|
|
293
|
+
rf'\1 transform {transform}' if transform != "normal" else r'\1',
|
|
294
|
+
current,
|
|
295
|
+
)
|
|
296
|
+
else:
|
|
297
|
+
current = (
|
|
298
|
+
f"profile waveshare-only {{\n"
|
|
299
|
+
f" output {connector} enable"
|
|
300
|
+
+ (f" transform {transform}" if transform != "normal" else "")
|
|
301
|
+
+ "\n"
|
|
302
|
+
+ " output HDMI-A-2 disable\n"
|
|
303
|
+
+ "}\n"
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
escaped = current.replace("'", "'\\''")
|
|
307
|
+
write_r = p.run(f"mkdir -p ~/.config/kanshi && printf '%s' '{escaped}' > {kanshi_cfg_path}")
|
|
308
|
+
if write_r.ok:
|
|
309
|
+
console.print(f"[green] ✓ kanshi config updated ({kanshi_cfg_path})[/green]")
|
|
310
|
+
p.run("pkill -SIGUSR1 kanshi 2>/dev/null || true")
|
|
311
|
+
|
|
312
|
+
|
|
175
313
|
# ── rendering helpers ─────────────────────────────────────────────────────────
|
|
176
314
|
|
|
177
315
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""diagnose command — Compare a migration spec against the live target host."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.command()
|
|
11
|
+
@click.argument("spec", type=click.Path(exists=True, dir_okay=False))
|
|
12
|
+
@click.option("--host", default=None, help="Override target host (default: spec.target.host)")
|
|
13
|
+
@click.option("--ssh-key", default=None, type=click.Path(), help="Path to SSH private key")
|
|
14
|
+
@click.option("--format", "output_fmt", default="yaml", type=click.Choice(["yaml", "json"]), help="Output format (default: yaml)")
|
|
15
|
+
@click.option("--exit-on-fail", is_flag=True, help="Exit with non-zero status if any check fails")
|
|
16
|
+
@click.pass_context
|
|
17
|
+
def diagnose(ctx, spec, host, ssh_key, output_fmt, exit_on_fail):
|
|
18
|
+
"""Compare a migration spec against the live target host.
|
|
19
|
+
|
|
20
|
+
Walks the spec (YAML or markpact .md), derives all expected facts
|
|
21
|
+
(binaries, directories, ports, container images, systemd units,
|
|
22
|
+
apt packages, env files, free disk) and probes the target read-only
|
|
23
|
+
via SSH. Reports what is missing or out of spec — without applying
|
|
24
|
+
any change.
|
|
25
|
+
|
|
26
|
+
\b
|
|
27
|
+
Examples:
|
|
28
|
+
redeploy diagnose migration.podman-rpi5-resume.md
|
|
29
|
+
redeploy diagnose migration.yaml --host pi@192.168.188.108
|
|
30
|
+
redeploy diagnose migration.yaml --format json
|
|
31
|
+
"""
|
|
32
|
+
from ...audit import audit_spec
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
report = audit_spec(spec, host=host, ssh_key=ssh_key)
|
|
36
|
+
except Exception as exc:
|
|
37
|
+
click.echo(f"audit failed: {exc}", err=True)
|
|
38
|
+
sys.exit(2)
|
|
39
|
+
|
|
40
|
+
if output_fmt == "json":
|
|
41
|
+
import json as _json
|
|
42
|
+
click.echo(_json.dumps(report.to_dict(), indent=2))
|
|
43
|
+
else:
|
|
44
|
+
click.echo(yaml.safe_dump(report.to_dict(), sort_keys=False))
|
|
45
|
+
|
|
46
|
+
if exit_on_fail and not report.ok:
|
|
47
|
+
sys.exit(1)
|
|
@@ -19,11 +19,7 @@ from rich.console import Console
|
|
|
19
19
|
"--to", "to_src", default=None,
|
|
20
20
|
help="Right side: IaC file/directory or SSH host"
|
|
21
21
|
)
|
|
22
|
-
|
|
23
|
-
"--format", "out_format", default="json",
|
|
24
|
-
type=click.Choice(["text", "json"]), show_default=True
|
|
25
|
-
)
|
|
26
|
-
def diff(ci_file, host, from_src, to_src, out_format):
|
|
22
|
+
def diff(ci_file, host, from_src, to_src):
|
|
27
23
|
"""Compare IaC file vs live host (drift detection). [Phase 3 — coming soon]
|
|
28
24
|
|
|
29
25
|
\b
|
|
@@ -9,10 +9,6 @@ from rich.console import Console
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
@click.command("export")
|
|
12
|
-
@click.option(
|
|
13
|
-
"--format", "fmt", default="yaml", type=click.Choice(["css", "yaml"]),
|
|
14
|
-
help="Output format (css or yaml)"
|
|
15
|
-
)
|
|
16
12
|
@click.option(
|
|
17
13
|
"-o", "--output", default=None, type=click.Path(),
|
|
18
14
|
help="Output file (default: print to stdout)"
|
|
@@ -21,19 +17,23 @@ from rich.console import Console
|
|
|
21
17
|
"--file", "src_file", default=None, type=click.Path(),
|
|
22
18
|
help="Source file to convert (auto-detected if omitted)"
|
|
23
19
|
)
|
|
20
|
+
@click.option(
|
|
21
|
+
"--format", "fmt", default="yaml", type=click.Choice(["yaml", "css"]),
|
|
22
|
+
help="Output format (default: yaml)"
|
|
23
|
+
)
|
|
24
24
|
@click.pass_context
|
|
25
|
-
def export_cmd(ctx,
|
|
25
|
+
def export_cmd(ctx, output, src_file, fmt):
|
|
26
26
|
"""Convert between redeploy.css and redeploy.yaml formats.
|
|
27
27
|
|
|
28
|
-
Reads the nearest redeploy.css or redeploy.yaml and exports to
|
|
29
|
-
|
|
28
|
+
Reads the nearest redeploy.css or redeploy.yaml and exports to YAML.
|
|
29
|
+
Useful for migration, sharing, and LLM inspection.
|
|
30
30
|
|
|
31
31
|
\b
|
|
32
32
|
Examples:
|
|
33
|
+
redeploy export # auto-detect source → yaml (stdout)
|
|
34
|
+
redeploy export -o redeploy.yaml # auto-detect source → yaml (file)
|
|
35
|
+
redeploy export --file redeploy.css # css → yaml
|
|
33
36
|
redeploy export --format css # yaml → css (stdout)
|
|
34
|
-
redeploy export --format css -o redeploy.css
|
|
35
|
-
redeploy export --format yaml -o redeploy.yaml
|
|
36
|
-
redeploy export --format yaml --file redeploy.css
|
|
37
37
|
"""
|
|
38
38
|
console = Console(stderr=True)
|
|
39
39
|
|