redeploy 0.2.51__tar.gz → 0.2.52__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.51 → redeploy-0.2.52}/PKG-INFO +43 -6
- {redeploy-0.2.51 → redeploy-0.2.52}/README.md +42 -5
- {redeploy-0.2.51 → redeploy-0.2.52}/pyproject.toml +1 -1
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/__init__.py +5 -1
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/apply/executor.py +88 -2
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/commands/detect.py +20 -1
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/commands/import_.py +103 -2
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/commands/prompt_cmd.py +28 -2
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/heal.py +37 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/integrations/op3_bridge.py +44 -1
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/markpact/compiler.py +6 -1
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/models.py +102 -2
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/plan/planner.py +1 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/schema.py +67 -0
- redeploy-0.2.52/redeploy/tests/test_heal.py +26 -0
- redeploy-0.2.52/redeploy/tests/test_hooks.py +123 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy.egg-info/PKG-INFO +43 -6
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy.egg-info/SOURCES.txt +7 -0
- redeploy-0.2.52/tests/test_example_argocd_flux_plugin.py +82 -0
- redeploy-0.2.52/tests/test_example_gitops_ci_plugin.py +74 -0
- redeploy-0.2.52/tests/test_example_helm_kustomize_plugin.py +72 -0
- redeploy-0.2.52/tests/test_iac_plugin_loading.py +79 -0
- redeploy-0.2.52/tests/test_import_plugin_template.py +55 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/tests/test_prompt_llm.py +93 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/apply/__init__.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/apply/exceptions.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/apply/handlers.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/apply/progress.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/apply/rollback.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/apply/state.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/apply/state_apply.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/apply/utils/__init__.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/apply/utils/run_container_build.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/audit.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/blueprint/__init__.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/blueprint/extractor.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/blueprint/generators/__init__.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/blueprint/generators/docker_compose.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/blueprint/generators/migration.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/blueprint/sources/__init__.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/blueprint/sources/compose.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/blueprint/sources/hardware.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/blueprint/sources/infra.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/blueprint/sources/migration.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/__init__.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/commands/__init__.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/commands/audit.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/commands/blueprint.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/commands/bump_fix.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/commands/device_map.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/commands/device_map_renderers.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/commands/devices.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/commands/diagnose.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/commands/diff.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/commands/exec_.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/commands/export.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/commands/hardware.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/commands/init.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/commands/inspect.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/commands/mcp_cmd.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/commands/patterns.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/commands/plan_apply.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/commands/plugin.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/commands/probe.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/commands/push.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/commands/state.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/commands/status.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/commands/target.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/commands/version/__init__.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/commands/version/commands.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/commands/version/helpers.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/commands/version/monorepo.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/commands/version/release.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/commands/version/scanner.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/commands/workflow.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/core.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/display.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli/query.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/cli.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/config_apply/__init__.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/config_apply/applier.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/config_apply/loader.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/data_sync.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/detect/__init__.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/detect/builtin/__init__.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/detect/detector.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/detect/hardware.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/detect/hardware_rules.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/detect/probes.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/detect/remote.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/detect/templates.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/detect/workflow.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/discovery.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/dsl/__init__.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/dsl/loader.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/dsl/parser.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/dsl_python/__init__.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/dsl_python/context.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/dsl_python/decorators.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/dsl_python/docker_steps.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/dsl_python/exceptions.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/dsl_python/runner.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/dsl_python/steps.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/fleet.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/hardware/__init__.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/hardware/config_txt.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/hardware/data/__init__.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/hardware/data/hyperpixel.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/hardware/data/official.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/hardware/data/waveshare.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/hardware/fixes.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/hardware/kiosk/__init__.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/hardware/kiosk/autostart.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/hardware/kiosk/browsers.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/hardware/kiosk/compositors.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/hardware/kiosk/output_profiles.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/hardware/panels.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/hardware/raspi_config.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/iac/__init__.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/iac/base.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/iac/config_hints.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/iac/docker_compose.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/iac/parsers/__init__.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/iac/parsers/compose.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/iac/registry.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/integrations/__init__.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/markpact/__init__.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/markpact/models.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/markpact/parser.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/mcp_server.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/observe.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/parse.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/patterns.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/plan/__init__.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/plugins/__init__.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/plugins/builtin/__init__.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/plugins/builtin/browser_reload.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/plugins/builtin/hardware_diagnostic.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/plugins/builtin/notify.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/plugins/builtin/process_control.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/plugins/builtin/systemd_reload.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/spec_loader.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/ssh.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/steps/__init__.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/steps/builtins.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/steps/kiosk.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/tests/__init__.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/tests/test_audit.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/tests/test_cli.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/tests/test_data_sync.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/tests/test_discovery.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/tests/test_dsl.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/tests/test_examples.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/tests/test_executor.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/tests/test_fleet.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/tests/test_git_integration.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/tests/test_iac.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/tests/test_manifest.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/tests/test_markpact_compiler.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/tests/test_markpact_parser.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/tests/test_models.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/tests/test_observe.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/tests/test_parse.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/tests/test_patterns.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/tests/test_planner_edge.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/tests/test_plugins.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/tests/test_probes.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/tests/test_public_api.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/tests/test_resume.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/tests/test_spec_loader.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/tests/test_ssh.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/tests/test_templates.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/tests/test_verify.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/tests/test_version.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/tests/test_version_changelog.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/tests/test_version_cli.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/tests/test_version_manifest.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/tests/test_version_sources.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/tests/test_workflow.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/verify.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/version/__init__.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/version/bump.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/version/changelog.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/version/commits.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/version/diff.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/version/git_integration.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/version/git_transaction.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/version/manifest.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/version/sources/__init__.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/version/sources/base.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/version/sources/json_.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/version/sources/plain.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/version/sources/regex.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/version/sources/toml_.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/version/sources/yaml_.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/version/transaction.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy/version.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy.egg-info/dependency_links.txt +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy.egg-info/entry_points.txt +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy.egg-info/requires.txt +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/redeploy.egg-info/top_level.txt +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/setup.cfg +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/tests/test_blueprint_sources.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/tests/test_blueprint_sources_compose.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/tests/test_cli_query.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/tests/test_hardware_config.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/tests/test_hardware_kiosk.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/tests/test_hardware_rules.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/tests/test_iac.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/tests/test_iac_config_hints.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/tests/test_parse.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/tests/test_redeploy.py +0 -0
- {redeploy-0.2.51 → redeploy-0.2.52}/tests/test_steps_kiosk.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.52
|
|
4
4
|
Summary: Infrastructure migration toolkit: detect → plan → apply
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -32,17 +32,17 @@ Requires-Dist: mcp>=1.0; extra == "mcp"
|
|
|
32
32
|
|
|
33
33
|
## AI Cost Tracking
|
|
34
34
|
|
|
35
|
-
     
|
|
36
|
+
  
|
|
37
37
|
|
|
38
|
-
- 🤖 **LLM usage:** $7.5000 (
|
|
39
|
-
- 👤 **Human dev:** ~$
|
|
38
|
+
- 🤖 **LLM usage:** $7.5000 (72 commits)
|
|
39
|
+
- 👤 **Human dev:** ~$2695 (27.0h @ $100/h, 30min dedup)
|
|
40
40
|
|
|
41
41
|
Generated on 2026-04-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
42
42
|
|
|
43
43
|
---
|
|
44
44
|
|
|
45
|
-
    
|
|
46
46
|
|
|
47
47
|
Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
|
|
48
48
|
|
|
@@ -684,6 +684,43 @@ Parser plugin extension:
|
|
|
684
684
|
- Project-local parsers: `./redeploy_iac_parsers/*.py`
|
|
685
685
|
- User-global parsers: `~/.redeploy/iac_parsers/*.py`
|
|
686
686
|
|
|
687
|
+
Built-in template generator:
|
|
688
|
+
- `redeploy import --list-plugin-templates`
|
|
689
|
+
- `redeploy import --plugin-template helm-kustomize`
|
|
690
|
+
- `redeploy import --plugin-template argocd-flux --plugin-dir redeploy_iac_parsers`
|
|
691
|
+
|
|
692
|
+
Example external plugin (Helm + Ansible):
|
|
693
|
+
- Source template: `examples/redeploy_iac_parsers/helm_ansible.py`
|
|
694
|
+
- Quick start:
|
|
695
|
+
1. `mkdir -p redeploy_iac_parsers`
|
|
696
|
+
2. `cp examples/redeploy_iac_parsers/helm_ansible.py redeploy_iac_parsers/`
|
|
697
|
+
3. `redeploy import path/to/Chart.yaml`
|
|
698
|
+
4. `redeploy import path/to/playbook.yml`
|
|
699
|
+
|
|
700
|
+
Example external plugin (Helm templates + Kustomize):
|
|
701
|
+
- Source template: `examples/redeploy_iac_parsers/helm_kustomize.py`
|
|
702
|
+
- Quick start:
|
|
703
|
+
1. `mkdir -p redeploy_iac_parsers`
|
|
704
|
+
2. `cp examples/redeploy_iac_parsers/helm_kustomize.py redeploy_iac_parsers/`
|
|
705
|
+
3. `redeploy import path/to/chart/templates/deployment.yaml`
|
|
706
|
+
4. `redeploy import path/to/kustomization.yaml`
|
|
707
|
+
|
|
708
|
+
Example external plugin (ArgoCD Application + Flux Kustomization):
|
|
709
|
+
- Source template: `examples/redeploy_iac_parsers/argocd_flux.py`
|
|
710
|
+
- Quick start:
|
|
711
|
+
1. `mkdir -p redeploy_iac_parsers`
|
|
712
|
+
2. `cp examples/redeploy_iac_parsers/argocd_flux.py redeploy_iac_parsers/`
|
|
713
|
+
3. `redeploy import path/to/argocd-application.yaml`
|
|
714
|
+
4. `redeploy import path/to/flux-kustomization.yaml`
|
|
715
|
+
|
|
716
|
+
Example external plugin (GitOps CI for ArgoCD/Flux):
|
|
717
|
+
- Source template: `examples/redeploy_iac_parsers/gitops_ci.py`
|
|
718
|
+
- Quick start:
|
|
719
|
+
1. `mkdir -p redeploy_iac_parsers`
|
|
720
|
+
2. `cp examples/redeploy_iac_parsers/gitops_ci.py redeploy_iac_parsers/`
|
|
721
|
+
3. `redeploy import .github/workflows/deploy-gitops.yml`
|
|
722
|
+
4. `redeploy import .gitlab-ci.yml`
|
|
723
|
+
|
|
687
724
|
## Examples
|
|
688
725
|
|
|
689
726
|
| Directory | Scenario | Strategy |
|
|
@@ -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 (72 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$2695 (27.0h @ $100/h, 30min dedup)
|
|
11
11
|
|
|
12
12
|
Generated on 2026-04-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
-
    
|
|
17
17
|
|
|
18
18
|
Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
|
|
19
19
|
|
|
@@ -655,6 +655,43 @@ Parser plugin extension:
|
|
|
655
655
|
- Project-local parsers: `./redeploy_iac_parsers/*.py`
|
|
656
656
|
- User-global parsers: `~/.redeploy/iac_parsers/*.py`
|
|
657
657
|
|
|
658
|
+
Built-in template generator:
|
|
659
|
+
- `redeploy import --list-plugin-templates`
|
|
660
|
+
- `redeploy import --plugin-template helm-kustomize`
|
|
661
|
+
- `redeploy import --plugin-template argocd-flux --plugin-dir redeploy_iac_parsers`
|
|
662
|
+
|
|
663
|
+
Example external plugin (Helm + Ansible):
|
|
664
|
+
- Source template: `examples/redeploy_iac_parsers/helm_ansible.py`
|
|
665
|
+
- Quick start:
|
|
666
|
+
1. `mkdir -p redeploy_iac_parsers`
|
|
667
|
+
2. `cp examples/redeploy_iac_parsers/helm_ansible.py redeploy_iac_parsers/`
|
|
668
|
+
3. `redeploy import path/to/Chart.yaml`
|
|
669
|
+
4. `redeploy import path/to/playbook.yml`
|
|
670
|
+
|
|
671
|
+
Example external plugin (Helm templates + Kustomize):
|
|
672
|
+
- Source template: `examples/redeploy_iac_parsers/helm_kustomize.py`
|
|
673
|
+
- Quick start:
|
|
674
|
+
1. `mkdir -p redeploy_iac_parsers`
|
|
675
|
+
2. `cp examples/redeploy_iac_parsers/helm_kustomize.py redeploy_iac_parsers/`
|
|
676
|
+
3. `redeploy import path/to/chart/templates/deployment.yaml`
|
|
677
|
+
4. `redeploy import path/to/kustomization.yaml`
|
|
678
|
+
|
|
679
|
+
Example external plugin (ArgoCD Application + Flux Kustomization):
|
|
680
|
+
- Source template: `examples/redeploy_iac_parsers/argocd_flux.py`
|
|
681
|
+
- Quick start:
|
|
682
|
+
1. `mkdir -p redeploy_iac_parsers`
|
|
683
|
+
2. `cp examples/redeploy_iac_parsers/argocd_flux.py redeploy_iac_parsers/`
|
|
684
|
+
3. `redeploy import path/to/argocd-application.yaml`
|
|
685
|
+
4. `redeploy import path/to/flux-kustomization.yaml`
|
|
686
|
+
|
|
687
|
+
Example external plugin (GitOps CI for ArgoCD/Flux):
|
|
688
|
+
- Source template: `examples/redeploy_iac_parsers/gitops_ci.py`
|
|
689
|
+
- Quick start:
|
|
690
|
+
1. `mkdir -p redeploy_iac_parsers`
|
|
691
|
+
2. `cp examples/redeploy_iac_parsers/gitops_ci.py redeploy_iac_parsers/`
|
|
692
|
+
3. `redeploy import .github/workflows/deploy-gitops.yml`
|
|
693
|
+
4. `redeploy import .gitlab-ci.yml`
|
|
694
|
+
|
|
658
695
|
## Examples
|
|
659
696
|
|
|
660
697
|
| Directory | Scenario | Strategy |
|
|
@@ -7,17 +7,19 @@ 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.52"
|
|
11
11
|
|
|
12
12
|
# ── Core models ───────────────────────────────────────────────────────────────
|
|
13
13
|
from .models import ( # noqa: F401
|
|
14
14
|
ConflictSeverity,
|
|
15
15
|
DeployStrategy,
|
|
16
|
+
Hook,
|
|
16
17
|
InfraSpec,
|
|
17
18
|
InfraState,
|
|
18
19
|
MigrationPlan,
|
|
19
20
|
MigrationSpec,
|
|
20
21
|
MigrationStep,
|
|
22
|
+
PipelinePhase,
|
|
21
23
|
StepAction,
|
|
22
24
|
StepStatus,
|
|
23
25
|
TargetConfig,
|
|
@@ -91,11 +93,13 @@ __all__ = [
|
|
|
91
93
|
# models
|
|
92
94
|
"ConflictSeverity",
|
|
93
95
|
"DeployStrategy",
|
|
96
|
+
"Hook",
|
|
94
97
|
"InfraSpec",
|
|
95
98
|
"InfraState",
|
|
96
99
|
"MigrationPlan",
|
|
97
100
|
"MigrationSpec",
|
|
98
101
|
"MigrationStep",
|
|
102
|
+
"PipelinePhase",
|
|
99
103
|
"StepAction",
|
|
100
104
|
"StepStatus",
|
|
101
105
|
"TargetConfig",
|
|
@@ -3,13 +3,13 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
import time
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import IO, Optional
|
|
6
|
+
from typing import IO, Any, Optional
|
|
7
7
|
|
|
8
8
|
import yaml
|
|
9
9
|
from loguru import logger
|
|
10
10
|
|
|
11
11
|
from ..detect.remote import RemoteProbe
|
|
12
|
-
from ..models import MigrationPlan, MigrationStep, StepAction, StepStatus
|
|
12
|
+
from ..models import Hook, MigrationPlan, MigrationStep, StepAction, StepStatus
|
|
13
13
|
from .exceptions import StepError
|
|
14
14
|
from .handlers import (
|
|
15
15
|
run_ssh, run_scp, run_rsync, run_docker_build, run_podman_build,
|
|
@@ -100,11 +100,13 @@ class Executor:
|
|
|
100
100
|
logger.info(f"resume: skipping {len(skip_ids)} already-completed step(s): "
|
|
101
101
|
f"{', '.join(sorted(skip_ids))}")
|
|
102
102
|
|
|
103
|
+
self._fire_hooks("before_apply")
|
|
103
104
|
ok = self._execute_steps_loop(skip_ids)
|
|
104
105
|
elapsed = time.monotonic() - self._t0
|
|
105
106
|
|
|
106
107
|
self._handle_completion(ok, elapsed)
|
|
107
108
|
self._write_audit(ok=ok, elapsed_s=elapsed)
|
|
109
|
+
self._fire_hooks("always", ok=ok, elapsed_s=elapsed)
|
|
108
110
|
return ok
|
|
109
111
|
|
|
110
112
|
def _execute_steps_loop(self, skip_ids: set[str]) -> bool:
|
|
@@ -118,13 +120,16 @@ class Executor:
|
|
|
118
120
|
try:
|
|
119
121
|
if self._emitter:
|
|
120
122
|
self._emitter.step_start(i, step)
|
|
123
|
+
self._fire_hooks("before_step", step=step)
|
|
121
124
|
self._execute_step(step)
|
|
122
125
|
self._completed.append(step)
|
|
123
126
|
if self._state is not None:
|
|
124
127
|
self._state.mark_done(step.id)
|
|
125
128
|
if self._emitter:
|
|
126
129
|
self._emitter.step_done(i, step)
|
|
130
|
+
self._fire_hooks("after_step", step=step)
|
|
127
131
|
except StepError as e:
|
|
132
|
+
self._fire_hooks("on_step_failure", step=step, error=str(e))
|
|
128
133
|
self._handle_step_failure(i, step, e)
|
|
129
134
|
ok = False
|
|
130
135
|
break
|
|
@@ -159,10 +164,91 @@ class Executor:
|
|
|
159
164
|
# Plan finished cleanly — drop the checkpoint.
|
|
160
165
|
if self._state is not None:
|
|
161
166
|
self._state.remove()
|
|
167
|
+
self._fire_hooks("after_apply", elapsed_s=elapsed)
|
|
162
168
|
else:
|
|
163
169
|
if self._state is not None and self._state_path is not None:
|
|
164
170
|
logger.info(f"resume: checkpoint saved → {self._state_path} "
|
|
165
171
|
f"({self._state.completed_count}/{self._state.total_steps} done)")
|
|
172
|
+
self._fire_hooks("on_failure", elapsed_s=elapsed)
|
|
173
|
+
|
|
174
|
+
# ── Pipeline hooks (generic) ─────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
def _fire_hooks(self, phase: str, **context: Any) -> None:
|
|
177
|
+
"""Fire all hooks registered for a given phase. Honors ``when`` + ``on_failure``."""
|
|
178
|
+
hooks = [h for h in getattr(self.plan, "hooks", []) if h.phase == phase]
|
|
179
|
+
if not hooks:
|
|
180
|
+
return
|
|
181
|
+
for hook in hooks:
|
|
182
|
+
if hook.when and not self._eval_hook_condition(hook.when, context):
|
|
183
|
+
continue
|
|
184
|
+
try:
|
|
185
|
+
self._execute_hook(hook, context)
|
|
186
|
+
except Exception as exc: # noqa: BLE001
|
|
187
|
+
self._handle_hook_failure(hook, exc)
|
|
188
|
+
|
|
189
|
+
def _eval_hook_condition(self, expr: str, context: dict) -> bool:
|
|
190
|
+
"""Evaluate a simple hook condition against the context.
|
|
191
|
+
|
|
192
|
+
Supports only ``step.id == 'foo'`` / ``step.id != 'foo'`` for now to avoid
|
|
193
|
+
executing arbitrary expressions. Extend with jmespath when needed.
|
|
194
|
+
"""
|
|
195
|
+
step = context.get("step")
|
|
196
|
+
step_id = getattr(step, "id", None)
|
|
197
|
+
expr = expr.strip()
|
|
198
|
+
for op in ("==", "!="):
|
|
199
|
+
if op in expr:
|
|
200
|
+
left, right = [p.strip() for p in expr.split(op, 1)]
|
|
201
|
+
right = right.strip("'\"")
|
|
202
|
+
if left == "step.id":
|
|
203
|
+
return (step_id == right) if op == "==" else (step_id != right)
|
|
204
|
+
logger.warning(f"hook: unsupported when expression: {expr!r}")
|
|
205
|
+
return True
|
|
206
|
+
|
|
207
|
+
def _execute_hook(self, hook: "Hook", context: dict) -> None:
|
|
208
|
+
"""Execute a hook. Built-in actions handled inline; rest delegated to step runners."""
|
|
209
|
+
action = hook.action
|
|
210
|
+
extra = hook.model_dump(exclude={"id", "phase", "action", "description", "when", "on_failure"})
|
|
211
|
+
logger.info(f"hook[{hook.phase}] {hook.id}: {action}")
|
|
212
|
+
if self.dry_run:
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
# Built-in: local_cmd — run a command on the controller machine (opens browser, clears local cache, etc.)
|
|
216
|
+
if action == "local_cmd":
|
|
217
|
+
import subprocess
|
|
218
|
+
cmd = extra.get("command") or ""
|
|
219
|
+
if not cmd:
|
|
220
|
+
raise ValueError(f"hook {hook.id}: local_cmd requires 'command'")
|
|
221
|
+
subprocess.run(cmd, shell=True, check=True)
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
# Built-in: open_url — open a URL in the default browser (controller machine)
|
|
225
|
+
if action == "open_url":
|
|
226
|
+
import webbrowser
|
|
227
|
+
url = extra.get("url") or extra.get("command") or ""
|
|
228
|
+
if not url:
|
|
229
|
+
raise ValueError(f"hook {hook.id}: open_url requires 'url'")
|
|
230
|
+
webbrowser.open(url)
|
|
231
|
+
return
|
|
232
|
+
|
|
233
|
+
# Fallback: treat hook like a MigrationStep and let existing runners handle it.
|
|
234
|
+
step = MigrationStep(
|
|
235
|
+
id=hook.id,
|
|
236
|
+
action=action, # pydantic will validate against StepAction enum
|
|
237
|
+
description=hook.description or f"hook:{hook.phase}:{hook.id}",
|
|
238
|
+
**extra,
|
|
239
|
+
)
|
|
240
|
+
self._execute_step(step)
|
|
241
|
+
|
|
242
|
+
def _handle_hook_failure(self, hook: "Hook", exc: Exception) -> None:
|
|
243
|
+
policy = hook.on_failure
|
|
244
|
+
msg = f"hook {hook.id} ({hook.phase}) failed: {exc}"
|
|
245
|
+
if policy == "abort":
|
|
246
|
+
logger.error(msg)
|
|
247
|
+
raise exc
|
|
248
|
+
if policy == "warn":
|
|
249
|
+
logger.warning(msg)
|
|
250
|
+
else: # continue
|
|
251
|
+
logger.debug(msg)
|
|
166
252
|
|
|
167
253
|
def _compute_skip_set(self) -> set[str]:
|
|
168
254
|
"""Determine which step ids should be skipped this run.
|
|
@@ -105,7 +105,26 @@ def detect(
|
|
|
105
105
|
|
|
106
106
|
try:
|
|
107
107
|
d = Detector(host=host, app=app_name, domain=domain)
|
|
108
|
-
|
|
108
|
+
from ...integrations.op3_bridge import should_use_op3
|
|
109
|
+
if should_use_op3():
|
|
110
|
+
from ...integrations.op3_bridge import (
|
|
111
|
+
make_scanner,
|
|
112
|
+
make_ssh_context,
|
|
113
|
+
snapshot_to_infra_state,
|
|
114
|
+
)
|
|
115
|
+
scanner = make_scanner(
|
|
116
|
+
[
|
|
117
|
+
"runtime.container",
|
|
118
|
+
"service.containers",
|
|
119
|
+
"endpoint.http",
|
|
120
|
+
"business.health",
|
|
121
|
+
]
|
|
122
|
+
)
|
|
123
|
+
ctx = make_ssh_context(target=host)
|
|
124
|
+
snapshot = scanner.scan(host, ctx.execute)
|
|
125
|
+
state = snapshot_to_infra_state(snapshot, host=host)
|
|
126
|
+
else:
|
|
127
|
+
state = d.run()
|
|
109
128
|
d.save(state, out_path)
|
|
110
129
|
except ConnectionError as e:
|
|
111
130
|
console.print(f"[red]✗ {e}[/red]")
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""import command — Parse IaC/CI-CD file and produce migration.yaml scaffold."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
+
import shutil
|
|
4
5
|
import sys
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
|
|
@@ -10,7 +11,7 @@ from rich.table import Table
|
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
@click.command(name="import")
|
|
13
|
-
@click.argument("source", type=click.Path(
|
|
14
|
+
@click.argument("source", required=False, type=click.Path())
|
|
14
15
|
@click.option(
|
|
15
16
|
"-o", "--output", default=None, type=click.Path(),
|
|
16
17
|
help="Output migration.yaml path (default: <source-stem>.migration.yaml)"
|
|
@@ -36,7 +37,36 @@ from rich.table import Table
|
|
|
36
37
|
"--parser", default=None,
|
|
37
38
|
help="Force specific parser (e.g. docker_compose). Default: auto-detect"
|
|
38
39
|
)
|
|
39
|
-
|
|
40
|
+
@click.option(
|
|
41
|
+
"--plugin-template",
|
|
42
|
+
type=click.Choice(["helm-ansible", "helm-kustomize", "argocd-flux", "gitops-ci"]),
|
|
43
|
+
default=None,
|
|
44
|
+
help="Copy a ready-made parser plugin template into the project instead of parsing SOURCE.",
|
|
45
|
+
)
|
|
46
|
+
@click.option(
|
|
47
|
+
"--plugin-dir",
|
|
48
|
+
default="redeploy_iac_parsers",
|
|
49
|
+
type=click.Path(),
|
|
50
|
+
show_default=True,
|
|
51
|
+
help="Destination directory for --plugin-template.",
|
|
52
|
+
)
|
|
53
|
+
@click.option(
|
|
54
|
+
"--list-plugin-templates",
|
|
55
|
+
is_flag=True,
|
|
56
|
+
help="List built-in parser plugin templates and exit.",
|
|
57
|
+
)
|
|
58
|
+
def import_cmd(
|
|
59
|
+
source,
|
|
60
|
+
output,
|
|
61
|
+
target_host,
|
|
62
|
+
target_strategy,
|
|
63
|
+
dry_run,
|
|
64
|
+
out_format,
|
|
65
|
+
parser,
|
|
66
|
+
plugin_template,
|
|
67
|
+
plugin_dir,
|
|
68
|
+
list_plugin_templates,
|
|
69
|
+
):
|
|
40
70
|
"""Parse an IaC/CI-CD file and produce a migration.yaml scaffold.
|
|
41
71
|
|
|
42
72
|
Auto-detects format from filename. Built-in parsers cover:
|
|
@@ -55,12 +85,34 @@ def import_cmd(source, output, target_host, target_strategy, dry_run, out_format
|
|
|
55
85
|
redeploy import docker-compose.yml --target-host root@vps.example.com
|
|
56
86
|
redeploy import . --dry-run # parse whole directory
|
|
57
87
|
redeploy import docker-compose.yml --format summary
|
|
88
|
+
redeploy import --plugin-template helm-kustomize
|
|
58
89
|
"""
|
|
59
90
|
import json as _json
|
|
60
91
|
from ...iac import parse_file, parse_dir, parser_registry
|
|
61
92
|
|
|
62
93
|
console = Console()
|
|
94
|
+
|
|
95
|
+
if list_plugin_templates:
|
|
96
|
+
_print_plugin_templates(console)
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
if plugin_template:
|
|
100
|
+
_copy_plugin_template(
|
|
101
|
+
console,
|
|
102
|
+
plugin_template=plugin_template,
|
|
103
|
+
plugin_dir=Path(plugin_dir),
|
|
104
|
+
dry_run=dry_run,
|
|
105
|
+
)
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
if not source:
|
|
109
|
+
console.print("[red]✗ SOURCE is required unless --plugin-template is used[/red]")
|
|
110
|
+
sys.exit(1)
|
|
111
|
+
|
|
63
112
|
src_path = Path(source)
|
|
113
|
+
if not src_path.exists():
|
|
114
|
+
console.print(f"[red]✗ source does not exist: {source}[/red]")
|
|
115
|
+
sys.exit(1)
|
|
64
116
|
|
|
65
117
|
# Parse
|
|
66
118
|
if parser:
|
|
@@ -190,6 +242,55 @@ def _default_output(src: Path, spec) -> Path:
|
|
|
190
242
|
return src.parent / f"{stem}.yaml"
|
|
191
243
|
|
|
192
244
|
|
|
245
|
+
def _plugin_templates() -> dict[str, str]:
|
|
246
|
+
return {
|
|
247
|
+
"helm-ansible": "helm_ansible.py",
|
|
248
|
+
"helm-kustomize": "helm_kustomize.py",
|
|
249
|
+
"argocd-flux": "argocd_flux.py",
|
|
250
|
+
"gitops-ci": "gitops_ci.py",
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _examples_plugin_dir() -> Path:
|
|
255
|
+
return Path(__file__).resolve().parents[3] / "examples" / "redeploy_iac_parsers"
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _print_plugin_templates(console: Console) -> None:
|
|
259
|
+
console.print("[bold]Available parser plugin templates[/bold]")
|
|
260
|
+
for name, filename in _plugin_templates().items():
|
|
261
|
+
console.print(f" - [cyan]{name}[/cyan] ({filename})")
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _copy_plugin_template(
|
|
265
|
+
console: Console,
|
|
266
|
+
*,
|
|
267
|
+
plugin_template: str,
|
|
268
|
+
plugin_dir: Path,
|
|
269
|
+
dry_run: bool,
|
|
270
|
+
) -> Path:
|
|
271
|
+
templates = _plugin_templates()
|
|
272
|
+
filename = templates[plugin_template]
|
|
273
|
+
src = _examples_plugin_dir() / filename
|
|
274
|
+
dst = plugin_dir / filename
|
|
275
|
+
|
|
276
|
+
if not src.exists():
|
|
277
|
+
console.print(f"[red]✗ built-in plugin template not found: {src}[/red]")
|
|
278
|
+
sys.exit(1)
|
|
279
|
+
|
|
280
|
+
console.print(f"[bold]plugin template[/bold] {plugin_template}")
|
|
281
|
+
console.print(f" source: {src}")
|
|
282
|
+
console.print(f" destination: {dst}")
|
|
283
|
+
|
|
284
|
+
if dry_run:
|
|
285
|
+
console.print("\n[dim][DRY RUN] No file written.[/dim]")
|
|
286
|
+
return dst
|
|
287
|
+
|
|
288
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
289
|
+
shutil.copy2(src, dst)
|
|
290
|
+
console.print(f"\n [green]✓[/green] copied → [bold]{dst}[/bold]")
|
|
291
|
+
return dst
|
|
292
|
+
|
|
293
|
+
|
|
193
294
|
def _spec_to_migration_yaml(
|
|
194
295
|
spec, *, target_host: str = None, target_strategy: str = None
|
|
195
296
|
) -> dict:
|
|
@@ -35,6 +35,7 @@ You will receive a JSON schema describing the current workspace with:
|
|
|
35
35
|
- git_branch : current git branch
|
|
36
36
|
- specs : list of discovered migration specs (path, version, target, name)
|
|
37
37
|
- commands : catalogue of available redeploy commands with descriptions and examples
|
|
38
|
+
- iac : parser/plugin-template info for IaC/CI-CD imports
|
|
38
39
|
|
|
39
40
|
Your task: map the user's instruction to a single redeploy command.
|
|
40
41
|
|
|
@@ -49,9 +50,23 @@ Output ONLY valid JSON (no markdown fences, no extra text) with this structure:
|
|
|
49
50
|
Rules:
|
|
50
51
|
- argv[0] MUST be "redeploy".
|
|
51
52
|
- Use relative paths for specs (relative to cwd).
|
|
53
|
+
- If the user asks about Dockerfile, nginx, Kubernetes, Terraform, TOML, Vite, GitHub Actions, GitLab CI, Jenkinsfile, Helm, Kustomize, ArgoCD, Flux, or GitOps configs, prefer `redeploy import ...`.
|
|
54
|
+
- If the user asks to add support for a new IaC/CI/CD format and a matching template exists in schema["iac"]["plugin_templates"], prefer `redeploy import --plugin-template <name>`.
|
|
55
|
+
- If the user asks what plugin templates or parsers are available, prefer `redeploy import --list-plugin-templates`.
|
|
52
56
|
- If the user's intent is unclear, pick the safest command (e.g. "plan" with --dry-run=true).
|
|
53
57
|
- Set confirm=false ONLY for read-only/safe commands (plan, inspect, status, audit, diff, diagnose).
|
|
54
|
-
- Set confirm=
|
|
58
|
+
- Set confirm=false for `import --list-plugin-templates` and `import --dry-run`.
|
|
59
|
+
- Set confirm=true for `import --plugin-template ...` because it writes files.
|
|
60
|
+
- Set confirm=false for "run --dry-run" (previewing a spec is safe, no changes made).
|
|
61
|
+
- Set confirm=true for "fix" commands (even with --dry-run flag - repair operations need confirmation for safety).
|
|
62
|
+
- Set confirm=true for "run" without --dry-run (actual deployment changes state).
|
|
63
|
+
- Set confirm=true for "bump", "push", "init" (these change project state).
|
|
64
|
+
- Examples:
|
|
65
|
+
* "pokaż plan" → argv=["redeploy", "run", "spec.md", "--dry-run"], confirm=false
|
|
66
|
+
* "deploy to pi109" → argv=["redeploy", "run", "spec.md"], confirm=true
|
|
67
|
+
* "napraw kiosk" → argv=["redeploy", "fix", "spec.md", "--dry-run"], confirm=true
|
|
68
|
+
* "dodaj parser helm + kustomize" → argv=["redeploy", "import", "--plugin-template", "helm-kustomize"], confirm=true
|
|
69
|
+
* "jakie plugin templates są dostępne" → argv=["redeploy", "import", "--list-plugin-templates"], confirm=false
|
|
55
70
|
- If the user explicitly says "dry run" / "pokaż" / "sprawdź" / "plan", prefer --dry-run.
|
|
56
71
|
- Never invent spec paths — use only paths from the schema["specs"] list.
|
|
57
72
|
- If no matching spec exists, return argv=["redeploy", "plan", "--help"] and explain in human_summary.
|
|
@@ -102,7 +117,7 @@ def _call_llm(schema: dict, user_prompt: str) -> str:
|
|
|
102
117
|
|
|
103
118
|
|
|
104
119
|
def _parse_llm_response(raw: str) -> dict:
|
|
105
|
-
"""Parse JSON from LLM response, strip accidental fences."""
|
|
120
|
+
"""Parse JSON from LLM response, strip accidental fences and escape control characters."""
|
|
106
121
|
# Strip ```json ... ``` if present
|
|
107
122
|
clean = raw.strip()
|
|
108
123
|
if clean.startswith("```"):
|
|
@@ -111,6 +126,17 @@ def _parse_llm_response(raw: str) -> dict:
|
|
|
111
126
|
line for line in lines
|
|
112
127
|
if not line.strip().startswith("```")
|
|
113
128
|
)
|
|
129
|
+
# Escape control characters (except \n, \r, \t) for JSON safety
|
|
130
|
+
def escape_control_char(match):
|
|
131
|
+
char = match.group(0)
|
|
132
|
+
# Keep allowed whitespace, escape others as \uXXXX
|
|
133
|
+
if char in "\n\r\t":
|
|
134
|
+
return char
|
|
135
|
+
return f"\\u{ord(char):04x}"
|
|
136
|
+
|
|
137
|
+
# Match any control character (ord < 32) except \n, \r, \t
|
|
138
|
+
import re
|
|
139
|
+
clean = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f]", escape_control_char, clean)
|
|
114
140
|
return json.loads(clean)
|
|
115
141
|
|
|
116
142
|
|
|
@@ -19,6 +19,7 @@ import os
|
|
|
19
19
|
import re
|
|
20
20
|
import textwrap
|
|
21
21
|
import datetime
|
|
22
|
+
from collections import defaultdict
|
|
22
23
|
from pathlib import Path
|
|
23
24
|
from typing import Optional
|
|
24
25
|
|
|
@@ -248,6 +249,33 @@ def parse_failed_step(executor_summary: str, executor=None) -> tuple[str | None,
|
|
|
248
249
|
return None, ""
|
|
249
250
|
|
|
250
251
|
|
|
252
|
+
class HealLoopDetector:
|
|
253
|
+
"""Detect repeated non-converging heal hints for a given step."""
|
|
254
|
+
|
|
255
|
+
def __init__(self, max_identical_hints: int = 3):
|
|
256
|
+
self.max_identical_hints = max_identical_hints
|
|
257
|
+
self._history: dict[str, list[str]] = defaultdict(list)
|
|
258
|
+
|
|
259
|
+
def observe(self, step_id: str, hint: str) -> bool:
|
|
260
|
+
"""Return True when the latest hints indicate a heal loop.
|
|
261
|
+
|
|
262
|
+
A loop is defined as ``max_identical_hints`` identical non-empty hints
|
|
263
|
+
in a row for the same failed step.
|
|
264
|
+
"""
|
|
265
|
+
normalized = (hint or "").strip()
|
|
266
|
+
if not normalized:
|
|
267
|
+
return False
|
|
268
|
+
|
|
269
|
+
history = self._history[step_id]
|
|
270
|
+
history.append(normalized)
|
|
271
|
+
|
|
272
|
+
if len(history) < self.max_identical_hints:
|
|
273
|
+
return False
|
|
274
|
+
|
|
275
|
+
recent = history[-self.max_identical_hints :]
|
|
276
|
+
return all(item == recent[0] for item in recent)
|
|
277
|
+
|
|
278
|
+
|
|
251
279
|
class HealRunner:
|
|
252
280
|
"""
|
|
253
281
|
Wraps Executor with self-healing loop.
|
|
@@ -296,6 +324,7 @@ class HealRunner:
|
|
|
296
324
|
self._state_file = executor_kwargs.pop("state_file", None)
|
|
297
325
|
self._no_state = executor_kwargs.pop("no_state", False)
|
|
298
326
|
self.repairs: list[dict] = []
|
|
327
|
+
self._loop_detector = HealLoopDetector(max_identical_hints=3)
|
|
299
328
|
|
|
300
329
|
from rich.console import Console
|
|
301
330
|
self.console = console or Console()
|
|
@@ -402,6 +431,14 @@ class HealRunner:
|
|
|
402
431
|
})
|
|
403
432
|
write_repair_log(self.spec_path, self.version, self.repairs)
|
|
404
433
|
|
|
434
|
+
loop_hint = f"{summary} | {diag_hint}".strip()
|
|
435
|
+
if self._loop_detector.observe(failed_step, loop_hint):
|
|
436
|
+
self.console.print(
|
|
437
|
+
"[yellow]heal: detected repeating non-converging hint pattern; "
|
|
438
|
+
"stopping auto-retry to avoid loop[/yellow]"
|
|
439
|
+
)
|
|
440
|
+
break
|
|
441
|
+
|
|
405
442
|
# Retry
|
|
406
443
|
executor = self._make_executor(resume=True)
|
|
407
444
|
ok = executor.run()
|