redeploy 0.2.8__tar.gz → 0.2.9__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 (101) hide show
  1. {redeploy-0.2.8 → redeploy-0.2.9}/PKG-INFO +6 -6
  2. {redeploy-0.2.8 → redeploy-0.2.9}/README.md +5 -5
  3. {redeploy-0.2.8 → redeploy-0.2.9}/pyproject.toml +1 -1
  4. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/__init__.py +1 -1
  5. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/cli.py +49 -14
  6. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/tests/test_version_cli.py +26 -4
  7. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy.egg-info/PKG-INFO +6 -6
  8. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/apply/__init__.py +0 -0
  9. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/apply/executor.py +0 -0
  10. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/data_sync.py +0 -0
  11. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/detect/__init__.py +0 -0
  12. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/detect/detector.py +0 -0
  13. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/detect/probes.py +0 -0
  14. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/detect/remote.py +0 -0
  15. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/detect/templates.py +0 -0
  16. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/detect/workflow.py +0 -0
  17. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/discovery.py +0 -0
  18. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/dsl/__init__.py +0 -0
  19. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/dsl/loader.py +0 -0
  20. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/dsl/parser.py +0 -0
  21. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/dsl_python/__init__.py +0 -0
  22. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/dsl_python/context.py +0 -0
  23. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/dsl_python/decorators.py +0 -0
  24. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/dsl_python/docker_steps.py +0 -0
  25. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/dsl_python/exceptions.py +0 -0
  26. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/dsl_python/runner.py +0 -0
  27. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/dsl_python/steps.py +0 -0
  28. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/fleet.py +0 -0
  29. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/iac/__init__.py +0 -0
  30. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/iac/base.py +0 -0
  31. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/iac/docker_compose.py +0 -0
  32. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/iac/parsers/__init__.py +0 -0
  33. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/iac/parsers/compose.py +0 -0
  34. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/iac/registry.py +0 -0
  35. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/models.py +0 -0
  36. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/observe.py +0 -0
  37. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/parse.py +0 -0
  38. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/patterns.py +0 -0
  39. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/plan/__init__.py +0 -0
  40. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/plan/planner.py +0 -0
  41. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/plugins/__init__.py +0 -0
  42. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/plugins/builtin/__init__.py +0 -0
  43. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/plugins/builtin/browser_reload.py +0 -0
  44. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/plugins/builtin/notify.py +0 -0
  45. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/plugins/builtin/systemd_reload.py +0 -0
  46. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/ssh.py +0 -0
  47. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/steps.py +0 -0
  48. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/tests/__init__.py +0 -0
  49. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/tests/test_cli.py +0 -0
  50. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/tests/test_data_sync.py +0 -0
  51. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/tests/test_discovery.py +0 -0
  52. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/tests/test_dsl.py +0 -0
  53. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/tests/test_examples.py +0 -0
  54. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/tests/test_executor.py +0 -0
  55. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/tests/test_fleet.py +0 -0
  56. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/tests/test_git_integration.py +0 -0
  57. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/tests/test_iac.py +0 -0
  58. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/tests/test_manifest.py +0 -0
  59. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/tests/test_models.py +0 -0
  60. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/tests/test_observe.py +0 -0
  61. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/tests/test_parse.py +0 -0
  62. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/tests/test_patterns.py +0 -0
  63. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/tests/test_planner_edge.py +0 -0
  64. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/tests/test_plugins.py +0 -0
  65. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/tests/test_probes.py +0 -0
  66. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/tests/test_public_api.py +0 -0
  67. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/tests/test_ssh.py +0 -0
  68. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/tests/test_templates.py +0 -0
  69. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/tests/test_verify.py +0 -0
  70. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/tests/test_version.py +0 -0
  71. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/tests/test_version_changelog.py +0 -0
  72. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/tests/test_version_manifest.py +0 -0
  73. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/tests/test_version_sources.py +0 -0
  74. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/tests/test_workflow.py +0 -0
  75. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/verify.py +0 -0
  76. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/version/__init__.py +0 -0
  77. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/version/bump.py +0 -0
  78. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/version/changelog.py +0 -0
  79. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/version/commits.py +0 -0
  80. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/version/diff.py +0 -0
  81. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/version/git_integration.py +0 -0
  82. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/version/git_transaction.py +0 -0
  83. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/version/manifest.py +0 -0
  84. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/version/sources/__init__.py +0 -0
  85. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/version/sources/base.py +0 -0
  86. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/version/sources/json_.py +0 -0
  87. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/version/sources/plain.py +0 -0
  88. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/version/sources/regex.py +0 -0
  89. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/version/sources/toml_.py +0 -0
  90. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/version/sources/yaml_.py +0 -0
  91. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/version/transaction.py +0 -0
  92. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy/version.py +0 -0
  93. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy.egg-info/SOURCES.txt +0 -0
  94. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy.egg-info/dependency_links.txt +0 -0
  95. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy.egg-info/entry_points.txt +0 -0
  96. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy.egg-info/requires.txt +0 -0
  97. {redeploy-0.2.8 → redeploy-0.2.9}/redeploy.egg-info/top_level.txt +0 -0
  98. {redeploy-0.2.8 → redeploy-0.2.9}/setup.cfg +0 -0
  99. {redeploy-0.2.8 → redeploy-0.2.9}/tests/test_iac.py +0 -0
  100. {redeploy-0.2.8 → redeploy-0.2.9}/tests/test_parse.py +0 -0
  101. {redeploy-0.2.8 → redeploy-0.2.9}/tests/test_redeploy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redeploy
3
- Version: 0.2.8
3
+ Version: 0.2.9
4
4
  Summary: Infrastructure migration toolkit: detect → plan → apply
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -26,17 +26,17 @@ Requires-Dist: pfix>=0.1.60; extra == "dev"
26
26
 
27
27
  ## AI Cost Tracking
28
28
 
29
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.8-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
30
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$4.05-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-7.7h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
29
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.9-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
30
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$4.20-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-8.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
31
31
 
32
- - 🤖 **LLM usage:** $4.0500 (27 commits)
33
- - 👤 **Human dev:** ~$773 (7.7h @ $100/h, 30min dedup)
32
+ - 🤖 **LLM usage:** $4.2000 (28 commits)
33
+ - 👤 **Human dev:** ~$802 (8.0h @ $100/h, 30min dedup)
34
34
 
35
35
  Generated on 2026-04-20 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
36
36
 
37
37
  ---
38
38
 
39
- ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.8-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
39
+ ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.9-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
40
40
 
41
41
  Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
42
42
 
@@ -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.8-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-$4.05-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-7.7h-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.9-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-$4.20-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-8.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $4.0500 (27 commits)
10
- - 👤 **Human dev:** ~$773 (7.7h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $4.2000 (28 commits)
10
+ - 👤 **Human dev:** ~$802 (8.0h @ $100/h, 30min dedup)
11
11
 
12
12
  Generated on 2026-04-20 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.8-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.9-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.8"
7
+ version = "0.2.9"
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.8"
10
+ __version__ = "0.2.9"
11
11
 
12
12
  # ── Core models ───────────────────────────────────────────────────────────────
13
13
  from .models import ( # noqa: F401
@@ -3177,18 +3177,53 @@ def _print_version_scan_review(console, root_sources, packages):
3177
3177
  console.print(" No version sources detected")
3178
3178
 
3179
3179
 
3180
- def _print_version_scan_group(console, label: str, sources, *, default_version: str):
3180
+ def _classify_version_scan_source_confidence(source, *, actual: str) -> str:
3181
+ if actual == "(unreadable)":
3182
+ return "unreadable"
3183
+ if source.format == "regex":
3184
+ return "heuristic"
3185
+ return "certain"
3186
+
3187
+
3188
+ def _format_version_scan_source_status(confidence: str, *, conflict: bool) -> str:
3189
+ parts = [f"confidence={confidence}"]
3190
+ if conflict:
3191
+ parts.append("conflict=yes")
3192
+ return " ".join(parts)
3193
+
3194
+
3195
+ def _default_keep_scanned_source(confidence: str, *, conflict: bool) -> bool:
3196
+ return confidence == "certain" and not conflict
3197
+
3198
+
3199
+ def _summarize_version_scan_group(sources, *, default_version: str):
3181
3200
  detected = _read_detected_source_versions(sources)
3182
3201
  detected_map = {str(source.path): version for source, version in detected}
3183
3202
  unique_versions = {version for _, version in detected}
3184
3203
  chosen_version = detected[0][1] if detected else default_version
3185
- suffix = " (conflict)" if len(unique_versions) > 1 else ""
3204
+ has_conflict = len(unique_versions) > 1
3205
+ reviewed_sources = []
3186
3206
 
3187
- console.print(f" {label}: chosen version {chosen_version}{suffix}")
3188
3207
  for source in sources:
3189
3208
  actual = detected_map.get(str(source.path), "(unreadable)")
3190
- marker = " (conflict)" if len(unique_versions) > 1 and actual != chosen_version else ""
3191
- console.print(f" - {source.path} ({source.format}) current: {actual}{marker}")
3209
+ confidence = _classify_version_scan_source_confidence(source, actual=actual)
3210
+ is_conflict = has_conflict and actual not in {"(unreadable)", chosen_version}
3211
+ reviewed_sources.append((source, actual, confidence, is_conflict))
3212
+
3213
+ return chosen_version, has_conflict, reviewed_sources
3214
+
3215
+
3216
+ def _print_version_scan_group(console, label: str, sources, *, default_version: str):
3217
+ chosen_version, has_conflict, reviewed_sources = _summarize_version_scan_group(
3218
+ sources,
3219
+ default_version=default_version,
3220
+ )
3221
+ suffix = " (conflict)" if has_conflict else ""
3222
+
3223
+ console.print(f" {label}: chosen version {chosen_version}{suffix}")
3224
+ for source, actual, confidence, is_conflict in reviewed_sources:
3225
+ status = _format_version_scan_source_status(confidence, conflict=is_conflict)
3226
+ console.print(f" - {source.path} ({source.format}) current: {actual} {status}")
3192
3227
 
3193
3228
 
3194
3229
  def _iter_version_scan_groups(root_sources, packages):
@@ -3205,18 +3240,18 @@ def _review_detected_sources_interactively(console, root_sources, packages) -> s
3205
3240
  console.print("[bold]Interactive scan review[/bold]")
3206
3241
 
3207
3242
  for label, sources, default_version in _iter_version_scan_groups(root_sources, packages):
3208
- detected = _read_detected_source_versions(sources)
3209
- detected_map = {str(source.path): version for source, version in detected}
3210
- unique_versions = {version for _, version in detected}
3211
- chosen_version = detected[0][1] if detected else default_version
3212
- suffix = " (conflict)" if len(unique_versions) > 1 else ""
3243
+ chosen_version, has_conflict, reviewed_sources = _summarize_version_scan_group(
3244
+ sources,
3245
+ default_version=default_version,
3246
+ )
3247
+ suffix = " (conflict)" if has_conflict else ""
3213
3248
 
3214
3249
  console.print(f"\n[bold]{label}[/bold]: chosen version {chosen_version}{suffix}")
3215
- for source in sources:
3216
- actual = detected_map.get(str(source.path), "(unreadable)")
3250
+ for source, actual, confidence, is_conflict in reviewed_sources:
3251
+ status = _format_version_scan_source_status(confidence, conflict=is_conflict)
3217
3252
  keep = click.confirm(
3218
- f"Keep {source.path} ({source.format}) current={actual}?",
3219
- default=True,
3253
+ f"Keep {source.path} ({source.format}) current={actual} {status}?",
3254
+ default=_default_keep_scanned_source(confidence, conflict=is_conflict),
3220
3255
  )
3221
3256
  if not keep:
3222
3257
  rejected.add(source.path.as_posix())
@@ -361,8 +361,9 @@ class TestVersionInit:
361
361
  assert result.exit_code == 0, result.output
362
362
  assert "Scan review" in result.output
363
363
  assert "backend: chosen version 1.0.0 (conflict)" in result.output
364
- assert "backend/VERSION (plain) current: 1.0.0" in result.output
365
- assert "backend/src/version.ts (regex) current: 1.1.0 (conflict)" in result.output
364
+ assert "backend/VERSION (plain) current: 1.0.0 confidence=certain" in result.output
365
+ assert "backend/src/version.ts (regex) current: 1.1.0 confidence=heuristic" in result.output
366
+ assert "conflict=yes" in result.output
366
367
  assert "Review only - manifest not written" in result.output
367
368
  assert not Path(".redeploy/version.yaml").exists()
368
369
 
@@ -385,8 +386,8 @@ class TestVersionInit:
385
386
 
386
387
  assert result.exit_code == 0, result.output
387
388
  assert "Interactive scan review" in result.output
388
- assert "Keep backend/VERSION (plain) current=1.0.0?" in result.output
389
- assert "Keep backend/src/version.ts (regex) current=1.1.0?" in result.output
389
+ assert "Keep backend/VERSION (plain) current=1.0.0 confidence=certain? [Y/n]:" in result.output
390
+ assert "Keep backend/src/version.ts (regex) current=1.1.0 confidence=heuristic conflict=yes? [y/N]:" in result.output
390
391
  assert "Write manifest to .redeploy/version.yaml?" in result.output
391
392
 
392
393
  manifest = yaml.safe_load(Path(".redeploy/version.yaml").read_text(encoding="utf-8"))
@@ -395,6 +396,27 @@ class TestVersionInit:
395
396
  {"path": "backend/VERSION", "format": "plain", "optional": False}
396
397
  ]
397
398
 
399
+ def test_init_scan_interactive_rejects_heuristic_source_by_default(self, tmp_path):
400
+ runner = _runner()
401
+ with runner.isolated_filesystem(temp_dir=tmp_path):
402
+ Path("packages/frontend/src").mkdir(parents=True, exist_ok=True)
403
+ Path("packages/frontend/src/version.ts").write_text(
404
+ 'export const VERSION = "2.1.0"\n',
405
+ encoding="utf-8",
406
+ )
407
+
408
+ result = runner.invoke(
409
+ cli,
410
+ ["version", "init", "--scan", "--interactive"],
411
+ input="\n",
412
+ catch_exceptions=False,
413
+ )
414
+
415
+ assert result.exit_code == 0, result.output
416
+ assert "Keep packages/frontend/src/version.ts (regex) current=2.1.0 confidence=heuristic? [y/N]:" in result.output
417
+ assert "No sources selected - manifest not written" in result.output
418
+ assert not Path(".redeploy/version.yaml").exists()
419
+
398
420
  def test_init_interactive_requires_scan(self, tmp_path):
399
421
  runner = _runner()
400
422
  with runner.isolated_filesystem(temp_dir=tmp_path):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redeploy
3
- Version: 0.2.8
3
+ Version: 0.2.9
4
4
  Summary: Infrastructure migration toolkit: detect → plan → apply
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -26,17 +26,17 @@ Requires-Dist: pfix>=0.1.60; extra == "dev"
26
26
 
27
27
  ## AI Cost Tracking
28
28
 
29
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.8-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
30
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$4.05-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-7.7h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
29
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.9-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
30
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$4.20-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-8.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
31
31
 
32
- - 🤖 **LLM usage:** $4.0500 (27 commits)
33
- - 👤 **Human dev:** ~$773 (7.7h @ $100/h, 30min dedup)
32
+ - 🤖 **LLM usage:** $4.2000 (28 commits)
33
+ - 👤 **Human dev:** ~$802 (8.0h @ $100/h, 30min dedup)
34
34
 
35
35
  Generated on 2026-04-20 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
36
36
 
37
37
  ---
38
38
 
39
- ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.8-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
39
+ ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.9-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
40
40
 
41
41
  Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
42
42
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes