redeploy 0.2.6__tar.gz → 0.2.7__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.6 → redeploy-0.2.7}/PKG-INFO +6 -6
  2. {redeploy-0.2.6 → redeploy-0.2.7}/README.md +5 -5
  3. {redeploy-0.2.6 → redeploy-0.2.7}/pyproject.toml +1 -1
  4. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/__init__.py +1 -1
  5. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/cli.py +135 -35
  6. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/tests/test_version_cli.py +54 -0
  7. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy.egg-info/PKG-INFO +6 -6
  8. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/apply/__init__.py +0 -0
  9. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/apply/executor.py +0 -0
  10. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/data_sync.py +0 -0
  11. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/detect/__init__.py +0 -0
  12. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/detect/detector.py +0 -0
  13. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/detect/probes.py +0 -0
  14. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/detect/remote.py +0 -0
  15. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/detect/templates.py +0 -0
  16. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/detect/workflow.py +0 -0
  17. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/discovery.py +0 -0
  18. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/dsl/__init__.py +0 -0
  19. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/dsl/loader.py +0 -0
  20. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/dsl/parser.py +0 -0
  21. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/dsl_python/__init__.py +0 -0
  22. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/dsl_python/context.py +0 -0
  23. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/dsl_python/decorators.py +0 -0
  24. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/dsl_python/docker_steps.py +0 -0
  25. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/dsl_python/exceptions.py +0 -0
  26. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/dsl_python/runner.py +0 -0
  27. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/dsl_python/steps.py +0 -0
  28. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/fleet.py +0 -0
  29. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/iac/__init__.py +0 -0
  30. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/iac/base.py +0 -0
  31. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/iac/docker_compose.py +0 -0
  32. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/iac/parsers/__init__.py +0 -0
  33. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/iac/parsers/compose.py +0 -0
  34. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/iac/registry.py +0 -0
  35. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/models.py +0 -0
  36. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/observe.py +0 -0
  37. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/parse.py +0 -0
  38. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/patterns.py +0 -0
  39. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/plan/__init__.py +0 -0
  40. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/plan/planner.py +0 -0
  41. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/plugins/__init__.py +0 -0
  42. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/plugins/builtin/__init__.py +0 -0
  43. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/plugins/builtin/browser_reload.py +0 -0
  44. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/plugins/builtin/notify.py +0 -0
  45. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/plugins/builtin/systemd_reload.py +0 -0
  46. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/ssh.py +0 -0
  47. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/steps.py +0 -0
  48. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/tests/__init__.py +0 -0
  49. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/tests/test_cli.py +0 -0
  50. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/tests/test_data_sync.py +0 -0
  51. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/tests/test_discovery.py +0 -0
  52. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/tests/test_dsl.py +0 -0
  53. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/tests/test_examples.py +0 -0
  54. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/tests/test_executor.py +0 -0
  55. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/tests/test_fleet.py +0 -0
  56. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/tests/test_git_integration.py +0 -0
  57. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/tests/test_iac.py +0 -0
  58. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/tests/test_manifest.py +0 -0
  59. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/tests/test_models.py +0 -0
  60. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/tests/test_observe.py +0 -0
  61. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/tests/test_parse.py +0 -0
  62. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/tests/test_patterns.py +0 -0
  63. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/tests/test_planner_edge.py +0 -0
  64. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/tests/test_plugins.py +0 -0
  65. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/tests/test_probes.py +0 -0
  66. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/tests/test_public_api.py +0 -0
  67. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/tests/test_ssh.py +0 -0
  68. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/tests/test_templates.py +0 -0
  69. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/tests/test_verify.py +0 -0
  70. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/tests/test_version.py +0 -0
  71. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/tests/test_version_changelog.py +0 -0
  72. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/tests/test_version_manifest.py +0 -0
  73. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/tests/test_version_sources.py +0 -0
  74. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/tests/test_workflow.py +0 -0
  75. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/verify.py +0 -0
  76. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/version/__init__.py +0 -0
  77. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/version/bump.py +0 -0
  78. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/version/changelog.py +0 -0
  79. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/version/commits.py +0 -0
  80. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/version/diff.py +0 -0
  81. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/version/git_integration.py +0 -0
  82. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/version/git_transaction.py +0 -0
  83. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/version/manifest.py +0 -0
  84. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/version/sources/__init__.py +0 -0
  85. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/version/sources/base.py +0 -0
  86. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/version/sources/json_.py +0 -0
  87. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/version/sources/plain.py +0 -0
  88. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/version/sources/regex.py +0 -0
  89. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/version/sources/toml_.py +0 -0
  90. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/version/sources/yaml_.py +0 -0
  91. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/version/transaction.py +0 -0
  92. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy/version.py +0 -0
  93. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy.egg-info/SOURCES.txt +0 -0
  94. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy.egg-info/dependency_links.txt +0 -0
  95. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy.egg-info/entry_points.txt +0 -0
  96. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy.egg-info/requires.txt +0 -0
  97. {redeploy-0.2.6 → redeploy-0.2.7}/redeploy.egg-info/top_level.txt +0 -0
  98. {redeploy-0.2.6 → redeploy-0.2.7}/setup.cfg +0 -0
  99. {redeploy-0.2.6 → redeploy-0.2.7}/tests/test_iac.py +0 -0
  100. {redeploy-0.2.6 → redeploy-0.2.7}/tests/test_parse.py +0 -0
  101. {redeploy-0.2.6 → redeploy-0.2.7}/tests/test_redeploy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redeploy
3
- Version: 0.2.6
3
+ Version: 0.2.7
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.6-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-$3.75-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-7.4h-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.7-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-$3.90-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-7.6h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
31
31
 
32
- - 🤖 **LLM usage:** $3.7500 (25 commits)
33
- - 👤 **Human dev:** ~$738 (7.4h @ $100/h, 30min dedup)
32
+ - 🤖 **LLM usage:** $3.9000 (26 commits)
33
+ - 👤 **Human dev:** ~$759 (7.6h @ $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.6-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.7-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.6-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-$3.75-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-7.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.7-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-$3.90-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-7.6h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $3.7500 (25 commits)
10
- - 👤 **Human dev:** ~$738 (7.4h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $3.9000 (26 commits)
10
+ - 👤 **Human dev:** ~$759 (7.6h @ $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.6-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.7-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.6"
7
+ version = "0.2.7"
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.6"
10
+ __version__ = "0.2.7"
11
11
 
12
12
  # ── Core models ───────────────────────────────────────────────────────────────
13
13
  from .models import ( # noqa: F401
@@ -2969,46 +2969,146 @@ def version_init(scan, force):
2969
2969
 
2970
2970
  manifest_path.parent.mkdir(parents=True, exist_ok=True)
2971
2971
 
2972
- # Detect sources if --scan
2973
- sources = []
2974
- if scan:
2975
- # Check common locations
2976
- if Path("VERSION").exists():
2977
- sources.append(SourceConfig(path=Path("VERSION"), format="plain"))
2978
- if Path("pyproject.toml").exists():
2979
- sources.append(SourceConfig(path=Path("pyproject.toml"), format="toml", key="project.version"))
2980
- if Path("package.json").exists():
2981
- sources.append(SourceConfig(path=Path("package.json"), format="json", key="version"))
2982
-
2983
- if not sources:
2984
- # Default minimal manifest
2985
- sources = [SourceConfig(path=Path("VERSION"), format="plain")]
2986
-
2987
- # Detect current version
2988
- current = "0.1.0"
2989
- for s in sources:
2990
- if s.path.exists():
2991
- try:
2992
- from .version.sources import get_adapter
2993
- current = get_adapter(s.format).read(s.path, s)
2994
- break
2995
- except Exception:
2996
- pass
2972
+ root = Path.cwd()
2973
+ root_sources = []
2974
+ packages = None
2997
2975
 
2998
- m = VersionManifest(
2999
- version=current,
3000
- scheme="semver",
3001
- policy="synced",
3002
- sources=sources,
3003
- git=GitConfig(),
3004
- )
2976
+ if scan:
2977
+ root_sources = _detect_version_sources_in_dir(root, root)
2978
+ packages = _scan_package_version_manifests(root)
2979
+
2980
+ if packages:
2981
+ current = _detect_source_version(root_sources, default="0.0.0") if root_sources else "0.0.0"
2982
+ m = VersionManifest(
2983
+ version=current,
2984
+ scheme="semver",
2985
+ policy="independent",
2986
+ sources=root_sources,
2987
+ git=GitConfig(),
2988
+ packages=packages,
2989
+ )
2990
+ else:
2991
+ sources = root_sources
2992
+ if not sources:
2993
+ sources = [SourceConfig(path=Path("VERSION"), format="plain")]
2994
+
2995
+ current = _detect_source_version(sources, default="0.1.0")
2996
+ m = VersionManifest(
2997
+ version=current,
2998
+ scheme="semver",
2999
+ policy="synced",
3000
+ sources=sources,
3001
+ git=GitConfig(),
3002
+ )
3005
3003
 
3006
3004
  m.save(manifest_path)
3007
3005
  console.print(f"[green]✓ Created {manifest_path}[/green]")
3008
3006
  console.print(f" Current version: {current}")
3009
- console.print(f" Sources: {len(sources)}")
3010
- for s in sources:
3011
- console.print(f" - {s.path} ({s.format})")
3007
+ console.print(f" Policy: {m.policy}")
3008
+
3009
+ if m.sources:
3010
+ console.print(f" Sources: {len(m.sources)}")
3011
+ for source in m.sources:
3012
+ console.print(f" - {source.path} ({source.format})")
3013
+
3014
+ if m.is_monorepo():
3015
+ console.print(f" Packages: {len(m.packages or {})}")
3016
+ for package_name in m.list_packages():
3017
+ package = m.get_package(package_name)
3018
+ console.print(f" - {package_name}: {package.version} ({len(package.sources)} sources)")
3019
+
3020
+
3021
+ def _version_scan_specs():
3022
+ return [
3023
+ ("VERSION", "plain", None),
3024
+ ("pyproject.toml", "toml", "project.version"),
3025
+ ("package.json", "json", "version"),
3026
+ ]
3027
+
3028
+
3029
+ def _is_scannable_version_path(path: Path) -> bool:
3030
+ ignored = {".git", ".venv", ".redeploy", "node_modules", "__pycache__", "dist", "build"}
3031
+ return not any(part in ignored for part in path.parts)
3032
+
3033
+
3034
+ def _detect_version_sources_in_dir(directory: Path, workspace_root: Path):
3035
+ from .version.manifest import SourceConfig
3036
+
3037
+ sources = []
3038
+ for filename, format_name, key in _version_scan_specs():
3039
+ candidate = directory / filename
3040
+ if not candidate.exists() or not candidate.is_file():
3041
+ continue
3042
+
3043
+ relative_path = candidate.relative_to(workspace_root)
3044
+ if not _is_scannable_version_path(relative_path):
3045
+ continue
3046
+
3047
+ source_kwargs = {
3048
+ "path": relative_path,
3049
+ "format": format_name,
3050
+ }
3051
+ if key is not None:
3052
+ source_kwargs["key"] = key
3053
+ sources.append(SourceConfig(**source_kwargs))
3054
+
3055
+ return sources
3056
+
3057
+
3058
+ def _detect_source_version(sources, *, default: str):
3059
+ from .version.sources import get_adapter
3060
+
3061
+ for source in sources:
3062
+ try:
3063
+ return get_adapter(source.format).read(source.path, source)
3064
+ except Exception:
3065
+ continue
3066
+ return default
3067
+
3068
+
3069
+ def _derive_scanned_package_name(package_dir: Path, workspace_root: Path, used_names: set[str]) -> str:
3070
+ relative_dir = package_dir.relative_to(workspace_root)
3071
+ if len(relative_dir.parts) > 1 and relative_dir.parts[0] in {"packages", "apps", "services", "modules"}:
3072
+ candidate = relative_dir.parts[-1]
3073
+ else:
3074
+ candidate = relative_dir.as_posix()
3075
+
3076
+ if candidate in used_names:
3077
+ candidate = relative_dir.as_posix().replace("/", "-")
3078
+
3079
+ return candidate
3080
+
3081
+
3082
+ def _scan_package_version_manifests(workspace_root: Path):
3083
+ from .version.manifest import PackageConfig
3084
+
3085
+ package_dirs = set()
3086
+ for filename, _, _ in _version_scan_specs():
3087
+ for candidate in workspace_root.rglob(filename):
3088
+ if not candidate.is_file():
3089
+ continue
3090
+ relative_path = candidate.relative_to(workspace_root)
3091
+ if not _is_scannable_version_path(relative_path):
3092
+ continue
3093
+ if candidate.parent == workspace_root:
3094
+ continue
3095
+ package_dirs.add(candidate.parent)
3096
+
3097
+ packages = {}
3098
+ used_names = set()
3099
+ for package_dir in sorted(package_dirs, key=lambda item: item.as_posix()):
3100
+ sources = _detect_version_sources_in_dir(package_dir, workspace_root)
3101
+ if not sources:
3102
+ continue
3103
+
3104
+ package_name = _derive_scanned_package_name(package_dir, workspace_root, used_names)
3105
+ used_names.add(package_name)
3106
+ packages[package_name] = PackageConfig(
3107
+ version=_detect_source_version(sources, default="0.1.0"),
3108
+ sources=sources,
3109
+ )
3110
+
3111
+ return packages or None
3012
3112
 
3013
3113
 
3014
3114
  @version_cmd.command(name="diff")
@@ -238,6 +238,60 @@ class TestVersionCurrent:
238
238
  assert "2.0.0" in result.output
239
239
 
240
240
 
241
+ class TestVersionInit:
242
+ def test_init_scan_creates_synced_manifest_from_root_sources(self, tmp_path):
243
+ runner = _runner()
244
+ with runner.isolated_filesystem(temp_dir=tmp_path):
245
+ Path("VERSION").write_text("2.3.4\n", encoding="utf-8")
246
+
247
+ result = runner.invoke(
248
+ cli,
249
+ ["version", "init", "--scan"],
250
+ catch_exceptions=False,
251
+ )
252
+
253
+ assert result.exit_code == 0, result.output
254
+ manifest = yaml.safe_load(Path(".redeploy/version.yaml").read_text(encoding="utf-8"))
255
+ assert manifest["version"]["version"] == "2.3.4"
256
+ assert manifest["version"]["policy"] == "synced"
257
+ assert manifest["version"]["sources"] == [
258
+ {"path": "VERSION", "format": "plain", "optional": False}
259
+ ]
260
+
261
+ def test_init_scan_creates_independent_manifest_from_package_dirs(self, tmp_path):
262
+ runner = _runner()
263
+ with runner.isolated_filesystem(temp_dir=tmp_path):
264
+ Path("backend").mkdir(parents=True, exist_ok=True)
265
+ Path("frontend").mkdir(parents=True, exist_ok=True)
266
+ Path("backend/VERSION").write_text("1.0.0\n", encoding="utf-8")
267
+ Path("frontend/package.json").write_text(
268
+ '{"name": "frontend", "version": "2.0.0"}\n',
269
+ encoding="utf-8",
270
+ )
271
+
272
+ result = runner.invoke(
273
+ cli,
274
+ ["version", "init", "--scan"],
275
+ catch_exceptions=False,
276
+ )
277
+
278
+ assert result.exit_code == 0, result.output
279
+ manifest = yaml.safe_load(Path(".redeploy/version.yaml").read_text(encoding="utf-8"))
280
+ assert manifest["version"]["policy"] == "independent"
281
+ assert manifest["version"]["version"] == "0.0.0"
282
+ assert manifest["version"].get("sources", []) == []
283
+ assert manifest["version"]["packages"]["backend"]["version"] == "1.0.0"
284
+ assert manifest["version"]["packages"]["backend"]["sources"] == [
285
+ {"path": "backend/VERSION", "format": "plain", "optional": False}
286
+ ]
287
+ assert manifest["version"]["packages"]["frontend"]["version"] == "2.0.0"
288
+ assert manifest["version"]["packages"]["frontend"]["sources"] == [
289
+ {"path": "frontend/package.json", "format": "json", "key": "version", "optional": False}
290
+ ]
291
+ assert "Policy: independent" in result.output
292
+ assert "Packages: 2" in result.output
293
+
294
+
241
295
  class TestVersionVerify:
242
296
  def test_verify_all_packages_succeeds_when_in_sync(self, tmp_path):
243
297
  runner = _runner()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redeploy
3
- Version: 0.2.6
3
+ Version: 0.2.7
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.6-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-$3.75-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-7.4h-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.7-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-$3.90-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-7.6h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
31
31
 
32
- - 🤖 **LLM usage:** $3.7500 (25 commits)
33
- - 👤 **Human dev:** ~$738 (7.4h @ $100/h, 30min dedup)
32
+ - 🤖 **LLM usage:** $3.9000 (26 commits)
33
+ - 👤 **Human dev:** ~$759 (7.6h @ $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.6-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.7-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