redeploy 0.2.20__tar.gz → 0.2.22__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. {redeploy-0.2.20 → redeploy-0.2.22}/PKG-INFO +6 -6
  2. {redeploy-0.2.20 → redeploy-0.2.22}/README.md +5 -5
  3. {redeploy-0.2.20 → redeploy-0.2.22}/pyproject.toml +1 -1
  4. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/__init__.py +1 -1
  5. redeploy-0.2.22/redeploy/cli/__init__.py +107 -0
  6. redeploy-0.2.22/redeploy/cli/commands/__init__.py +44 -0
  7. redeploy-0.2.22/redeploy/cli/commands/audit.py +126 -0
  8. redeploy-0.2.22/redeploy/cli/commands/detect.py +118 -0
  9. redeploy-0.2.22/redeploy/cli/commands/devices.py +216 -0
  10. redeploy-0.2.22/redeploy/cli/commands/diagnose.py +88 -0
  11. redeploy-0.2.22/redeploy/cli/commands/diff.py +37 -0
  12. redeploy-0.2.22/redeploy/cli/commands/exec_.py +251 -0
  13. redeploy-0.2.22/redeploy/cli/commands/export.py +126 -0
  14. redeploy-0.2.22/redeploy/cli/commands/import_.py +250 -0
  15. redeploy-0.2.22/redeploy/cli/commands/init.py +111 -0
  16. redeploy-0.2.22/redeploy/cli/commands/inspect.py +73 -0
  17. redeploy-0.2.22/redeploy/cli/commands/patterns.py +104 -0
  18. redeploy-0.2.22/redeploy/cli/commands/plan_apply.py +436 -0
  19. redeploy-0.2.22/redeploy/cli/commands/plugin.py +79 -0
  20. redeploy-0.2.22/redeploy/cli/commands/probe.py +144 -0
  21. redeploy-0.2.22/redeploy/cli/commands/state.py +118 -0
  22. redeploy-0.2.22/redeploy/cli/commands/status.py +55 -0
  23. redeploy-0.2.22/redeploy/cli/commands/target.py +97 -0
  24. redeploy-0.2.22/redeploy/cli/commands/version.py +1405 -0
  25. redeploy-0.2.22/redeploy/cli/commands/workflow.py +87 -0
  26. redeploy-0.2.22/redeploy/cli/core.py +179 -0
  27. redeploy-0.2.22/redeploy/cli/display.py +349 -0
  28. redeploy-0.2.22/redeploy/cli.py +93 -0
  29. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/markpact/parser.py +58 -33
  30. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/ssh.py +2 -0
  31. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/tests/test_cli.py +3 -3
  32. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/version/changelog.py +37 -28
  33. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy.egg-info/PKG-INFO +6 -6
  34. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy.egg-info/SOURCES.txt +22 -0
  35. redeploy-0.2.20/redeploy/cli/__init__.py +0 -66
  36. redeploy-0.2.20/redeploy/cli.py +0 -3945
  37. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/apply/__init__.py +0 -0
  38. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/apply/executor.py +0 -0
  39. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/apply/state.py +0 -0
  40. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/audit.py +0 -0
  41. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/data_sync.py +0 -0
  42. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/detect/__init__.py +0 -0
  43. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/detect/detector.py +0 -0
  44. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/detect/probes.py +0 -0
  45. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/detect/remote.py +0 -0
  46. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/detect/templates.py +0 -0
  47. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/detect/workflow.py +0 -0
  48. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/discovery.py +0 -0
  49. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/dsl/__init__.py +0 -0
  50. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/dsl/loader.py +0 -0
  51. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/dsl/parser.py +0 -0
  52. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/dsl_python/__init__.py +0 -0
  53. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/dsl_python/context.py +0 -0
  54. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/dsl_python/decorators.py +0 -0
  55. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/dsl_python/docker_steps.py +0 -0
  56. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/dsl_python/exceptions.py +0 -0
  57. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/dsl_python/runner.py +0 -0
  58. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/dsl_python/steps.py +0 -0
  59. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/fleet.py +0 -0
  60. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/iac/__init__.py +0 -0
  61. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/iac/base.py +0 -0
  62. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/iac/docker_compose.py +0 -0
  63. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/iac/parsers/__init__.py +0 -0
  64. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/iac/parsers/compose.py +0 -0
  65. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/iac/registry.py +0 -0
  66. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/markpact/__init__.py +0 -0
  67. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/markpact/compiler.py +0 -0
  68. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/markpact/models.py +0 -0
  69. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/models.py +0 -0
  70. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/observe.py +0 -0
  71. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/parse.py +0 -0
  72. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/patterns.py +0 -0
  73. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/plan/__init__.py +0 -0
  74. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/plan/planner.py +0 -0
  75. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/plugins/__init__.py +0 -0
  76. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/plugins/builtin/__init__.py +0 -0
  77. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/plugins/builtin/browser_reload.py +0 -0
  78. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/plugins/builtin/notify.py +0 -0
  79. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/plugins/builtin/systemd_reload.py +0 -0
  80. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/spec_loader.py +0 -0
  81. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/steps.py +0 -0
  82. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/tests/__init__.py +0 -0
  83. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/tests/test_audit.py +0 -0
  84. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/tests/test_data_sync.py +0 -0
  85. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/tests/test_discovery.py +0 -0
  86. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/tests/test_dsl.py +0 -0
  87. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/tests/test_examples.py +0 -0
  88. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/tests/test_executor.py +0 -0
  89. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/tests/test_fleet.py +0 -0
  90. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/tests/test_git_integration.py +0 -0
  91. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/tests/test_iac.py +0 -0
  92. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/tests/test_manifest.py +0 -0
  93. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/tests/test_markpact_compiler.py +0 -0
  94. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/tests/test_markpact_parser.py +0 -0
  95. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/tests/test_models.py +0 -0
  96. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/tests/test_observe.py +0 -0
  97. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/tests/test_parse.py +0 -0
  98. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/tests/test_patterns.py +0 -0
  99. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/tests/test_planner_edge.py +0 -0
  100. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/tests/test_plugins.py +0 -0
  101. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/tests/test_probes.py +0 -0
  102. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/tests/test_public_api.py +0 -0
  103. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/tests/test_resume.py +0 -0
  104. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/tests/test_spec_loader.py +0 -0
  105. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/tests/test_ssh.py +0 -0
  106. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/tests/test_templates.py +0 -0
  107. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/tests/test_verify.py +0 -0
  108. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/tests/test_version.py +0 -0
  109. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/tests/test_version_changelog.py +0 -0
  110. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/tests/test_version_cli.py +0 -0
  111. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/tests/test_version_manifest.py +0 -0
  112. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/tests/test_version_sources.py +0 -0
  113. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/tests/test_workflow.py +0 -0
  114. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/verify.py +0 -0
  115. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/version/__init__.py +0 -0
  116. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/version/bump.py +0 -0
  117. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/version/commits.py +0 -0
  118. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/version/diff.py +0 -0
  119. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/version/git_integration.py +0 -0
  120. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/version/git_transaction.py +0 -0
  121. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/version/manifest.py +0 -0
  122. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/version/sources/__init__.py +0 -0
  123. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/version/sources/base.py +0 -0
  124. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/version/sources/json_.py +0 -0
  125. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/version/sources/plain.py +0 -0
  126. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/version/sources/regex.py +0 -0
  127. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/version/sources/toml_.py +0 -0
  128. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/version/sources/yaml_.py +0 -0
  129. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/version/transaction.py +0 -0
  130. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy/version.py +0 -0
  131. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy.egg-info/dependency_links.txt +0 -0
  132. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy.egg-info/entry_points.txt +0 -0
  133. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy.egg-info/requires.txt +0 -0
  134. {redeploy-0.2.20 → redeploy-0.2.22}/redeploy.egg-info/top_level.txt +0 -0
  135. {redeploy-0.2.20 → redeploy-0.2.22}/setup.cfg +0 -0
  136. {redeploy-0.2.20 → redeploy-0.2.22}/tests/test_iac.py +0 -0
  137. {redeploy-0.2.20 → redeploy-0.2.22}/tests/test_parse.py +0 -0
  138. {redeploy-0.2.20 → redeploy-0.2.22}/tests/test_redeploy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redeploy
3
- Version: 0.2.20
3
+ Version: 0.2.22
4
4
  Summary: Infrastructure migration toolkit: detect → plan → apply
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -27,17 +27,17 @@ Requires-Dist: pfix>=0.1.60; extra == "dev"
27
27
 
28
28
  ## AI Cost Tracking
29
29
 
30
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.20-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
31
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$5.85-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-13.4h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
30
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.22-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
31
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$6.15-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-13.6h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
32
32
 
33
- - 🤖 **LLM usage:** $5.8500 (39 commits)
34
- - 👤 **Human dev:** ~$1337 (13.4h @ $100/h, 30min dedup)
33
+ - 🤖 **LLM usage:** $6.1500 (41 commits)
34
+ - 👤 **Human dev:** ~$1363 (13.6h @ $100/h, 30min dedup)
35
35
 
36
36
  Generated on 2026-04-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
37
37
 
38
38
  ---
39
39
 
40
- ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.20-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
40
+ ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.22-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
41
41
 
42
42
  Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
43
43
 
@@ -3,17 +3,17 @@
3
3
 
4
4
  ## AI Cost Tracking
5
5
 
6
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.20-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$5.85-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-13.4h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
6
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.22-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$6.15-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-13.6h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $5.8500 (39 commits)
10
- - 👤 **Human dev:** ~$1337 (13.4h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $6.1500 (41 commits)
10
+ - 👤 **Human dev:** ~$1363 (13.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
- ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.20-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
16
+ ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.22-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
17
17
 
18
18
  Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
19
19
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "redeploy"
7
- version = "0.2.20"
7
+ version = "0.2.22"
8
8
  description = "Infrastructure migration toolkit: detect → plan → apply"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -7,7 +7,7 @@ Public API (stable from 0.2.0, semver guaranteed)::
7
7
 
8
8
  Everything not listed in ``__all__`` is internal and may change without notice.
9
9
  """
10
- __version__ = "0.2.20"
10
+ __version__ = "0.2.22"
11
11
 
12
12
  # ── Core models ───────────────────────────────────────────────────────────────
13
13
  from .models import ( # noqa: F401
@@ -0,0 +1,107 @@
1
+ """redeploy CLI — detect | plan | apply | migrate."""
2
+ from __future__ import annotations
3
+
4
+ import sys
5
+
6
+ import click
7
+ from loguru import logger
8
+
9
+ from .. import __version__
10
+
11
+
12
+ def _setup_logging(verbose: bool) -> None:
13
+ logger.remove()
14
+ level = "DEBUG" if verbose else "INFO"
15
+ logger.add(
16
+ sys.stderr,
17
+ level=level,
18
+ format="<green>{time:HH:mm:ss}</green> | <level>{level:<7}</level> | {message}",
19
+ )
20
+
21
+
22
+ @click.group()
23
+ @click.version_option(__version__)
24
+ @click.option("-v", "--verbose", is_flag=True)
25
+ @click.pass_context
26
+ def cli(ctx, verbose):
27
+ """redeploy — Infrastructure migration toolkit: detect → plan → apply"""
28
+ _setup_logging(verbose)
29
+ ctx.ensure_object(dict)
30
+ ctx.obj["verbose"] = verbose
31
+
32
+
33
+ # Import all commands from submodules (not from package level)
34
+ from .commands.audit import audit
35
+ from .commands.detect import detect
36
+ from .commands.devices import devices, scan, device_add, device_rm
37
+ from .commands.diagnose import diagnose
38
+ from .commands.diff import diff
39
+ from .commands.export import export_cmd
40
+ from .commands.import_ import import_cmd
41
+ from .commands.inspect import inspect
42
+ from .commands.patterns import patterns
43
+ from .commands.plugin import plugin_cmd
44
+ from .commands.probe import probe
45
+ from .commands.target import target
46
+ from .commands.version import version_cmd
47
+ from .commands.workflow import workflow_cmd
48
+ from .commands.exec_ import exec_cmd, exec_multi_cmd
49
+ from .commands.plan_apply import plan, apply, migrate, run
50
+ from .commands.state import state_cmd
51
+ from .commands.init import init
52
+ from .commands.status import status
53
+
54
+ # Register commands
55
+ cli.add_command(audit)
56
+ cli.add_command(detect)
57
+ cli.add_command(devices)
58
+ cli.add_command(scan)
59
+ cli.add_command(device_add)
60
+ cli.add_command(device_rm)
61
+ cli.add_command(diagnose)
62
+ cli.add_command(diff)
63
+ cli.add_command(exec_cmd)
64
+ cli.add_command(exec_multi_cmd)
65
+ cli.add_command(export_cmd)
66
+ cli.add_command(import_cmd)
67
+ cli.add_command(init)
68
+ cli.add_command(inspect)
69
+ cli.add_command(patterns)
70
+ cli.add_command(plan)
71
+ cli.add_command(apply)
72
+ cli.add_command(migrate)
73
+ cli.add_command(run)
74
+ cli.add_command(plugin_cmd)
75
+ cli.add_command(probe)
76
+ cli.add_command(state_cmd)
77
+ cli.add_command(status)
78
+ cli.add_command(target)
79
+ cli.add_command(version_cmd)
80
+ cli.add_command(workflow_cmd)
81
+
82
+
83
+ # Backward compatibility: _resolve_device for tests
84
+ def _resolve_device(console, device_id: str) -> tuple:
85
+ """Resolve device from registry or auto-probe. Returns (device, registry) or (None, None)."""
86
+ from ..discovery import auto_probe
87
+ from ..models import DeviceRegistry
88
+
89
+ reg = DeviceRegistry.load()
90
+ dev = reg.get(device_id)
91
+
92
+ if not dev:
93
+ console.print(f"[yellow]⚠ {device_id} not in registry — probing…[/yellow]")
94
+ r = auto_probe(device_id, timeout=8, save=True)
95
+ if r.reachable:
96
+ reg = DeviceRegistry.load()
97
+ dev = reg.get(r.host) or reg.get(r.ip)
98
+ key_name = __import__('os').path.basename(r.ssh_key) if r.ssh_key else 'agent'
99
+ console.print(f" [green]✓[/green] auto-probe OK: {r.host} strategy={r.strategy} key={key_name}")
100
+ else:
101
+ console.print(f" [red]✗ probe failed: {r.error}[/red]")
102
+ console.print("[dim] Add manually: redeploy device-add HOST --strategy STRATEGY[/dim]")
103
+
104
+ return dev, reg
105
+
106
+
107
+ __all__ = ["cli", "_resolve_device"]
@@ -0,0 +1,44 @@
1
+ """CLI commands package."""
2
+ # Commands are registered in cli/__init__.py
3
+
4
+ from . import audit
5
+ from . import detect
6
+ from . import devices
7
+ from . import diagnose
8
+ from . import diff
9
+ from . import exec_
10
+ from . import export
11
+ from . import import_
12
+ from . import init
13
+ from . import inspect
14
+ from . import patterns
15
+ from . import plan_apply
16
+ from . import plugin
17
+ from . import probe
18
+ from . import state
19
+ from . import status
20
+ from . import target
21
+ from . import version
22
+ from . import workflow
23
+
24
+ __all__ = [
25
+ "audit",
26
+ "detect",
27
+ "devices",
28
+ "diagnose",
29
+ "diff",
30
+ "exec_",
31
+ "export",
32
+ "import_",
33
+ "init",
34
+ "inspect",
35
+ "patterns",
36
+ "plan_apply",
37
+ "plugin",
38
+ "probe",
39
+ "state",
40
+ "status",
41
+ "target",
42
+ "version",
43
+ "workflow",
44
+ ]
@@ -0,0 +1,126 @@
1
+ """audit command — Show deploy audit log."""
2
+ from __future__ import annotations
3
+
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import click
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+
12
+ @click.command()
13
+ @click.option(
14
+ "-n", "--last", default=20, show_default=True,
15
+ help="Number of most-recent entries to show"
16
+ )
17
+ @click.option("--host", default=None, help="Filter by host (substring match)")
18
+ @click.option("--app", default=None, help="Filter by app name")
19
+ @click.option("--failed", "only_failed", is_flag=True, help="Show only failed deployments")
20
+ @click.option("--ok", "only_ok", is_flag=True, help="Show only successful deployments")
21
+ @click.option("--log", default=None, type=click.Path(), help="Custom audit log path")
22
+ @click.option("--json", "as_json", is_flag=True, help="Output raw JSONL")
23
+ @click.option(
24
+ "--report", "show_report", default=None,
25
+ help="Show full DeployReport for entry N (1-based)"
26
+ )
27
+ @click.option("--clear", "do_clear", is_flag=True, help="Truncate audit log (irreversible)")
28
+ def audit(last, host, app, only_failed, only_ok, log, as_json, show_report, do_clear):
29
+ """Show deploy audit log from ~/.config/redeploy/audit.jsonl.
30
+
31
+ \b
32
+ Examples:
33
+ redeploy audit
34
+ redeploy audit --last 50 --failed
35
+ redeploy audit --app myapp --host prod
36
+ redeploy audit --report 1
37
+ redeploy audit --clear
38
+ """
39
+ import json as _json
40
+ from ...observe import DeployAuditLog, DeployReport
41
+
42
+ console = Console()
43
+ log_path = Path(log) if log else None
44
+ audit_log = DeployAuditLog(path=log_path)
45
+
46
+ if do_clear:
47
+ if not audit_log.path.exists():
48
+ console.print("[dim]Audit log is already empty.[/dim]")
49
+ return
50
+ click.confirm(f"Truncate {audit_log.path}?", abort=True)
51
+ audit_log.clear()
52
+ console.print(f"[green]✓[/green] Audit log cleared: {audit_log.path}")
53
+ return
54
+
55
+ ok_filter = None
56
+ if only_failed:
57
+ ok_filter = False
58
+ elif only_ok:
59
+ ok_filter = True
60
+
61
+ entries = audit_log.filter(host=host, app=app, ok=ok_filter)
62
+ entries = entries[-last:]
63
+
64
+ if not entries:
65
+ console.print("[dim]No audit entries found.[/dim]")
66
+ console.print(f" Log: {audit_log.path}")
67
+ return
68
+
69
+ if show_report:
70
+ try:
71
+ idx = int(show_report) - 1
72
+ entry = entries[idx]
73
+ except (ValueError, IndexError):
74
+ console.print(
75
+ f"[red]Entry {show_report} not found (1–{len(entries)} available)[/red]"
76
+ )
77
+ sys.exit(1)
78
+ console.print(DeployReport(entry).text())
79
+ return
80
+
81
+ if as_json:
82
+ for e in entries:
83
+ print(_json.dumps(e.to_dict(), ensure_ascii=False))
84
+ return
85
+
86
+ t = Table(show_header=True, box=None, padding=(0, 2))
87
+ t.add_column("#", style="dim", width=3)
88
+ t.add_column("Time", style="dim")
89
+ t.add_column("Host")
90
+ t.add_column("App", style="bold")
91
+ t.add_column("Strategy", style="cyan")
92
+ t.add_column("Result")
93
+ t.add_column("Steps", style="dim")
94
+ t.add_column("Elapsed", style="dim")
95
+
96
+ for i, e in enumerate(entries, 1):
97
+ ts = e.ts[11:16] if len(e.ts) >= 16 else e.ts
98
+ date = e.ts[:10] if len(e.ts) >= 10 else ""
99
+ strategy = f"{e.from_strategy}→{e.to_strategy}"
100
+ if e.ok:
101
+ result = "[green]ok[/green]"
102
+ else:
103
+ result = "[red]FAIL[/red]"
104
+ if e.dry_run:
105
+ result += " [dim](dry)[/dim]"
106
+ steps_str = f"{e.steps_ok}/{e.steps_total}"
107
+ if e.steps_failed:
108
+ steps_str += f" [red]✗{e.steps_failed}[/red]"
109
+ elapsed = f"{e.elapsed_s:.1f}s"
110
+ t.add_row(
111
+ str(i),
112
+ f"{date} {ts}",
113
+ e.host,
114
+ e.app,
115
+ strategy,
116
+ result,
117
+ steps_str,
118
+ elapsed,
119
+ )
120
+
121
+ console.print(t)
122
+ console.print(
123
+ f"\n [dim]{len(entries)} entr{'y' if len(entries) == 1 else 'ies'}"
124
+ f" • {audit_log.path}[/dim]"
125
+ )
126
+ console.print(" [dim]Tip: --report N for full step breakdown[/dim]")
@@ -0,0 +1,118 @@
1
+ """detect command — Probe infrastructure and produce infra.yaml."""
2
+ from __future__ import annotations
3
+
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import click
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ from ..core import run_detect_workflow
12
+ from ..display import (
13
+ print_infrastructure_summary,
14
+ print_docker_services,
15
+ print_k3s_pods,
16
+ print_conflicts,
17
+ )
18
+
19
+
20
+ @click.command()
21
+ @click.option("--host", default=None, help="SSH host (user@ip) or 'local'")
22
+ @click.option(
23
+ "--app",
24
+ default=None,
25
+ show_default=True,
26
+ help="Application name (default from redeploy.yaml)",
27
+ )
28
+ @click.option("--domain", default=None, help="Public domain for HTTP health checks")
29
+ @click.option(
30
+ "-o",
31
+ "--output",
32
+ default="infra.yaml",
33
+ show_default=True,
34
+ type=click.Path(),
35
+ help="Output file for InfraState",
36
+ )
37
+ @click.option(
38
+ "--workflow",
39
+ "run_workflow",
40
+ is_flag=True,
41
+ help="Run full multi-host workflow (detect + template scoring)",
42
+ )
43
+ @click.option(
44
+ "--scan", "scan_subnet", default=None, help="Subnet to scan for devices (used with --workflow)"
45
+ )
46
+ @click.option(
47
+ "--no-deep", is_flag=True, help="Workflow: skip deep SSH probe (faster, less accurate)"
48
+ )
49
+ @click.option(
50
+ "--save-yaml",
51
+ default=None,
52
+ type=click.Path(),
53
+ help="Workflow: save generated manifest to file",
54
+ )
55
+ @click.option(
56
+ "--format",
57
+ "output_fmt",
58
+ default="yaml",
59
+ type=click.Choice(["yaml", "css"]),
60
+ help="Output format for generated manifest (yaml or css)",
61
+ )
62
+ @click.pass_context
63
+ def detect(
64
+ ctx, host, app, domain, output, run_workflow, scan_subnet, no_deep, save_yaml, output_fmt
65
+ ):
66
+ """Probe infrastructure and produce infra.yaml.
67
+
68
+ With --workflow: multi-host detection with template scoring.
69
+ Reads hosts from redeploy.yaml / redeploy.css environments + registry + --scan.
70
+
71
+ \b
72
+ Examples:
73
+ redeploy detect --host pi@192.168.188.108
74
+ redeploy detect --workflow
75
+ redeploy detect --workflow --format css --save-yaml redeploy.css
76
+ redeploy detect --workflow --scan 192.168.188.0/24
77
+ redeploy detect --workflow --no-deep
78
+ """
79
+ from ...models import ProjectManifest
80
+ from ...detect import Detector
81
+
82
+ console = Console()
83
+ manifest = ProjectManifest.find_and_load(Path.cwd())
84
+ app_name = app or (manifest.app if manifest else "app")
85
+
86
+ if run_workflow or scan_subnet:
87
+ run_detect_workflow(
88
+ console,
89
+ hosts=[host] if host else [],
90
+ manifest=manifest,
91
+ app=app_name,
92
+ scan_subnet=scan_subnet,
93
+ deep=not no_deep,
94
+ save_yaml=save_yaml,
95
+ fmt=output_fmt,
96
+ )
97
+ return
98
+
99
+ if not host:
100
+ console.print("[red]✗ --host required (or use --workflow)[/red]")
101
+ sys.exit(1)
102
+
103
+ out_path = Path(output)
104
+ domain = domain or (manifest.domain if manifest else None)
105
+
106
+ try:
107
+ d = Detector(host=host, app=app_name, domain=domain)
108
+ state = d.run()
109
+ d.save(state, out_path)
110
+ except ConnectionError as e:
111
+ console.print(f"[red]✗ {e}[/red]")
112
+ sys.exit(1)
113
+
114
+ print_infrastructure_summary(console, state, host)
115
+ print_docker_services(console, state)
116
+ print_k3s_pods(console, state)
117
+ print_conflicts(console, state)
118
+ console.print(f"\n[dim]Saved to {out_path}[/dim]")
@@ -0,0 +1,216 @@
1
+ """devices, scan, device-add, device-rm commands — Device management."""
2
+ from __future__ import annotations
3
+
4
+ import sys
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+
8
+ import click
9
+ from rich.console import Console
10
+ from rich.table import Table
11
+
12
+
13
+ @click.command()
14
+ @click.option("--tag", default=None, help="Filter by tag")
15
+ @click.option("--strategy", default=None, help="Filter by strategy")
16
+ @click.option("--reachable", is_flag=True, help="Show only recently-seen devices")
17
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
18
+ def devices(tag, strategy, reachable, as_json):
19
+ """List known devices from ~/.config/redeploy/devices.yaml.
20
+
21
+ \b
22
+ Example:
23
+ redeploy devices
24
+ redeploy devices --tag kiosk
25
+ redeploy devices --reachable
26
+ """
27
+ import json as _json
28
+ from ...models import DeviceRegistry
29
+
30
+ console = Console()
31
+ reg = DeviceRegistry.load()
32
+ devs = reg.devices
33
+
34
+ if tag:
35
+ devs = [d for d in devs if tag in d.tags]
36
+ if strategy:
37
+ devs = [d for d in devs if d.strategy == strategy]
38
+ if reachable:
39
+ devs = [d for d in devs if d.is_reachable]
40
+
41
+ if as_json:
42
+ print(
43
+ _json.dumps(
44
+ [d.model_dump(mode="json") for d in devs],
45
+ indent=2,
46
+ default=str,
47
+ )
48
+ )
49
+ return
50
+
51
+ if not devs:
52
+ console.print("[dim]No devices found. Run:[/dim] redeploy scan")
53
+ return
54
+
55
+ t = Table(show_header=True, box=None, padding=(0, 2))
56
+ t.add_column("ID", style="bold")
57
+ t.add_column("Host")
58
+ t.add_column("Strategy", style="cyan")
59
+ t.add_column("App")
60
+ t.add_column("Tags", style="dim")
61
+ t.add_column("Last seen", style="dim")
62
+ t.add_column("SSH", style="dim")
63
+
64
+ for d in devs:
65
+ seen = d.last_seen.strftime("%m-%d %H:%M") if d.last_seen else "never"
66
+ ssh = "[green]✓[/green]" if d.last_ssh_ok else "[red]✗[/red]"
67
+ t.add_row(
68
+ d.id,
69
+ d.host,
70
+ d.strategy,
71
+ d.app or "—",
72
+ ",".join(d.tags) or "—",
73
+ seen,
74
+ ssh,
75
+ )
76
+ console.print(t)
77
+ console.print(
78
+ f"\n [dim]{len(devs)} device(s) • registry: {DeviceRegistry.default_path()}[/dim]"
79
+ )
80
+
81
+
82
+ @click.command()
83
+ @click.option(
84
+ "--subnet", default=None,
85
+ help="CIDR to scan, e.g. 192.168.1.0/24 (auto-detect if omitted)"
86
+ )
87
+ @click.option(
88
+ "--user", "ssh_users", multiple=True, default=None,
89
+ help="SSH user(s) to try (repeatable). Default: current user + root + pi + ubuntu"
90
+ )
91
+ @click.option("--port", "ssh_port", default=22, show_default=True, help="SSH port")
92
+ @click.option("--ping", is_flag=True, help="Active ICMP ping sweep (sends packets)")
93
+ @click.option("--no-mdns", is_flag=True, help="Disable mDNS discovery")
94
+ @click.option(
95
+ "--timeout", default=5, show_default=True, help="Per-host SSH timeout (seconds)"
96
+ )
97
+ @click.option("--no-save", is_flag=True, help="Do not save results to registry")
98
+ def scan(subnet, ssh_users, ssh_port, ping, no_mdns, timeout, no_save):
99
+ """Discover SSH-accessible devices on the local network.
100
+
101
+ Sources (passive by default, zero packets unless --ping):
102
+ known_hosts — parse ~/.ssh/known_hosts
103
+ arp — read ARP/neighbor cache
104
+ mdns — query _ssh._tcp via avahi-browse
105
+ ping sweep — ICMP /24 sweep (--ping flag required)
106
+
107
+ Results are saved to ~/.config/redeploy/devices.yaml (chmod 600).
108
+
109
+ \b
110
+ Example:
111
+ redeploy scan
112
+ redeploy scan --ping --subnet 192.168.1.0/24
113
+ redeploy scan --user pi --user ubuntu --timeout 8
114
+ """
115
+ from ...discovery import discover, update_registry
116
+ from ...models import DeviceRegistry
117
+
118
+ console = Console()
119
+ console.print("[bold]redeploy scan[/bold] discovering devices...")
120
+
121
+ users = list(ssh_users) if ssh_users else None
122
+ found = discover(
123
+ subnet=subnet,
124
+ ssh_users=users,
125
+ ssh_port=ssh_port,
126
+ ping=ping,
127
+ mdns=not no_mdns,
128
+ probe_ssh=True,
129
+ timeout=timeout,
130
+ )
131
+
132
+ ssh_ok = [h for h in found if h.ssh_ok]
133
+ console.print(f" found {len(found)} host(s), {len(ssh_ok)} SSH-accessible\n")
134
+
135
+ t = Table(show_header=True, box=None, padding=(0, 2))
136
+ t.add_column("IP")
137
+ t.add_column("Hostname", style="dim")
138
+ t.add_column("MAC", style="dim")
139
+ t.add_column("SSH user", style="cyan")
140
+ t.add_column("Source", style="dim")
141
+ for h in found:
142
+ ssh_col = f"[green]{h.ssh_user}[/green]" if h.ssh_ok else "[red]✗[/red]"
143
+ t.add_row(h.ip, h.hostname or "—", h.mac or "—", ssh_col, h.source)
144
+ console.print(t)
145
+
146
+ if not no_save and ssh_ok:
147
+ reg = update_registry(found, save=True)
148
+ console.print(
149
+ f"\n [dim]registry updated → {DeviceRegistry.default_path()}[/dim]"
150
+ )
151
+ console.print(f" [dim]{len(reg.devices)} device(s) total[/dim]")
152
+ elif not ssh_ok:
153
+ console.print("\n [dim]No SSH-accessible devices — nothing saved.[/dim]")
154
+
155
+
156
+ @click.command("device-add")
157
+ @click.argument("host")
158
+ @click.option("--id", "device_id", default=None, help="Device ID (default: host)")
159
+ @click.option("--name", default="", help="Human-friendly label")
160
+ @click.option("--tag", "tags", multiple=True, help="Tag (repeatable)")
161
+ @click.option(
162
+ "--strategy", default="docker_full", show_default=True,
163
+ type=click.Choice(["docker_full", "podman_quadlet", "native_kiosk", "docker_kiosk", "k3s", "systemd"]),
164
+ help="Deploy strategy"
165
+ )
166
+ @click.option("--app", default="", help="Application name")
167
+ @click.option("--port", "ssh_port", default=22, show_default=True)
168
+ @click.option("--key", "ssh_key", default=None, help="Path to SSH private key")
169
+ def device_add(host, device_id, name, tags, strategy, app, ssh_port, ssh_key):
170
+ """Add or update a device in the registry.
171
+
172
+ \b
173
+ Example:
174
+ redeploy device-add pi@192.168.1.42 --tag kiosk --strategy native_kiosk --app kiosk-app
175
+ redeploy device-add root@10.0.0.5 --tag prod --strategy docker_full --app myapp
176
+ """
177
+ from ...models import DeviceRegistry, KnownDevice
178
+
179
+ console = Console()
180
+ reg = DeviceRegistry.load()
181
+
182
+ did = device_id or host
183
+ dev = reg.get(did) or KnownDevice(id=did, host=host)
184
+ dev.host = host
185
+ if name:
186
+ dev.name = name
187
+ if tags:
188
+ dev.tags = list(tags)
189
+ dev.strategy = strategy
190
+ if app:
191
+ dev.app = app
192
+ dev.ssh_port = ssh_port
193
+ if ssh_key:
194
+ dev.ssh_key = ssh_key
195
+ dev.source = "manual"
196
+
197
+ reg.upsert(dev)
198
+ reg.save()
199
+ console.print(
200
+ f"[green]✓[/green] device [bold]{did}[/bold] saved → {DeviceRegistry.default_path()}"
201
+ )
202
+
203
+
204
+ @click.command("device-rm")
205
+ @click.argument("device_id")
206
+ def device_rm(device_id):
207
+ """Remove a device from the registry."""
208
+ from ...models import DeviceRegistry
209
+
210
+ console = Console()
211
+ reg = DeviceRegistry.load()
212
+ if reg.remove(device_id):
213
+ reg.save()
214
+ console.print(f"[green]✓[/green] removed {device_id}")
215
+ else:
216
+ console.print(f"[yellow]⚠ not found: {device_id}[/yellow]")