remo-cli 1.0.2__tar.gz → 2.0.0rc2__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.
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/PKG-INFO +1 -1
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/pyproject.toml +1 -1
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/src/remo_cli/cli/providers/incus.py +14 -2
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/src/remo_cli/cli/providers/proxmox.py +14 -2
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/src/remo_cli/cli/shell.py +40 -13
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/src/remo_cli/core/ssh.py +33 -11
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/src/remo_cli/providers/incus.py +15 -3
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/src/remo_cli/providers/proxmox.py +17 -6
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/tests/unit/cli/test_shell.py +58 -5
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/tests/unit/core/test_ssh.py +44 -18
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/uv.lock +1 -1
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/.claude/commands/speckit.analyze.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/.claude/commands/speckit.checklist.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/.claude/commands/speckit.clarify.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/.claude/commands/speckit.constitution.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/.claude/commands/speckit.implement.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/.claude/commands/speckit.plan.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/.claude/commands/speckit.specify.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/.claude/commands/speckit.tasks.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/.claude/commands/speckit.taskstoissues.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/.devcontainer/devcontainer.json +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/.dockerignore +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/.github/dependabot.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/.github/workflows/ci.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/.github/workflows/provision.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/.github/workflows/release.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/.github/workflows/smoke-test-cleanup.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/.github/workflows/smoke-test.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/.github/workflows/teardown.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/.gitignore +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/.maverick/checkpoints/feature/implementation_complete.json +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/.maverick/checkpoints/feature/phase_${{ index }}_complete.json +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/.maverick/checkpoints/feature/validation_complete.json +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/.specify/memory/constitution.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/.specify/scripts/bash/check-prerequisites.sh +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/.specify/scripts/bash/common.sh +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/.specify/scripts/bash/create-new-feature.sh +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/.specify/scripts/bash/setup-plan.sh +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/.specify/scripts/bash/update-agent-context.sh +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/.specify/templates/agent-file-template.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/.specify/templates/checklist-template.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/.specify/templates/plan-template.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/.specify/templates/spec-template.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/.specify/templates/tasks-template.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/CLAUDE.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/CONTRIBUTING.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/Dockerfile +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/LICENSE +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/README.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/.ansible-lint +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/README.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/ansible.cfg +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/aws_configure.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/aws_resize.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/aws_site.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/aws_teardown.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/group_vars/all.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/group_vars/incus_containers.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/group_vars/proxmox_containers.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/hetzner_configure.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/hetzner_provision.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/hetzner_resize.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/hetzner_site.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/hetzner_teardown.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/incus_bootstrap.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/incus_configure.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/incus_provision.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/incus_resize.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/incus_site.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/incus_teardown.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/inventory/hosts.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/inventory/incus_containers.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/proxmox_bootstrap.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/proxmox_configure.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/proxmox_provision.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/proxmox_resize.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/proxmox_site.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/proxmox_teardown.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/requirements.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/aws_server/defaults/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/aws_server/tasks/ebs.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/aws_server/tasks/ec2.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/aws_server/tasks/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/aws_server/tasks/patch_manager.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/aws_server/tasks/preflight.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/aws_server/tasks/security_group.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/devcontainers/tasks/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/docker/handlers/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/docker/tasks/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/fzf/tasks/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/github_cli/tasks/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/hetzner_server/defaults/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/hetzner_server/tasks/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/incus_bootstrap/defaults/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/incus_bootstrap/handlers/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/incus_bootstrap/tasks/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/incus_container/defaults/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/incus_container/handlers/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/incus_container/meta/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/incus_container/tasks/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/incus_container/tasks/preflight.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/incus_container_teardown/defaults/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/incus_container_teardown/meta/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/incus_container_teardown/tasks/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/nodejs/defaults/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/nodejs/tasks/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/proxmox_bootstrap/defaults/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/proxmox_bootstrap/meta/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/proxmox_bootstrap/tasks/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/proxmox_container/defaults/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/proxmox_container/handlers/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/proxmox_container/meta/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/proxmox_container/tasks/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/proxmox_container/tasks/preflight.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/proxmox_container_teardown/defaults/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/proxmox_container_teardown/meta/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/proxmox_container_teardown/tasks/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/user_setup/defaults/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/user_setup/tasks/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/user_setup/templates/devshell.sh.j2 +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/user_setup/templates/project-menu.sh.j2 +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/zellij/defaults/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/zellij/tasks/main.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/zellij/templates/config.kdl.j2 +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/roles/zellij/templates/layouts/minimal.kdl.j2 +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/ansible/tasks/configure_dev_tools.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/docs/aws.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/docs/hetzner.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/docs/incus.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/docs/install.sh +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/docs/proxmox.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/install.sh +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/maverick.yaml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/project-menu.sh +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/remo +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/run.sh +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/scripts/setup-aws-oidc.sh +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/specs/001-bootstrap-incus-host/checklists/requirements.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/specs/001-bootstrap-incus-host/checklists/validation.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/specs/001-bootstrap-incus-host/contracts/playbook-interface.yaml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/specs/001-bootstrap-incus-host/contracts/role-interface.yaml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/specs/001-bootstrap-incus-host/data-model.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/specs/001-bootstrap-incus-host/plan.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/specs/001-bootstrap-incus-host/quickstart.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/specs/001-bootstrap-incus-host/research.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/specs/001-bootstrap-incus-host/spec.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/specs/001-bootstrap-incus-host/tasks.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/specs/002-incus-container-support/checklists/requirements.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/specs/002-incus-container-support/contracts/playbook-interface.yaml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/specs/002-incus-container-support/contracts/role-interface.yaml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/specs/002-incus-container-support/data-model.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/specs/002-incus-container-support/plan.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/specs/002-incus-container-support/quickstart.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/specs/002-incus-container-support/research.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/specs/002-incus-container-support/spec.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/specs/002-incus-container-support/tasks.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/specs/003-python-cli-rewrite/checklists/requirements.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/specs/003-python-cli-rewrite/data-model.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/specs/003-python-cli-rewrite/plan.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/specs/003-python-cli-rewrite/quickstart.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/specs/003-python-cli-rewrite/research.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/specs/003-python-cli-rewrite/spec.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/specs/003-python-cli-rewrite/tasks.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/specs/004-proxmox-container-support/spec.md +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/src/remo_cli/__init__.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/src/remo_cli/__main__.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/src/remo_cli/cli/__init__.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/src/remo_cli/cli/cp.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/src/remo_cli/cli/main.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/src/remo_cli/cli/providers/__init__.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/src/remo_cli/cli/providers/aws.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/src/remo_cli/cli/providers/hetzner.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/src/remo_cli/core/__init__.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/src/remo_cli/core/ansible_runner.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/src/remo_cli/core/completion.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/src/remo_cli/core/config.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/src/remo_cli/core/known_hosts.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/src/remo_cli/core/output.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/src/remo_cli/core/picker.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/src/remo_cli/core/rsync.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/src/remo_cli/core/validation.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/src/remo_cli/core/version.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/src/remo_cli/models/__init__.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/src/remo_cli/models/host.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/src/remo_cli/providers/__init__.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/src/remo_cli/providers/aws.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/src/remo_cli/providers/hetzner.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/tests/__init__.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/tests/conftest.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/tests/integration/__init__.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/tests/integration/orbstack-cloud-init.yml +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/tests/integration/orbstack.sh +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/tests/unit/__init__.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/tests/unit/cli/__init__.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/tests/unit/cli/test_cp.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/tests/unit/cli/test_main.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/tests/unit/core/__init__.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/tests/unit/core/test_ansible_runner.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/tests/unit/core/test_config.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/tests/unit/core/test_known_hosts.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/tests/unit/core/test_output.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/tests/unit/core/test_picker.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/tests/unit/core/test_rsync.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/tests/unit/core/test_validation.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/tests/unit/core/test_version.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/tests/unit/providers/__init__.py +0 -0
- {remo_cli-1.0.2 → remo_cli-2.0.0rc2}/tests/unit/test_host_model.py +0 -0
|
@@ -40,6 +40,11 @@ def incus() -> None:
|
|
|
40
40
|
)
|
|
41
41
|
@click.option("--only", multiple=True, help="Only install these tools.")
|
|
42
42
|
@click.option("--skip", multiple=True, help="Skip these tools.")
|
|
43
|
+
@click.option(
|
|
44
|
+
"--use-ip",
|
|
45
|
+
is_flag=True,
|
|
46
|
+
help="Store the container's IP address in known_hosts instead of its name (for setups without DNS/MagicDNS).",
|
|
47
|
+
)
|
|
43
48
|
@click.option("--yes", "-y", is_flag=True, help="Auto-confirm prompts.")
|
|
44
49
|
@click.option("-v", "--verbose", is_flag=True, help="Verbose output.")
|
|
45
50
|
def create(
|
|
@@ -53,6 +58,7 @@ def create(
|
|
|
53
58
|
memory: int,
|
|
54
59
|
only: tuple[str, ...],
|
|
55
60
|
skip: tuple[str, ...],
|
|
61
|
+
use_ip: bool,
|
|
56
62
|
yes: bool,
|
|
57
63
|
verbose: bool,
|
|
58
64
|
) -> None:
|
|
@@ -68,6 +74,7 @@ def create(
|
|
|
68
74
|
memory=memory,
|
|
69
75
|
tools_only=only,
|
|
70
76
|
tools_skip=skip,
|
|
77
|
+
use_ip=use_ip,
|
|
71
78
|
verbose=verbose,
|
|
72
79
|
)
|
|
73
80
|
sys.exit(rc)
|
|
@@ -173,9 +180,14 @@ def info(name: str, host: str, user: str) -> None:
|
|
|
173
180
|
@incus.command()
|
|
174
181
|
@click.option("--host", default="localhost", help="Incus host (default: localhost).")
|
|
175
182
|
@click.option("--user", default="", help="SSH user for remote Incus host.")
|
|
176
|
-
|
|
183
|
+
@click.option(
|
|
184
|
+
"--use-ip",
|
|
185
|
+
is_flag=True,
|
|
186
|
+
help="Store each container's IP address in known_hosts instead of its name (for setups without DNS/MagicDNS).",
|
|
187
|
+
)
|
|
188
|
+
def sync(host: str, user: str, use_ip: bool) -> None:
|
|
177
189
|
"""Discover containers from an Incus host."""
|
|
178
|
-
providers_incus.sync(host=host, user=user)
|
|
190
|
+
providers_incus.sync(host=host, user=user, use_ip=use_ip)
|
|
179
191
|
|
|
180
192
|
|
|
181
193
|
@incus.command()
|
|
@@ -38,6 +38,11 @@ def proxmox() -> None:
|
|
|
38
38
|
@click.option("--domain", default="", help="Domain name for the container.")
|
|
39
39
|
@click.option("--only", multiple=True, help="Only install these tools.")
|
|
40
40
|
@click.option("--skip", multiple=True, help="Skip these tools.")
|
|
41
|
+
@click.option(
|
|
42
|
+
"--use-ip",
|
|
43
|
+
is_flag=True,
|
|
44
|
+
help="Store the container's IP address in known_hosts instead of its name (for setups without DNS/MagicDNS).",
|
|
45
|
+
)
|
|
41
46
|
@click.option("--yes", "-y", is_flag=True, help="Auto-confirm prompts.")
|
|
42
47
|
@click.option("-v", "--verbose", is_flag=True, help="Verbose output.")
|
|
43
48
|
def create(
|
|
@@ -55,6 +60,7 @@ def create(
|
|
|
55
60
|
domain: str,
|
|
56
61
|
only: tuple[str, ...],
|
|
57
62
|
skip: tuple[str, ...],
|
|
63
|
+
use_ip: bool,
|
|
58
64
|
yes: bool,
|
|
59
65
|
verbose: bool,
|
|
60
66
|
) -> None:
|
|
@@ -74,6 +80,7 @@ def create(
|
|
|
74
80
|
domain=domain,
|
|
75
81
|
tools_only=only,
|
|
76
82
|
tools_skip=skip,
|
|
83
|
+
use_ip=use_ip,
|
|
77
84
|
verbose=verbose,
|
|
78
85
|
)
|
|
79
86
|
sys.exit(rc)
|
|
@@ -179,9 +186,14 @@ def info(name: str, host: str, user: str) -> None:
|
|
|
179
186
|
@proxmox.command()
|
|
180
187
|
@click.option("--host", required=True, help="Proxmox host to scan.")
|
|
181
188
|
@click.option("--user", default="", help="SSH user for the Proxmox host.")
|
|
182
|
-
|
|
189
|
+
@click.option(
|
|
190
|
+
"--use-ip",
|
|
191
|
+
is_flag=True,
|
|
192
|
+
help="Store each container's IP address in known_hosts instead of its name (for setups without DNS/MagicDNS).",
|
|
193
|
+
)
|
|
194
|
+
def sync(host: str, user: str, use_ip: bool) -> None:
|
|
183
195
|
"""Discover containers from a Proxmox host."""
|
|
184
|
-
providers_proxmox.sync(host=host, user=user)
|
|
196
|
+
providers_proxmox.sync(host=host, user=user, use_ip=use_ip)
|
|
185
197
|
|
|
186
198
|
|
|
187
199
|
@proxmox.command()
|
|
@@ -33,7 +33,7 @@ def shell(
|
|
|
33
33
|
) -> None:
|
|
34
34
|
"""Connect to a remo environment (auto-detects or picker)."""
|
|
35
35
|
from remo_cli.core.ssh import check_remote_version, resolve_remo_host, shell_connect # noqa: PLC0415
|
|
36
|
-
from remo_cli.core.output import confirm, print_warning # noqa: PLC0415
|
|
36
|
+
from remo_cli.core.output import confirm, print_error, print_warning # noqa: PLC0415
|
|
37
37
|
from remo_cli.core.version import get_current_version, version_is_newer # noqa: PLC0415
|
|
38
38
|
from remo_cli.providers.aws import auto_start_aws_if_stopped # noqa: PLC0415
|
|
39
39
|
|
|
@@ -46,10 +46,20 @@ def shell(
|
|
|
46
46
|
if not no_update_check:
|
|
47
47
|
local_version = get_current_version()
|
|
48
48
|
if local_version != "unknown":
|
|
49
|
-
remote_version = check_remote_version(host)
|
|
49
|
+
remote_version, remote_err = check_remote_version(host)
|
|
50
50
|
|
|
51
51
|
should_update = False
|
|
52
|
-
if
|
|
52
|
+
if remote_err is not None:
|
|
53
|
+
# SSH itself failed — we can't tell whether the marker exists
|
|
54
|
+
# or what version is on the box. Don't prompt to update from
|
|
55
|
+
# an unknown baseline; surface the error so the user can fix
|
|
56
|
+
# it (DNS, host key, auth, ...) and re-run.
|
|
57
|
+
print_warning(
|
|
58
|
+
f"Could not check tools version on '{host.name}':\n"
|
|
59
|
+
f" {remote_err}\n"
|
|
60
|
+
f" Skipping update check; proceeding with connection."
|
|
61
|
+
)
|
|
62
|
+
elif remote_version is None:
|
|
53
63
|
# No marker file on remote
|
|
54
64
|
should_update = confirm(
|
|
55
65
|
f"Instance '{host.name}' has no version info. Update tools?",
|
|
@@ -71,29 +81,45 @@ def shell(
|
|
|
71
81
|
)
|
|
72
82
|
|
|
73
83
|
if should_update:
|
|
74
|
-
_run_provider_update(host)
|
|
84
|
+
rc = _run_provider_update(host)
|
|
85
|
+
if rc != 0:
|
|
86
|
+
# The playbook log has already been dumped, but the SSH
|
|
87
|
+
# connection (and any remote project picker) would scroll
|
|
88
|
+
# it offscreen immediately. Pause so the user can read it.
|
|
89
|
+
print_error(
|
|
90
|
+
f"Tools update for '{host.name}' failed "
|
|
91
|
+
f"(ansible-playbook exit code {rc})."
|
|
92
|
+
)
|
|
93
|
+
if not confirm(
|
|
94
|
+
"Connect anyway?",
|
|
95
|
+
default=False,
|
|
96
|
+
):
|
|
97
|
+
raise SystemExit(rc)
|
|
75
98
|
|
|
76
99
|
shell_connect(host, list(tunnels), no_open)
|
|
77
100
|
|
|
78
101
|
|
|
79
|
-
def _run_provider_update(host) ->
|
|
80
|
-
"""Run the appropriate provider update for the given host.
|
|
102
|
+
def _run_provider_update(host) -> int: # noqa: ANN001
|
|
103
|
+
"""Run the appropriate provider update for the given host.
|
|
104
|
+
|
|
105
|
+
Returns the provider update's exit code (0 on success).
|
|
106
|
+
"""
|
|
81
107
|
from remo_cli.core.output import print_info # noqa: PLC0415
|
|
82
108
|
|
|
83
109
|
print_info(f"Updating instance '{host.name}'...")
|
|
84
110
|
|
|
85
111
|
if host.type == "aws":
|
|
86
112
|
from remo_cli.providers.aws import update as aws_update # noqa: PLC0415
|
|
87
|
-
aws_update(name=host.name)
|
|
88
|
-
|
|
113
|
+
return aws_update(name=host.name)
|
|
114
|
+
if host.type == "hetzner":
|
|
89
115
|
from remo_cli.providers.hetzner import update as hetzner_update # noqa: PLC0415
|
|
90
|
-
hetzner_update(name=host.name)
|
|
91
|
-
|
|
116
|
+
return hetzner_update(name=host.name)
|
|
117
|
+
if host.type == "incus":
|
|
92
118
|
from remo_cli.providers.incus import update as incus_update # noqa: PLC0415
|
|
93
119
|
# Incus name in known_hosts is "host/container" — extract just the container name
|
|
94
120
|
container_name = host.name.split("/", maxsplit=1)[-1] if "/" in host.name else host.name
|
|
95
|
-
incus_update(name=container_name)
|
|
96
|
-
|
|
121
|
+
return incus_update(name=container_name)
|
|
122
|
+
if host.type == "proxmox":
|
|
97
123
|
from remo_cli.providers.proxmox import update as proxmox_update # noqa: PLC0415
|
|
98
124
|
# Proxmox name in known_hosts is "node/container".
|
|
99
125
|
# The proxmox SSH user is stored in the region slot (see providers.proxmox.create).
|
|
@@ -101,8 +127,9 @@ def _run_provider_update(host) -> None: # noqa: ANN001
|
|
|
101
127
|
if not container_name:
|
|
102
128
|
container_name = host.name
|
|
103
129
|
proxmox_host = ""
|
|
104
|
-
proxmox_update(
|
|
130
|
+
return proxmox_update(
|
|
105
131
|
name=container_name,
|
|
106
132
|
host=proxmox_host,
|
|
107
133
|
user=host.region or "",
|
|
108
134
|
)
|
|
135
|
+
return 0
|
|
@@ -257,24 +257,46 @@ def detect_timezone() -> str:
|
|
|
257
257
|
return ""
|
|
258
258
|
|
|
259
259
|
|
|
260
|
-
def check_remote_version(host: KnownHost) -> str | None:
|
|
260
|
+
def check_remote_version(host: KnownHost) -> tuple[str | None, str | None]:
|
|
261
261
|
"""Read the remo version marker from the remote instance.
|
|
262
262
|
|
|
263
|
-
Runs ``cat ~/.remo-version`` over SSH
|
|
264
|
-
|
|
263
|
+
Runs ``cat ~/.remo-version`` over SSH. Returns a ``(version, error)``
|
|
264
|
+
tuple where exactly one element is non-None:
|
|
265
|
+
|
|
266
|
+
* ``(version, None)`` — marker present and readable.
|
|
267
|
+
* ``(None, None)`` — SSH succeeded but the marker is missing/empty.
|
|
268
|
+
* ``(None, error)`` — SSH itself failed (DNS, host key, network,
|
|
269
|
+
timeout, ...); *error* is the underlying message so the caller can
|
|
270
|
+
surface it to the user.
|
|
265
271
|
"""
|
|
266
272
|
ssh_opts, ssh_target = build_ssh_opts(host)
|
|
267
|
-
cmd = ["ssh"] + ssh_opts + [
|
|
273
|
+
cmd = ["ssh"] + ssh_opts + [
|
|
274
|
+
"-o", "ConnectTimeout=10",
|
|
275
|
+
"-o", "BatchMode=yes",
|
|
276
|
+
ssh_target,
|
|
277
|
+
"cat ~/.remo-version 2>/dev/null",
|
|
278
|
+
]
|
|
268
279
|
|
|
269
280
|
try:
|
|
270
281
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
282
|
+
except subprocess.TimeoutExpired:
|
|
283
|
+
return None, "SSH timed out after 15s"
|
|
284
|
+
except OSError as e:
|
|
285
|
+
return None, f"SSH failed: {e}"
|
|
286
|
+
|
|
287
|
+
# ssh exits with 255 for its own failures (auth, host key, DNS,
|
|
288
|
+
# connection refused, ...). Any other non-zero exit code comes from
|
|
289
|
+
# the remote command — here, `cat` failing because the marker is
|
|
290
|
+
# absent — which we treat as "no marker" rather than an SSH error.
|
|
291
|
+
if result.returncode == 255:
|
|
292
|
+
stderr = result.stderr.strip()
|
|
293
|
+
return None, stderr or "SSH connection failed (exit code 255)"
|
|
294
|
+
|
|
295
|
+
version = result.stdout.strip()
|
|
296
|
+
if result.returncode == 0 and version:
|
|
297
|
+
return version, None
|
|
298
|
+
|
|
299
|
+
return None, None
|
|
278
300
|
|
|
279
301
|
|
|
280
302
|
def shell_connect(host: KnownHost, tunnels: list[str], no_open: bool) -> None:
|
|
@@ -165,6 +165,7 @@ def create(
|
|
|
165
165
|
memory: int = 0,
|
|
166
166
|
tools_only: tuple[str, ...] = (),
|
|
167
167
|
tools_skip: tuple[str, ...] = (),
|
|
168
|
+
use_ip: bool = False,
|
|
168
169
|
verbose: bool = False,
|
|
169
170
|
) -> int:
|
|
170
171
|
"""Create a new Incus container and configure it with dev tools.
|
|
@@ -206,7 +207,10 @@ def create(
|
|
|
206
207
|
rc = run_playbook("incus_site.yml", extra_vars, verbose=verbose)
|
|
207
208
|
|
|
208
209
|
if rc == 0:
|
|
209
|
-
|
|
210
|
+
if use_ip:
|
|
211
|
+
container_host = _resolve_container_ip(name, host, user) or name
|
|
212
|
+
else:
|
|
213
|
+
container_host = name
|
|
210
214
|
save_known_host(
|
|
211
215
|
KnownHost(
|
|
212
216
|
type="incus",
|
|
@@ -483,13 +487,17 @@ def info(name: str, host: str = "", user: str = "") -> int:
|
|
|
483
487
|
return 0
|
|
484
488
|
|
|
485
489
|
|
|
486
|
-
def sync(host: str = "localhost", user: str = "") -> None:
|
|
490
|
+
def sync(host: str = "localhost", user: str = "", use_ip: bool = False) -> None:
|
|
487
491
|
"""Discover Incus containers on *host* and register them in known-hosts.
|
|
488
492
|
|
|
489
493
|
For localhost, runs ``incus list -f csv -c n`` directly. For remote hosts,
|
|
490
494
|
the same command is executed over SSH. All previously registered entries
|
|
491
495
|
for the given host prefix are cleared before the newly discovered
|
|
492
496
|
containers are saved.
|
|
497
|
+
|
|
498
|
+
When *use_ip* is true, each container's eth0 IP is resolved and stored as
|
|
499
|
+
the ``host`` field; otherwise the container name itself is stored (and
|
|
500
|
+
relies on DNS/MagicDNS for resolution at connect time).
|
|
493
501
|
"""
|
|
494
502
|
if host == "localhost":
|
|
495
503
|
result = subprocess.run(
|
|
@@ -523,11 +531,15 @@ def sync(host: str = "localhost", user: str = "") -> None:
|
|
|
523
531
|
clear_known_hosts_by_prefix("incus", f"{host}/")
|
|
524
532
|
|
|
525
533
|
for name in containers:
|
|
534
|
+
if use_ip:
|
|
535
|
+
container_host = _resolve_container_ip(name, host, user) or name
|
|
536
|
+
else:
|
|
537
|
+
container_host = name
|
|
526
538
|
save_known_host(
|
|
527
539
|
KnownHost(
|
|
528
540
|
type="incus",
|
|
529
541
|
name=f"{host}/{name}",
|
|
530
|
-
host=
|
|
542
|
+
host=container_host,
|
|
531
543
|
user="remo",
|
|
532
544
|
instance_id=user,
|
|
533
545
|
access_mode="direct",
|
|
@@ -180,6 +180,7 @@ def create(
|
|
|
180
180
|
domain: str = "",
|
|
181
181
|
tools_only: tuple[str, ...] = (),
|
|
182
182
|
tools_skip: tuple[str, ...] = (),
|
|
183
|
+
use_ip: bool = False,
|
|
183
184
|
verbose: bool = False,
|
|
184
185
|
) -> int:
|
|
185
186
|
"""Create a new Proxmox LXC container and configure dev tools.
|
|
@@ -241,12 +242,15 @@ def create(
|
|
|
241
242
|
|
|
242
243
|
if rc == 0:
|
|
243
244
|
vmid = _resolve_vmid(name, host, user)
|
|
244
|
-
|
|
245
|
+
if use_ip:
|
|
246
|
+
container_host = _resolve_container_ip(name, host, user, vmid=vmid) or name
|
|
247
|
+
else:
|
|
248
|
+
container_host = name
|
|
245
249
|
save_known_host(
|
|
246
250
|
KnownHost(
|
|
247
251
|
type="proxmox",
|
|
248
252
|
name=f"{host}/{name}",
|
|
249
|
-
host=
|
|
253
|
+
host=container_host,
|
|
250
254
|
user="remo",
|
|
251
255
|
instance_id=vmid,
|
|
252
256
|
access_mode="direct",
|
|
@@ -541,11 +545,15 @@ def _parse_pct_config_field(config_text: str, field: str) -> str:
|
|
|
541
545
|
return match.group(1).strip() if match else ""
|
|
542
546
|
|
|
543
547
|
|
|
544
|
-
def sync(host: str, user: str = "") -> None:
|
|
548
|
+
def sync(host: str, user: str = "", use_ip: bool = False) -> None:
|
|
545
549
|
"""Discover Proxmox LXC containers on *host* and register them.
|
|
546
550
|
|
|
547
551
|
Runs ``pct list`` over SSH (or locally if host == "localhost"),
|
|
548
|
-
parses the output, then queries each container for its VMID
|
|
552
|
+
parses the output, then queries each container for its VMID. When
|
|
553
|
+
*use_ip* is true, each container's eth0 IP is also resolved and stored
|
|
554
|
+
as the ``host`` field; otherwise the container name itself is stored
|
|
555
|
+
(and relies on DNS/MagicDNS for resolution at connect time).
|
|
556
|
+
|
|
549
557
|
Existing entries with the host prefix are cleared first.
|
|
550
558
|
"""
|
|
551
559
|
if not host:
|
|
@@ -581,12 +589,15 @@ def sync(host: str, user: str = "") -> None:
|
|
|
581
589
|
clear_known_hosts_by_prefix("proxmox", f"{host}/")
|
|
582
590
|
|
|
583
591
|
for vmid, hostname in containers:
|
|
584
|
-
|
|
592
|
+
if use_ip:
|
|
593
|
+
container_host = _resolve_container_ip(hostname, host, user, vmid=vmid) or hostname
|
|
594
|
+
else:
|
|
595
|
+
container_host = hostname
|
|
585
596
|
save_known_host(
|
|
586
597
|
KnownHost(
|
|
587
598
|
type="proxmox",
|
|
588
599
|
name=f"{host}/{hostname}",
|
|
589
|
-
host=
|
|
600
|
+
host=container_host,
|
|
590
601
|
user="remo",
|
|
591
602
|
instance_id=vmid,
|
|
592
603
|
access_mode="direct",
|
|
@@ -52,7 +52,7 @@ class TestShellVersionCheck:
|
|
|
52
52
|
def test_equal_versions_proceeds_silently(self, runner, mocker):
|
|
53
53
|
"""When remote and local versions match, no prompt is shown."""
|
|
54
54
|
mocker.patch("remo_cli.core.version.get_current_version", return_value="0.8.0")
|
|
55
|
-
mocker.patch("remo_cli.core.ssh.check_remote_version", return_value="0.8.0")
|
|
55
|
+
mocker.patch("remo_cli.core.ssh.check_remote_version", return_value=("0.8.0", None))
|
|
56
56
|
mock_confirm = mocker.patch("remo_cli.core.output.confirm")
|
|
57
57
|
|
|
58
58
|
result = runner.invoke(shell, [])
|
|
@@ -64,7 +64,7 @@ class TestShellVersionCheck:
|
|
|
64
64
|
def test_remote_behind_prompts_update(self, runner, mocker):
|
|
65
65
|
"""When remote is behind local, user is prompted to update."""
|
|
66
66
|
mocker.patch("remo_cli.core.version.get_current_version", return_value="0.9.0")
|
|
67
|
-
mocker.patch("remo_cli.core.ssh.check_remote_version", return_value="0.8.0")
|
|
67
|
+
mocker.patch("remo_cli.core.ssh.check_remote_version", return_value=("0.8.0", None))
|
|
68
68
|
mock_confirm = mocker.patch("remo_cli.core.output.confirm", return_value=False)
|
|
69
69
|
|
|
70
70
|
result = runner.invoke(shell, [])
|
|
@@ -78,7 +78,7 @@ class TestShellVersionCheck:
|
|
|
78
78
|
def test_remote_behind_update_accepted(self, runner, mocker):
|
|
79
79
|
"""When user accepts update, provider update is called."""
|
|
80
80
|
mocker.patch("remo_cli.core.version.get_current_version", return_value="0.9.0")
|
|
81
|
-
mocker.patch("remo_cli.core.ssh.check_remote_version", return_value="0.8.0")
|
|
81
|
+
mocker.patch("remo_cli.core.ssh.check_remote_version", return_value=("0.8.0", None))
|
|
82
82
|
mocker.patch("remo_cli.core.output.confirm", return_value=True)
|
|
83
83
|
mock_update = mocker.patch("remo_cli.providers.hetzner.update", return_value=0)
|
|
84
84
|
|
|
@@ -87,11 +87,45 @@ class TestShellVersionCheck:
|
|
|
87
87
|
assert result.exit_code == 0
|
|
88
88
|
mock_update.assert_called_once_with(name="webserver")
|
|
89
89
|
|
|
90
|
+
@pytest.mark.usefixtures("_patch_shell_deps")
|
|
91
|
+
def test_update_failure_prompts_before_connect(self, runner, mocker):
|
|
92
|
+
"""When tools update fails, user is prompted to confirm connect."""
|
|
93
|
+
mocker.patch("remo_cli.core.version.get_current_version", return_value="0.9.0")
|
|
94
|
+
mocker.patch("remo_cli.core.ssh.check_remote_version", return_value=("0.8.0", None))
|
|
95
|
+
# First confirm() = "Update?" → True; second = "Connect anyway?" → True
|
|
96
|
+
mock_confirm = mocker.patch(
|
|
97
|
+
"remo_cli.core.output.confirm", side_effect=[True, True]
|
|
98
|
+
)
|
|
99
|
+
mocker.patch("remo_cli.providers.hetzner.update", return_value=2)
|
|
100
|
+
mock_shell_connect = mocker.patch("remo_cli.core.ssh.shell_connect")
|
|
101
|
+
|
|
102
|
+
result = runner.invoke(shell, [])
|
|
103
|
+
|
|
104
|
+
assert result.exit_code == 0
|
|
105
|
+
assert mock_confirm.call_count == 2
|
|
106
|
+
assert "Connect anyway?" in mock_confirm.call_args_list[1][0][0]
|
|
107
|
+
mock_shell_connect.assert_called_once()
|
|
108
|
+
|
|
109
|
+
@pytest.mark.usefixtures("_patch_shell_deps")
|
|
110
|
+
def test_update_failure_decline_aborts(self, runner, mocker):
|
|
111
|
+
"""When user declines after failed update, shell_connect is not called."""
|
|
112
|
+
mocker.patch("remo_cli.core.version.get_current_version", return_value="0.9.0")
|
|
113
|
+
mocker.patch("remo_cli.core.ssh.check_remote_version", return_value=("0.8.0", None))
|
|
114
|
+
# First confirm() = "Update?" → True; second = "Connect anyway?" → False
|
|
115
|
+
mocker.patch("remo_cli.core.output.confirm", side_effect=[True, False])
|
|
116
|
+
mocker.patch("remo_cli.providers.hetzner.update", return_value=2)
|
|
117
|
+
mock_shell_connect = mocker.patch("remo_cli.core.ssh.shell_connect")
|
|
118
|
+
|
|
119
|
+
result = runner.invoke(shell, [])
|
|
120
|
+
|
|
121
|
+
assert result.exit_code == 2
|
|
122
|
+
mock_shell_connect.assert_not_called()
|
|
123
|
+
|
|
90
124
|
@pytest.mark.usefixtures("_patch_shell_deps")
|
|
91
125
|
def test_remote_ahead_shows_warning(self, runner, mocker):
|
|
92
126
|
"""When remote is ahead of local, a warning is shown."""
|
|
93
127
|
mocker.patch("remo_cli.core.version.get_current_version", return_value="0.8.0")
|
|
94
|
-
mocker.patch("remo_cli.core.ssh.check_remote_version", return_value="0.9.0")
|
|
128
|
+
mocker.patch("remo_cli.core.ssh.check_remote_version", return_value=("0.9.0", None))
|
|
95
129
|
mock_confirm = mocker.patch("remo_cli.core.output.confirm")
|
|
96
130
|
|
|
97
131
|
result = runner.invoke(shell, [])
|
|
@@ -105,7 +139,7 @@ class TestShellVersionCheck:
|
|
|
105
139
|
def test_no_marker_prompts_update(self, runner, mocker):
|
|
106
140
|
"""When remote has no version marker, user is prompted to update."""
|
|
107
141
|
mocker.patch("remo_cli.core.version.get_current_version", return_value="0.8.0")
|
|
108
|
-
mocker.patch("remo_cli.core.ssh.check_remote_version", return_value=None)
|
|
142
|
+
mocker.patch("remo_cli.core.ssh.check_remote_version", return_value=(None, None))
|
|
109
143
|
mock_confirm = mocker.patch("remo_cli.core.output.confirm", return_value=False)
|
|
110
144
|
|
|
111
145
|
result = runner.invoke(shell, [])
|
|
@@ -114,6 +148,25 @@ class TestShellVersionCheck:
|
|
|
114
148
|
mock_confirm.assert_called_once()
|
|
115
149
|
assert "no version info" in mock_confirm.call_args[0][0]
|
|
116
150
|
|
|
151
|
+
@pytest.mark.usefixtures("_patch_shell_deps")
|
|
152
|
+
def test_ssh_error_skips_update_prompt(self, runner, mocker):
|
|
153
|
+
"""When SSH itself fails, the user is warned and not prompted to update."""
|
|
154
|
+
mocker.patch("remo_cli.core.version.get_current_version", return_value="0.8.0")
|
|
155
|
+
mocker.patch(
|
|
156
|
+
"remo_cli.core.ssh.check_remote_version",
|
|
157
|
+
return_value=(None, "Host key verification failed."),
|
|
158
|
+
)
|
|
159
|
+
mock_confirm = mocker.patch("remo_cli.core.output.confirm")
|
|
160
|
+
mock_shell_connect = mocker.patch("remo_cli.core.ssh.shell_connect")
|
|
161
|
+
|
|
162
|
+
result = runner.invoke(shell, [])
|
|
163
|
+
|
|
164
|
+
assert result.exit_code == 0
|
|
165
|
+
mock_confirm.assert_not_called()
|
|
166
|
+
assert "Could not check tools version" in result.output
|
|
167
|
+
assert "Host key verification failed." in result.output
|
|
168
|
+
mock_shell_connect.assert_called_once()
|
|
169
|
+
|
|
117
170
|
@pytest.mark.usefixtures("_patch_shell_deps")
|
|
118
171
|
def test_unknown_local_version_skips_check(self, runner, mocker):
|
|
119
172
|
"""When local version is unknown, skip the version check."""
|
|
@@ -395,51 +395,71 @@ class TestCheckRemoteVersion:
|
|
|
395
395
|
|
|
396
396
|
@pytest.mark.usefixtures("_suppress_tz")
|
|
397
397
|
def test_returns_version_on_success(self, hetzner_host, mocker):
|
|
398
|
-
"""Returns
|
|
398
|
+
"""Returns (version, None) when SSH succeeds and marker is present."""
|
|
399
399
|
mocker.patch("remo_cli.core.ssh.get_aws_region", return_value="us-west-2")
|
|
400
400
|
mock_run = mocker.patch(
|
|
401
401
|
"subprocess.run",
|
|
402
|
-
return_value=MagicMock(returncode=0, stdout="0.8.0rc1\n"),
|
|
402
|
+
return_value=MagicMock(returncode=0, stdout="0.8.0rc1\n", stderr=""),
|
|
403
403
|
)
|
|
404
404
|
|
|
405
405
|
from remo_cli.core.ssh import check_remote_version
|
|
406
406
|
|
|
407
|
-
|
|
408
|
-
assert result == "0.8.0rc1"
|
|
407
|
+
assert check_remote_version(hetzner_host) == ("0.8.0rc1", None)
|
|
409
408
|
|
|
410
409
|
# Verify SSH command includes cat ~/.remo-version
|
|
411
410
|
call_args = mock_run.call_args[0][0]
|
|
412
411
|
assert "cat ~/.remo-version 2>/dev/null" in call_args
|
|
412
|
+
# And uses BatchMode so it can't hang on host-key/auth prompts
|
|
413
|
+
assert "BatchMode=yes" in call_args
|
|
413
414
|
|
|
414
415
|
@pytest.mark.usefixtures("_suppress_tz")
|
|
415
|
-
def
|
|
416
|
-
"""Returns None when the remote
|
|
416
|
+
def test_returns_no_marker_on_missing_file(self, hetzner_host, mocker):
|
|
417
|
+
"""Returns (None, None) when the remote command failed (cat → marker missing)."""
|
|
417
418
|
mocker.patch("remo_cli.core.ssh.get_aws_region", return_value="us-west-2")
|
|
418
419
|
mocker.patch(
|
|
419
420
|
"subprocess.run",
|
|
420
|
-
return_value=MagicMock(returncode=1, stdout=""),
|
|
421
|
+
return_value=MagicMock(returncode=1, stdout="", stderr=""),
|
|
421
422
|
)
|
|
422
423
|
|
|
423
424
|
from remo_cli.core.ssh import check_remote_version
|
|
424
425
|
|
|
425
|
-
assert check_remote_version(hetzner_host)
|
|
426
|
+
assert check_remote_version(hetzner_host) == (None, None)
|
|
426
427
|
|
|
427
428
|
@pytest.mark.usefixtures("_suppress_tz")
|
|
428
|
-
def
|
|
429
|
-
"""Returns None when SSH succeeds but
|
|
429
|
+
def test_returns_no_marker_on_empty_output(self, hetzner_host, mocker):
|
|
430
|
+
"""Returns (None, None) when SSH succeeds but the marker is empty."""
|
|
430
431
|
mocker.patch("remo_cli.core.ssh.get_aws_region", return_value="us-west-2")
|
|
431
432
|
mocker.patch(
|
|
432
433
|
"subprocess.run",
|
|
433
|
-
return_value=MagicMock(returncode=0, stdout=""),
|
|
434
|
+
return_value=MagicMock(returncode=0, stdout="", stderr=""),
|
|
434
435
|
)
|
|
435
436
|
|
|
436
437
|
from remo_cli.core.ssh import check_remote_version
|
|
437
438
|
|
|
438
|
-
assert check_remote_version(hetzner_host)
|
|
439
|
+
assert check_remote_version(hetzner_host) == (None, None)
|
|
439
440
|
|
|
440
441
|
@pytest.mark.usefixtures("_suppress_tz")
|
|
441
|
-
def
|
|
442
|
-
"""Returns None when SSH
|
|
442
|
+
def test_returns_error_on_ssh_failure(self, hetzner_host, mocker):
|
|
443
|
+
"""Returns (None, stderr) when SSH itself fails (rc 255)."""
|
|
444
|
+
mocker.patch("remo_cli.core.ssh.get_aws_region", return_value="us-west-2")
|
|
445
|
+
mocker.patch(
|
|
446
|
+
"subprocess.run",
|
|
447
|
+
return_value=MagicMock(
|
|
448
|
+
returncode=255,
|
|
449
|
+
stdout="",
|
|
450
|
+
stderr="Host key verification failed.\n",
|
|
451
|
+
),
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
from remo_cli.core.ssh import check_remote_version
|
|
455
|
+
|
|
456
|
+
version, err = check_remote_version(hetzner_host)
|
|
457
|
+
assert version is None
|
|
458
|
+
assert err == "Host key verification failed."
|
|
459
|
+
|
|
460
|
+
@pytest.mark.usefixtures("_suppress_tz")
|
|
461
|
+
def test_returns_error_on_timeout(self, hetzner_host, mocker):
|
|
462
|
+
"""Returns (None, error) when SSH times out."""
|
|
443
463
|
mocker.patch("remo_cli.core.ssh.get_aws_region", return_value="us-west-2")
|
|
444
464
|
mocker.patch(
|
|
445
465
|
"subprocess.run",
|
|
@@ -448,14 +468,20 @@ class TestCheckRemoteVersion:
|
|
|
448
468
|
|
|
449
469
|
from remo_cli.core.ssh import check_remote_version
|
|
450
470
|
|
|
451
|
-
|
|
471
|
+
version, err = check_remote_version(hetzner_host)
|
|
472
|
+
assert version is None
|
|
473
|
+
assert err is not None
|
|
474
|
+
assert "timed out" in err.lower()
|
|
452
475
|
|
|
453
476
|
@pytest.mark.usefixtures("_suppress_tz")
|
|
454
|
-
def
|
|
455
|
-
"""Returns None when
|
|
477
|
+
def test_returns_error_on_os_error(self, hetzner_host, mocker):
|
|
478
|
+
"""Returns (None, error) when the ssh binary can't be invoked."""
|
|
456
479
|
mocker.patch("remo_cli.core.ssh.get_aws_region", return_value="us-west-2")
|
|
457
480
|
mocker.patch("subprocess.run", side_effect=OSError("No ssh"))
|
|
458
481
|
|
|
459
482
|
from remo_cli.core.ssh import check_remote_version
|
|
460
483
|
|
|
461
|
-
|
|
484
|
+
version, err = check_remote_version(hetzner_host)
|
|
485
|
+
assert version is None
|
|
486
|
+
assert err is not None
|
|
487
|
+
assert "No ssh" in err
|
|
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
|
|
File without changes
|
|
File without changes
|