remo-cli 2.0.0rc2__tar.gz → 2.0.0rc4__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 (226) hide show
  1. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/.devcontainer/devcontainer.json +1 -1
  2. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/.github/workflows/smoke-test.yml +175 -0
  3. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/CLAUDE.md +2 -1
  4. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/PKG-INFO +38 -1
  5. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/README.md +37 -0
  6. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/docs/aws.md +110 -0
  7. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/pyproject.toml +1 -1
  8. remo_cli-2.0.0rc4/specs/005-provider-snapshots/checklists/requirements.md +36 -0
  9. remo_cli-2.0.0rc4/specs/005-provider-snapshots/contracts/cli-surface.md +160 -0
  10. remo_cli-2.0.0rc4/specs/005-provider-snapshots/data-model.md +114 -0
  11. remo_cli-2.0.0rc4/specs/005-provider-snapshots/plan.md +101 -0
  12. remo_cli-2.0.0rc4/specs/005-provider-snapshots/quickstart.md +161 -0
  13. remo_cli-2.0.0rc4/specs/005-provider-snapshots/research.md +132 -0
  14. remo_cli-2.0.0rc4/specs/005-provider-snapshots/spec.md +181 -0
  15. remo_cli-2.0.0rc4/specs/005-provider-snapshots/tasks.md +286 -0
  16. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/src/remo_cli/cli/main.py +18 -1
  17. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/src/remo_cli/cli/providers/aws.py +125 -0
  18. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/src/remo_cli/cli/providers/hetzner.py +105 -0
  19. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/src/remo_cli/cli/providers/incus.py +126 -0
  20. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/src/remo_cli/cli/providers/proxmox.py +167 -0
  21. remo_cli-2.0.0rc4/src/remo_cli/core/snapshot.py +170 -0
  22. remo_cli-2.0.0rc4/src/remo_cli/models/snapshot.py +43 -0
  23. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/src/remo_cli/providers/aws.py +505 -0
  24. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/src/remo_cli/providers/hetzner.py +335 -1
  25. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/src/remo_cli/providers/incus.py +348 -0
  26. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/src/remo_cli/providers/proxmox.py +349 -0
  27. remo_cli-2.0.0rc4/tests/unit/cli/providers/test_aws_snapshot.py +126 -0
  28. remo_cli-2.0.0rc4/tests/unit/cli/providers/test_hetzner_snapshot.py +129 -0
  29. remo_cli-2.0.0rc4/tests/unit/cli/providers/test_incus_snapshot.py +212 -0
  30. remo_cli-2.0.0rc4/tests/unit/cli/providers/test_proxmox_snapshot.py +158 -0
  31. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/tests/unit/cli/test_main.py +39 -0
  32. remo_cli-2.0.0rc4/tests/unit/core/test_snapshot.py +176 -0
  33. remo_cli-2.0.0rc4/tests/unit/providers/test_aws_snapshot.py +458 -0
  34. remo_cli-2.0.0rc4/tests/unit/providers/test_hetzner_snapshot.py +372 -0
  35. remo_cli-2.0.0rc4/tests/unit/providers/test_incus_snapshot.py +467 -0
  36. remo_cli-2.0.0rc4/tests/unit/providers/test_proxmox_snapshot.py +465 -0
  37. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/uv.lock +1 -1
  38. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/.claude/commands/speckit.analyze.md +0 -0
  39. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/.claude/commands/speckit.checklist.md +0 -0
  40. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/.claude/commands/speckit.clarify.md +0 -0
  41. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/.claude/commands/speckit.constitution.md +0 -0
  42. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/.claude/commands/speckit.implement.md +0 -0
  43. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/.claude/commands/speckit.plan.md +0 -0
  44. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/.claude/commands/speckit.specify.md +0 -0
  45. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/.claude/commands/speckit.tasks.md +0 -0
  46. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/.claude/commands/speckit.taskstoissues.md +0 -0
  47. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/.dockerignore +0 -0
  48. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/.github/dependabot.yml +0 -0
  49. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/.github/workflows/ci.yml +0 -0
  50. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/.github/workflows/provision.yml +0 -0
  51. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/.github/workflows/release.yml +0 -0
  52. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/.github/workflows/smoke-test-cleanup.yml +0 -0
  53. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/.github/workflows/teardown.yml +0 -0
  54. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/.gitignore +0 -0
  55. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/.maverick/checkpoints/feature/implementation_complete.json +0 -0
  56. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/.maverick/checkpoints/feature/phase_${{ index }}_complete.json +0 -0
  57. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/.maverick/checkpoints/feature/validation_complete.json +0 -0
  58. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/.specify/memory/constitution.md +0 -0
  59. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/.specify/scripts/bash/check-prerequisites.sh +0 -0
  60. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/.specify/scripts/bash/common.sh +0 -0
  61. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/.specify/scripts/bash/create-new-feature.sh +0 -0
  62. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/.specify/scripts/bash/setup-plan.sh +0 -0
  63. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/.specify/scripts/bash/update-agent-context.sh +0 -0
  64. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/.specify/templates/agent-file-template.md +0 -0
  65. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/.specify/templates/checklist-template.md +0 -0
  66. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/.specify/templates/plan-template.md +0 -0
  67. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/.specify/templates/spec-template.md +0 -0
  68. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/.specify/templates/tasks-template.md +0 -0
  69. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/CONTRIBUTING.md +0 -0
  70. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/Dockerfile +0 -0
  71. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/LICENSE +0 -0
  72. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/.ansible-lint +0 -0
  73. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/README.md +0 -0
  74. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/ansible.cfg +0 -0
  75. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/aws_configure.yml +0 -0
  76. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/aws_resize.yml +0 -0
  77. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/aws_site.yml +0 -0
  78. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/aws_teardown.yml +0 -0
  79. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/group_vars/all.yml +0 -0
  80. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/group_vars/incus_containers.yml +0 -0
  81. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/group_vars/proxmox_containers.yml +0 -0
  82. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/hetzner_configure.yml +0 -0
  83. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/hetzner_provision.yml +0 -0
  84. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/hetzner_resize.yml +0 -0
  85. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/hetzner_site.yml +0 -0
  86. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/hetzner_teardown.yml +0 -0
  87. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/incus_bootstrap.yml +0 -0
  88. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/incus_configure.yml +0 -0
  89. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/incus_provision.yml +0 -0
  90. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/incus_resize.yml +0 -0
  91. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/incus_site.yml +0 -0
  92. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/incus_teardown.yml +0 -0
  93. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/inventory/hosts.yml +0 -0
  94. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/inventory/incus_containers.yml +0 -0
  95. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/proxmox_bootstrap.yml +0 -0
  96. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/proxmox_configure.yml +0 -0
  97. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/proxmox_provision.yml +0 -0
  98. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/proxmox_resize.yml +0 -0
  99. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/proxmox_site.yml +0 -0
  100. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/proxmox_teardown.yml +0 -0
  101. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/requirements.yml +0 -0
  102. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/aws_server/defaults/main.yml +0 -0
  103. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/aws_server/tasks/ebs.yml +0 -0
  104. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/aws_server/tasks/ec2.yml +0 -0
  105. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/aws_server/tasks/main.yml +0 -0
  106. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/aws_server/tasks/patch_manager.yml +0 -0
  107. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/aws_server/tasks/preflight.yml +0 -0
  108. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/aws_server/tasks/security_group.yml +0 -0
  109. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/devcontainers/tasks/main.yml +0 -0
  110. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/docker/handlers/main.yml +0 -0
  111. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/docker/tasks/main.yml +0 -0
  112. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/fzf/tasks/main.yml +0 -0
  113. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/github_cli/tasks/main.yml +0 -0
  114. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/hetzner_server/defaults/main.yml +0 -0
  115. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/hetzner_server/tasks/main.yml +0 -0
  116. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/incus_bootstrap/defaults/main.yml +0 -0
  117. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/incus_bootstrap/handlers/main.yml +0 -0
  118. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/incus_bootstrap/tasks/main.yml +0 -0
  119. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/incus_container/defaults/main.yml +0 -0
  120. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/incus_container/handlers/main.yml +0 -0
  121. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/incus_container/meta/main.yml +0 -0
  122. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/incus_container/tasks/main.yml +0 -0
  123. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/incus_container/tasks/preflight.yml +0 -0
  124. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/incus_container_teardown/defaults/main.yml +0 -0
  125. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/incus_container_teardown/meta/main.yml +0 -0
  126. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/incus_container_teardown/tasks/main.yml +0 -0
  127. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/nodejs/defaults/main.yml +0 -0
  128. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/nodejs/tasks/main.yml +0 -0
  129. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/proxmox_bootstrap/defaults/main.yml +0 -0
  130. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/proxmox_bootstrap/meta/main.yml +0 -0
  131. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/proxmox_bootstrap/tasks/main.yml +0 -0
  132. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/proxmox_container/defaults/main.yml +0 -0
  133. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/proxmox_container/handlers/main.yml +0 -0
  134. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/proxmox_container/meta/main.yml +0 -0
  135. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/proxmox_container/tasks/main.yml +0 -0
  136. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/proxmox_container/tasks/preflight.yml +0 -0
  137. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/proxmox_container_teardown/defaults/main.yml +0 -0
  138. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/proxmox_container_teardown/meta/main.yml +0 -0
  139. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/proxmox_container_teardown/tasks/main.yml +0 -0
  140. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/user_setup/defaults/main.yml +0 -0
  141. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/user_setup/tasks/main.yml +0 -0
  142. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/user_setup/templates/devshell.sh.j2 +0 -0
  143. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/user_setup/templates/project-menu.sh.j2 +0 -0
  144. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/zellij/defaults/main.yml +0 -0
  145. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/zellij/tasks/main.yml +0 -0
  146. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/zellij/templates/config.kdl.j2 +0 -0
  147. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/roles/zellij/templates/layouts/minimal.kdl.j2 +0 -0
  148. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/ansible/tasks/configure_dev_tools.yml +0 -0
  149. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/docs/hetzner.md +0 -0
  150. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/docs/incus.md +0 -0
  151. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/docs/install.sh +0 -0
  152. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/docs/proxmox.md +0 -0
  153. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/install.sh +0 -0
  154. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/maverick.yaml +0 -0
  155. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/project-menu.sh +0 -0
  156. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/remo +0 -0
  157. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/run.sh +0 -0
  158. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/scripts/setup-aws-oidc.sh +0 -0
  159. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/specs/001-bootstrap-incus-host/checklists/requirements.md +0 -0
  160. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/specs/001-bootstrap-incus-host/checklists/validation.md +0 -0
  161. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/specs/001-bootstrap-incus-host/contracts/playbook-interface.yaml +0 -0
  162. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/specs/001-bootstrap-incus-host/contracts/role-interface.yaml +0 -0
  163. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/specs/001-bootstrap-incus-host/data-model.md +0 -0
  164. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/specs/001-bootstrap-incus-host/plan.md +0 -0
  165. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/specs/001-bootstrap-incus-host/quickstart.md +0 -0
  166. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/specs/001-bootstrap-incus-host/research.md +0 -0
  167. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/specs/001-bootstrap-incus-host/spec.md +0 -0
  168. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/specs/001-bootstrap-incus-host/tasks.md +0 -0
  169. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/specs/002-incus-container-support/checklists/requirements.md +0 -0
  170. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/specs/002-incus-container-support/contracts/playbook-interface.yaml +0 -0
  171. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/specs/002-incus-container-support/contracts/role-interface.yaml +0 -0
  172. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/specs/002-incus-container-support/data-model.md +0 -0
  173. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/specs/002-incus-container-support/plan.md +0 -0
  174. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/specs/002-incus-container-support/quickstart.md +0 -0
  175. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/specs/002-incus-container-support/research.md +0 -0
  176. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/specs/002-incus-container-support/spec.md +0 -0
  177. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/specs/002-incus-container-support/tasks.md +0 -0
  178. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/specs/003-python-cli-rewrite/checklists/requirements.md +0 -0
  179. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/specs/003-python-cli-rewrite/data-model.md +0 -0
  180. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/specs/003-python-cli-rewrite/plan.md +0 -0
  181. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/specs/003-python-cli-rewrite/quickstart.md +0 -0
  182. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/specs/003-python-cli-rewrite/research.md +0 -0
  183. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/specs/003-python-cli-rewrite/spec.md +0 -0
  184. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/specs/003-python-cli-rewrite/tasks.md +0 -0
  185. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/specs/004-proxmox-container-support/spec.md +0 -0
  186. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/src/remo_cli/__init__.py +0 -0
  187. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/src/remo_cli/__main__.py +0 -0
  188. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/src/remo_cli/cli/__init__.py +0 -0
  189. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/src/remo_cli/cli/cp.py +0 -0
  190. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/src/remo_cli/cli/providers/__init__.py +0 -0
  191. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/src/remo_cli/cli/shell.py +0 -0
  192. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/src/remo_cli/core/__init__.py +0 -0
  193. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/src/remo_cli/core/ansible_runner.py +0 -0
  194. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/src/remo_cli/core/completion.py +0 -0
  195. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/src/remo_cli/core/config.py +0 -0
  196. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/src/remo_cli/core/known_hosts.py +0 -0
  197. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/src/remo_cli/core/output.py +0 -0
  198. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/src/remo_cli/core/picker.py +0 -0
  199. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/src/remo_cli/core/rsync.py +0 -0
  200. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/src/remo_cli/core/ssh.py +0 -0
  201. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/src/remo_cli/core/validation.py +0 -0
  202. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/src/remo_cli/core/version.py +0 -0
  203. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/src/remo_cli/models/__init__.py +0 -0
  204. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/src/remo_cli/models/host.py +0 -0
  205. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/src/remo_cli/providers/__init__.py +0 -0
  206. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/tests/__init__.py +0 -0
  207. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/tests/conftest.py +0 -0
  208. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/tests/integration/__init__.py +0 -0
  209. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/tests/integration/orbstack-cloud-init.yml +0 -0
  210. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/tests/integration/orbstack.sh +0 -0
  211. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/tests/unit/__init__.py +0 -0
  212. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/tests/unit/cli/__init__.py +0 -0
  213. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/tests/unit/cli/test_cp.py +0 -0
  214. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/tests/unit/cli/test_shell.py +0 -0
  215. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/tests/unit/core/__init__.py +0 -0
  216. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/tests/unit/core/test_ansible_runner.py +0 -0
  217. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/tests/unit/core/test_config.py +0 -0
  218. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/tests/unit/core/test_known_hosts.py +0 -0
  219. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/tests/unit/core/test_output.py +0 -0
  220. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/tests/unit/core/test_picker.py +0 -0
  221. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/tests/unit/core/test_rsync.py +0 -0
  222. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/tests/unit/core/test_ssh.py +0 -0
  223. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/tests/unit/core/test_validation.py +0 -0
  224. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/tests/unit/core/test_version.py +0 -0
  225. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/tests/unit/providers/__init__.py +0 -0
  226. {remo_cli-2.0.0rc2 → remo_cli-2.0.0rc4}/tests/unit/test_host_model.py +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "Python Agentic Development",
3
- "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
3
+ "image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04",
4
4
  "remoteUser": "vscode",
5
5
  "features": {
6
6
  "ghcr.io/devcontainers/features/python:1": {},
@@ -139,9 +139,55 @@ jobs:
139
139
  echo "$OUT" | grep -q "EBS volume:" || { echo "FAIL: missing EBS volume"; exit 1; }
140
140
  echo "aws info: PASS"
141
141
 
142
+ - name: Snapshot — create (async kickoff)
143
+ run: |
144
+ SNAP=ci-smoke-aws
145
+ echo "SNAP_NAME=$SNAP" >> "$GITHUB_ENV"
146
+ OUT=$(uv run remo aws snapshot create "$INSTANCE_NAME" --name "$SNAP" --description "smoke test")
147
+ echo "$OUT"
148
+ echo "$OUT" | grep -q "creation started" || { echo "FAIL: missing async-create hint"; exit 1; }
149
+ echo "$OUT" | grep -q "several minutes" || { echo "FAIL: missing duration hint"; exit 1; }
150
+ echo "aws snapshot create: PASS"
151
+
152
+ - name: Snapshot — list shows pending then available
153
+ run: |
154
+ # Initial list: pending (or already available if AWS is fast today)
155
+ uv run remo aws snapshot list "$INSTANCE_NAME" | tee /tmp/list-1.txt
156
+ grep -q "$SNAP_NAME" /tmp/list-1.txt || { echo "FAIL: snapshot missing from initial list"; exit 1; }
157
+ grep -q "STATUS" /tmp/list-1.txt || { echo "FAIL: AWS list missing STATUS column"; exit 1; }
158
+
159
+ # Poll up to 10 minutes for the snapshot to reach available state
160
+ for attempt in $(seq 1 60); do
161
+ OUT=$(uv run remo aws snapshot list "$INSTANCE_NAME")
162
+ if echo "$OUT" | grep -E "^\s*$INSTANCE_NAME\s+$SNAP_NAME\s.*\savailable" >/dev/null; then
163
+ echo "Snapshot $SNAP_NAME is available (attempt $attempt)"
164
+ break
165
+ fi
166
+ echo "Attempt $attempt: not yet available; sleeping 10s..."
167
+ sleep 10
168
+ done
169
+ uv run remo aws snapshot list "$INSTANCE_NAME" | grep -E "^\s*$INSTANCE_NAME\s+$SNAP_NAME\s.*\savailable" \
170
+ || { echo "FAIL: snapshot did not reach available within 10 min"; exit 1; }
171
+ echo "aws snapshot list: PASS"
172
+
173
+ - name: Snapshot — delete
174
+ run: |
175
+ uv run remo aws snapshot delete "$INSTANCE_NAME" "$SNAP_NAME" --yes
176
+ # Verify it's gone from list
177
+ OUT=$(uv run remo aws snapshot list "$INSTANCE_NAME")
178
+ if echo "$OUT" | grep -q "$SNAP_NAME"; then
179
+ echo "FAIL: snapshot still present after delete"; exit 1
180
+ fi
181
+ echo "aws snapshot delete: PASS"
182
+
142
183
  - name: Teardown
143
184
  if: always()
144
185
  run: |
186
+ # Best-effort cleanup of any leftover smoke snapshots (in case of
187
+ # mid-step failure) before tearing down the instance.
188
+ if [ -n "${SNAP_NAME:-}" ]; then
189
+ uv run remo aws snapshot delete "$INSTANCE_NAME" "$SNAP_NAME" --yes 2>/dev/null || true
190
+ fi
145
191
  uv run remo aws destroy --name "$INSTANCE_NAME" --yes --remove-storage || true
146
192
 
147
193
  # ===========================================================================
@@ -217,9 +263,51 @@ jobs:
217
263
  echo "$OUT" | grep -q "Volume:" || { echo "FAIL: missing Volume"; exit 1; }
218
264
  echo "hetzner info: PASS"
219
265
 
266
+ - name: Snapshot — create (async kickoff)
267
+ run: |
268
+ SNAP=ci-smoke-hetzner
269
+ echo "SNAP_NAME=$SNAP" >> "$GITHUB_ENV"
270
+ OUT=$(uv run remo hetzner snapshot create "$INSTANCE_NAME" --name "$SNAP" --description "smoke test")
271
+ echo "$OUT"
272
+ echo "$OUT" | grep -q "creation started" || { echo "FAIL: missing async-create hint"; exit 1; }
273
+ echo "$OUT" | grep -q "several minutes" || { echo "FAIL: missing duration hint"; exit 1; }
274
+ echo "hetzner snapshot create: PASS"
275
+
276
+ - name: Snapshot — list shows pending then available
277
+ run: |
278
+ uv run remo hetzner snapshot list "$INSTANCE_NAME" | tee /tmp/list-1.txt
279
+ grep -q "$SNAP_NAME" /tmp/list-1.txt || { echo "FAIL: snapshot missing from initial list"; exit 1; }
280
+ grep -q "STATUS" /tmp/list-1.txt || { echo "FAIL: Hetzner list missing STATUS column"; exit 1; }
281
+
282
+ # Poll up to 5 minutes for the snapshot to become available
283
+ for attempt in $(seq 1 30); do
284
+ OUT=$(uv run remo hetzner snapshot list "$INSTANCE_NAME")
285
+ if echo "$OUT" | grep -E "^\s*$INSTANCE_NAME\s+$SNAP_NAME\s.*\savailable" >/dev/null; then
286
+ echo "Snapshot $SNAP_NAME is available (attempt $attempt)"
287
+ break
288
+ fi
289
+ echo "Attempt $attempt: not yet available; sleeping 10s..."
290
+ sleep 10
291
+ done
292
+ uv run remo hetzner snapshot list "$INSTANCE_NAME" | grep -E "^\s*$INSTANCE_NAME\s+$SNAP_NAME\s.*\savailable" \
293
+ || { echo "FAIL: snapshot did not reach available within 5 min"; exit 1; }
294
+ echo "hetzner snapshot list: PASS"
295
+
296
+ - name: Snapshot — delete
297
+ run: |
298
+ uv run remo hetzner snapshot delete "$INSTANCE_NAME" "$SNAP_NAME" --yes
299
+ OUT=$(uv run remo hetzner snapshot list "$INSTANCE_NAME")
300
+ if echo "$OUT" | grep -q "$SNAP_NAME"; then
301
+ echo "FAIL: snapshot still present after delete"; exit 1
302
+ fi
303
+ echo "hetzner snapshot delete: PASS"
304
+
220
305
  - name: Teardown
221
306
  if: always()
222
307
  run: |
308
+ if [ -n "${SNAP_NAME:-}" ]; then
309
+ uv run remo hetzner snapshot delete "$INSTANCE_NAME" "$SNAP_NAME" --yes 2>/dev/null || true
310
+ fi
223
311
  uv run remo hetzner destroy --name "$INSTANCE_NAME" --yes --remove-volume || true
224
312
 
225
313
  # ===========================================================================
@@ -365,6 +453,93 @@ jobs:
365
453
  verify_tools
366
454
  echo "Post-update dev tools verification: PASS"
367
455
 
456
+ - name: Snapshot — create
457
+ run: |
458
+ SNAP=ci-smoke-incus
459
+ echo "SNAP_NAME=$SNAP" >> "$GITHUB_ENV"
460
+ OUT=$(sg incus-admin -c "uv run remo incus snapshot create $CONTAINER_NAME --name $SNAP --description 'smoke test'")
461
+ echo "$OUT"
462
+ echo "$OUT" | grep -q "Created snapshot '$SNAP'" || { echo "FAIL: missing sync-create success message"; exit 1; }
463
+ echo "incus snapshot create: PASS"
464
+
465
+ - name: Snapshot — list shows the new snapshot
466
+ run: |
467
+ OUT=$(sg incus-admin -c "uv run remo incus snapshot list $CONTAINER_NAME")
468
+ echo "$OUT"
469
+ echo "$OUT" | grep -q "$SNAP_NAME" || { echo "FAIL: snapshot missing from list"; exit 1; }
470
+ # Incus list MUST NOT have a STATUS column (always AVAILABLE)
471
+ if echo "$OUT" | head -1 | grep -q "STATUS"; then
472
+ echo "FAIL: Incus list should not have a STATUS column"; exit 1
473
+ fi
474
+ echo "incus snapshot list: PASS"
475
+
476
+ - name: Snapshot — restore round-trip
477
+ run: |
478
+ SSH_CMD="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
479
+ -o ConnectTimeout=30 -o BatchMode=yes -i ~/.ssh/id_rsa remo@$HOST"
480
+
481
+ # Plant a marker file inside the container AFTER the snapshot was
482
+ # taken. If restore works, this marker must be gone afterwards.
483
+ $SSH_CMD "echo post-snapshot > /tmp/should-be-gone-after-restore.txt"
484
+ $SSH_CMD "test -f /tmp/should-be-gone-after-restore.txt"
485
+ echo "Marker file planted post-snapshot."
486
+
487
+ # Restore. -y to bypass the confirm. The provider stops the
488
+ # container, restores, and starts it again.
489
+ sg incus-admin -c "uv run remo incus snapshot restore $CONTAINER_NAME $SNAP_NAME -y"
490
+
491
+ # Container may take a few seconds to come back online after start.
492
+ for attempt in $(seq 1 12); do
493
+ if $SSH_CMD "echo connection-ok" 2>/dev/null | grep -q "connection-ok"; then
494
+ echo "SSH back after restore (attempt $attempt)"
495
+ break
496
+ fi
497
+ sleep 5
498
+ done
499
+
500
+ # The post-snapshot marker MUST be gone (SC-001).
501
+ if $SSH_CMD "test -f /tmp/should-be-gone-after-restore.txt" 2>/dev/null; then
502
+ echo "FAIL: marker file still present after restore — rollback didn't happen"
503
+ exit 1
504
+ fi
505
+ echo "Post-restore: marker file is gone — rollback PASS"
506
+ echo "incus snapshot restore: PASS"
507
+
508
+ - name: Snapshot — delete
509
+ run: |
510
+ sg incus-admin -c "uv run remo incus snapshot delete $CONTAINER_NAME $SNAP_NAME -y"
511
+ OUT=$(sg incus-admin -c "uv run remo incus snapshot list $CONTAINER_NAME")
512
+ if echo "$OUT" | grep -q "$SNAP_NAME"; then
513
+ echo "FAIL: snapshot still present after delete"
514
+ echo "$OUT"
515
+ exit 1
516
+ fi
517
+ echo "incus snapshot delete: PASS"
518
+
519
+ - name: Snapshot — destroy-time cleanup prompt
520
+ run: |
521
+ # Create two snapshots, then destroy with -y. The destroy-time prompt
522
+ # is bypassed by -y, defaulting to KEEP snapshots (safer default).
523
+ # The destroy output must mention the snapshots and the keep-warning.
524
+ # We re-create the container after so the rest of the suite (cp tests)
525
+ # has a clean target.
526
+ sg incus-admin -c "uv run remo incus snapshot create $CONTAINER_NAME --name keep-1"
527
+ sg incus-admin -c "uv run remo incus snapshot create $CONTAINER_NAME --name keep-2"
528
+
529
+ # Capture destroy output to verify the prompt path fires
530
+ OUT=$(sg incus-admin -c "uv run remo incus destroy --name $CONTAINER_NAME --yes" 2>&1 || true)
531
+ echo "$OUT"
532
+ echo "$OUT" | grep -q "has 2 snapshot" || { echo "FAIL: destroy did not mention snapshots"; exit 1; }
533
+ echo "$OUT" | grep -q "keeping the 2 snapshot" || { echo "FAIL: destroy did not print --yes keep warning"; exit 1; }
534
+ echo "incus destroy snapshot-prompt: PASS"
535
+
536
+ # Re-create the container for downstream steps (cp tests etc.)
537
+ sg incus-admin -c "uv run remo incus create --name $CONTAINER_NAME"
538
+ # Refresh $HOST since the container was rebuilt
539
+ HOST=$(sudo incus list $CONTAINER_NAME --format=json | \
540
+ python3 -c "import sys,json; d=json.load(sys.stdin)[0]['state']['network']['eth0']['addresses']; print([a['address'] for a in d if a['family']=='inet'][0])")
541
+ echo "HOST=$HOST" >> "$GITHUB_ENV"
542
+
368
543
  - name: Prepare for remo cp tests
369
544
  run: |
370
545
  # Make container name resolvable (remo stores name, not IP, for Incus)
@@ -11,6 +11,7 @@ See `.specify/memory/constitution.md` for project principles and non-negotiable
11
11
  - N/A (Incus storage pools already configured by 001-bootstrap-incus-host) (002-incus-container-support)
12
12
  - Python 3.11+ + Click (CLI framework), InquirerPy (interactive picker), boto3 (AWS, optional), hcloud (Hetzner, optional) (003-python-cli-rewrite)
13
13
  - Flat file (`~/.config/remo/known_hosts`, colon-delimited) (003-python-cli-rewrite)
14
+ - Cross-provider snapshot model (`models/snapshot.py`) + shared helpers in `core/snapshot.py` (name generator, validator, table formatter, destroy-time cleanup hook). No new runtime deps. (005-provider-snapshots)
14
15
 
15
16
  - Ansible 2.14+ / YAML + `ansible.builtin`, `community.general` (for zypper module) (001-bootstrap-incus-host)
16
17
 
@@ -131,10 +132,10 @@ Provider SDKs (boto3, hcloud) are lazy-imported with clear error messages if mis
131
132
  - Ansible 2.14+ / YAML: Follow standard conventions plus Constitution principles
132
133
 
133
134
  ## Recent Changes
135
+ - 005-provider-snapshots: Added cross-provider snapshot CLI (`remo <P> snapshot {create,list,restore,delete}`) + destroy-time cleanup hook across Incus / Proxmox / AWS / Hetzner.
134
136
  - 003-python-cli-rewrite: Added Python 3.11+ + Click (CLI framework), InquirerPy (interactive picker), boto3 (AWS, optional), hcloud (Hetzner, optional)
135
137
  - 002-incus-container-support: Added Ansible 2.14+ / YAML + `ansible.builtin`, `community.general` (existing), Incus CLI (local)
136
138
 
137
- - 001-bootstrap-incus-host: Added Ansible 2.14+ / YAML + `ansible.builtin`, `community.general` (for zypper module)
138
139
 
139
140
  <!-- MANUAL ADDITIONS START -->
140
141
  <!-- MANUAL ADDITIONS END -->
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remo-cli
3
- Version: 2.0.0rc2
3
+ Version: 2.0.0rc4
4
4
  Summary: Remote development environment CLI
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -160,6 +160,34 @@ Every remo environment includes:
160
160
 
161
161
  ---
162
162
 
163
+ ## Snapshots
164
+
165
+ Capture a point-in-time copy of an instance before a risky change, then roll
166
+ back if it breaks. Available on every provider, with the same command surface:
167
+
168
+ ```bash
169
+ remo <provider> snapshot create <instance> [--name NAME] [--description TEXT]
170
+ remo <provider> snapshot list [INSTANCE]
171
+ remo <provider> snapshot restore <instance> <snapshot> [-y]
172
+ remo <provider> snapshot delete <instance> <snapshot> [-y]
173
+ ```
174
+
175
+ `--name` defaults to `remo-YYYYMMDD-HHMMSS`. `-y` / `--yes` bypasses the
176
+ confirm prompt on destructive operations.
177
+
178
+ | Provider | Create | Restore | Notes |
179
+ |---|---|---|---|
180
+ | **Incus** | seconds | in-place rollback (container stopped briefly) | Free; uses native `incus snapshot`. |
181
+ | **Proxmox** | seconds | in-place rollback (container stopped briefly) | Free; requires snapshot-capable rootfs storage (ZFS, LVM-thin, Btrfs, Ceph, NFS, CIFS). `dir` storage is rejected pre-flight. |
182
+ | **AWS** | async — several minutes | in-place EBS volume swap; stops the instance, swaps the root volume, restarts. Typically 2-5 min downtime. | Costs $ per GB-month in EBS. Pre-restore root volume is preserved as a tagged orphan — delete it manually once you've verified the restore. |
183
+ | **Hetzner** | async — several minutes | server rebuild from the snapshot image, in-place. Typically 1-2 min downtime. | Costs € per GB-month. |
184
+
185
+ `remo` does not estimate storage cost — check your provider's billing console.
186
+
187
+ The `destroy` command on each provider checks for existing snapshots first and
188
+ offers to clean them up. Decline and the snapshots remain (you'll be warned
189
+ they continue to incur storage costs on AWS/Hetzner).
190
+
163
191
  ## CLI Reference
164
192
 
165
193
  ```bash
@@ -223,6 +251,15 @@ remo proxmox update --name <n> --volume-size 40 --cores 4 --memory 4096
223
251
  remo proxmox destroy --name <n> [--yes] [--purge] # Destroy container
224
252
  remo proxmox bootstrap --host <node> # Verify node + download LXC template
225
253
 
254
+ # Snapshots (all four providers)
255
+ remo <provider> snapshot create <instance> # Auto-named
256
+ remo <provider> snapshot create <instance> --name pre-x --description "before upgrade"
257
+ remo <provider> snapshot list # All instances
258
+ remo <provider> snapshot list <instance> # One instance
259
+ remo <provider> snapshot restore <instance> <snap-name> [-y] # In-place rollback
260
+ remo <provider> snapshot delete <instance> <snap-name> [-y] # Remove
261
+ # `<provider> destroy` will list existing snapshots and offer to clean them up first.
262
+
226
263
  # Updates
227
264
  uv tool upgrade remo-cli # Update CLI to latest version
228
265
  remo <platform> update # Update dev tools on remote
@@ -141,6 +141,34 @@ Every remo environment includes:
141
141
 
142
142
  ---
143
143
 
144
+ ## Snapshots
145
+
146
+ Capture a point-in-time copy of an instance before a risky change, then roll
147
+ back if it breaks. Available on every provider, with the same command surface:
148
+
149
+ ```bash
150
+ remo <provider> snapshot create <instance> [--name NAME] [--description TEXT]
151
+ remo <provider> snapshot list [INSTANCE]
152
+ remo <provider> snapshot restore <instance> <snapshot> [-y]
153
+ remo <provider> snapshot delete <instance> <snapshot> [-y]
154
+ ```
155
+
156
+ `--name` defaults to `remo-YYYYMMDD-HHMMSS`. `-y` / `--yes` bypasses the
157
+ confirm prompt on destructive operations.
158
+
159
+ | Provider | Create | Restore | Notes |
160
+ |---|---|---|---|
161
+ | **Incus** | seconds | in-place rollback (container stopped briefly) | Free; uses native `incus snapshot`. |
162
+ | **Proxmox** | seconds | in-place rollback (container stopped briefly) | Free; requires snapshot-capable rootfs storage (ZFS, LVM-thin, Btrfs, Ceph, NFS, CIFS). `dir` storage is rejected pre-flight. |
163
+ | **AWS** | async — several minutes | in-place EBS volume swap; stops the instance, swaps the root volume, restarts. Typically 2-5 min downtime. | Costs $ per GB-month in EBS. Pre-restore root volume is preserved as a tagged orphan — delete it manually once you've verified the restore. |
164
+ | **Hetzner** | async — several minutes | server rebuild from the snapshot image, in-place. Typically 1-2 min downtime. | Costs € per GB-month. |
165
+
166
+ `remo` does not estimate storage cost — check your provider's billing console.
167
+
168
+ The `destroy` command on each provider checks for existing snapshots first and
169
+ offers to clean them up. Decline and the snapshots remain (you'll be warned
170
+ they continue to incur storage costs on AWS/Hetzner).
171
+
144
172
  ## CLI Reference
145
173
 
146
174
  ```bash
@@ -204,6 +232,15 @@ remo proxmox update --name <n> --volume-size 40 --cores 4 --memory 4096
204
232
  remo proxmox destroy --name <n> [--yes] [--purge] # Destroy container
205
233
  remo proxmox bootstrap --host <node> # Verify node + download LXC template
206
234
 
235
+ # Snapshots (all four providers)
236
+ remo <provider> snapshot create <instance> # Auto-named
237
+ remo <provider> snapshot create <instance> --name pre-x --description "before upgrade"
238
+ remo <provider> snapshot list # All instances
239
+ remo <provider> snapshot list <instance> # One instance
240
+ remo <provider> snapshot restore <instance> <snap-name> [-y] # In-place rollback
241
+ remo <provider> snapshot delete <instance> <snap-name> [-y] # Remove
242
+ # `<provider> destroy` will list existing snapshots and offer to clean them up first.
243
+
207
244
  # Updates
208
245
  uv tool upgrade remo-cli # Update CLI to latest version
209
246
  remo <platform> update # Update dev tools on remote
@@ -85,6 +85,14 @@ remo aws destroy --yes
85
85
 
86
86
  # Destroy instance AND EBS (removes all data)
87
87
  remo aws destroy --yes --remove-storage
88
+
89
+ # Snapshots
90
+ remo aws snapshot create <instance> # Auto-named (remo-YYYYMMDD-HHMMSS)
91
+ remo aws snapshot create <instance> --name pre-x --description "before upgrade"
92
+ remo aws snapshot list <instance> # Single instance
93
+ remo aws snapshot list # All registered AWS instances
94
+ remo aws snapshot restore <instance> <snap-name> [-y] # In-place EBS volume swap
95
+ remo aws snapshot delete <instance> <snap-name> [-y] # Remove snapshot
88
96
  ```
89
97
 
90
98
  ### Create Options
@@ -129,6 +137,25 @@ Available tools: `docker`, `user_setup`, `nodejs`, `devcontainers`, `github_cli`
129
137
  |--------|-------------|
130
138
  | `--name <name>` | Resource namespace (default: `$USER`) |
131
139
 
140
+ ### Snapshot Options
141
+
142
+ | Option | Applies to | Description |
143
+ |--------|------------|-------------|
144
+ | `--name <snap>` | `create` | Snapshot name. Defaults to `remo-YYYYMMDD-HHMMSS`. Validated client-side: 1–40 chars, `^[A-Za-z0-9][A-Za-z0-9_-]*$`. |
145
+ | `--description <text>` | `create` | Free-text description stored on the EBS snapshot and shown in `list`. |
146
+ | `--region <region>` | all | AWS region. Defaults to the registered instance's region. |
147
+ | `--yes`, `-y` | `restore`, `delete` | Bypass the confirmation prompt. |
148
+
149
+ ### Snapshot Behavior
150
+
151
+ * **Create is asynchronous.** The command returns within a few seconds with a "creation started; will take several minutes" hint. Use `snapshot list` to watch the `STATUS` column transition from `pending` to `available`. Restore and delete refuse to operate on pending snapshots.
152
+ * **Restore is an in-place volume swap.** The instance is stopped, the root EBS volume is detached, a new volume is created from the snapshot, attached as root, and the instance is restarted. Typical downtime: 2-5 minutes.
153
+ * **Restored volume preserves current size.** If you grew the EBS volume after taking the snapshot, the restored volume is created at the current (larger) size. The filesystem inside stays at the snapshot's recorded size until you run `sudo resize2fs $(findmnt -no SOURCE /)` inside the instance — `remo` prints this hint on restore.
154
+ * **Pre-restore volume is preserved as an orphan.** After a successful restore, the pre-restore root volume is *not* deleted; it stays in the AZ tagged `remo-restore-orphan=<ISO-8601-timestamp>` as a safety net. Once you've verified the restore is healthy, delete it manually with `aws ec2 delete-volume --volume-id <id>`. `remo` prints the exact command on restore.
155
+ * **Snapshots are scoped by volume ID.** Snapshots are listed by filtering on the *current* root volume ID, so if you destroy an instance and create a new one with the same name, the old snapshots will not appear in `list` (they become orphans, invisible to `remo`, manageable only via the AWS console).
156
+ * **No cost estimation.** `remo` does not estimate monthly EBS snapshot storage cost — check your AWS billing console.
157
+ * **Destroy-time cleanup.** `remo aws destroy` lists existing snapshots and offers to delete them as part of teardown. Declining (or passing `-y`) keeps the snapshots, which will continue to incur storage cost.
158
+
132
159
  ## Features
133
160
 
134
161
  | Feature | Description |
@@ -180,6 +207,89 @@ df -h /home/remo
180
207
 
181
208
  All instances use SSM Session Manager — no inbound ports are opened.
182
209
 
210
+ ### IAM Permissions
211
+
212
+ The principal `remo` uses (your local IAM user, SSO session, or assumed role) needs the following EC2 actions. If you scoped your `remo` policy tightly, you'll see `UnauthorizedOperation` errors the first time you try a subcommand whose actions aren't in your policy.
213
+
214
+ | Subcommand | Required EC2 actions |
215
+ |---|---|
216
+ | `create` | `RunInstances`, `DescribeInstances`, `DescribeImages`, `DescribeVolumes`, `CreateTags`, `CreateVolume`, `DescribeSecurityGroups`, `CreateSecurityGroup`, `AuthorizeSecurityGroupIngress`, `CreateKeyPair`, `ImportKeyPair`, `DescribeKeyPairs`, `RunInstances`, `iam:PassRole`, `iam:GetRole`, `iam:CreateRole`, `iam:AttachRolePolicy`, `iam:GetInstanceProfile`, `iam:CreateInstanceProfile`, `iam:AddRoleToInstanceProfile` |
217
+ | `destroy` | `DescribeInstances`, `TerminateInstances`, `DescribeVolumes`, `DeleteVolume`, `DescribeSecurityGroups`, `DeleteSecurityGroup`, `DescribeKeyPairs`, `DeleteKeyPair`, plus the IAM cleanup mirror of `create` |
218
+ | `update`, `info`, `list`, `sync` | `DescribeInstances`, `DescribeVolumes` (+ `ModifyVolume` and `DescribeVolumesModifications` for `update --volume-size`) |
219
+ | `stop`, `start`, `reboot` | `DescribeInstances`, `StopInstances` / `StartInstances` / `RebootInstances` |
220
+ | `snapshot create` | `DescribeInstances`, `DescribeVolumes`, `DescribeSnapshots`, `CreateSnapshot`, `CreateTags` |
221
+ | `snapshot list` | `DescribeInstances`, `DescribeVolumes`, `DescribeSnapshots` |
222
+ | `snapshot delete` | `DescribeInstances`, `DescribeVolumes`, `DescribeSnapshots`, `DeleteSnapshot` |
223
+ | `snapshot restore` | Everything in `snapshot list` **plus** `StopInstances`, `DetachVolume`, `CreateVolume`, `AttachVolume`, `StartInstances`, `CreateTags` |
224
+ | `destroy` (with existing snapshots, `-y` declined cleanup) | Adds `DescribeSnapshots` (to enumerate) and `DeleteSnapshot` (if cleanup accepted) on top of the regular `destroy` set |
225
+
226
+ > **Two AWS constraints to know.** First: `Describe*` actions can't be ARN- or tag-scoped — `Resource: "*"` is the only allowed form (read-side metadata leaks broader than mutation does). Second: snapshot ARNs are auto-generated (`snap-xxxxxx`) at create time, so prefix-on-the-ARN scoping doesn't help. Tag-based scoping is the practical path.
227
+
228
+ #### Snapshot-only policy with tag-locked mutations (attach as an inline policy to your `remo` role)
229
+
230
+ This is the recommended minimum. Reads stay broad (AWS limitation); mutations are locked to resources tagged `remo=true`, which is exactly what `remo` creates and never touches what it doesn't own.
231
+
232
+ ```json
233
+ {
234
+ "Version": "2012-10-17",
235
+ "Statement": [
236
+ {
237
+ "Sid": "CreateSnapshotRequiresRemoTag",
238
+ "Effect": "Allow",
239
+ "Action": "ec2:CreateSnapshot",
240
+ "Resource": "arn:aws:ec2:*:*:snapshot/*",
241
+ "Condition": {
242
+ "StringEquals": {"aws:RequestTag/remo": "true"}
243
+ }
244
+ },
245
+ {
246
+ "Sid": "CreateSnapshotFromAnyVolume",
247
+ "Effect": "Allow",
248
+ "Action": "ec2:CreateSnapshot",
249
+ "Resource": "arn:aws:ec2:*:*:volume/*"
250
+ },
251
+ {
252
+ "Sid": "TagSnapshotOnCreate",
253
+ "Effect": "Allow",
254
+ "Action": "ec2:CreateTags",
255
+ "Resource": "arn:aws:ec2:*:*:snapshot/*",
256
+ "Condition": {
257
+ "StringEquals": {"ec2:CreateAction": "CreateSnapshot"}
258
+ }
259
+ },
260
+ {
261
+ "Sid": "DeleteOnlyRemoSnapshots",
262
+ "Effect": "Allow",
263
+ "Action": "ec2:DeleteSnapshot",
264
+ "Resource": "arn:aws:ec2:*:*:snapshot/*",
265
+ "Condition": {
266
+ "StringEquals": {"ec2:ResourceTag/remo": "true"}
267
+ }
268
+ },
269
+ {
270
+ "Sid": "DescribeSnapshotsUnscopable",
271
+ "Effect": "Allow",
272
+ "Action": "ec2:DescribeSnapshots",
273
+ "Resource": "*"
274
+ }
275
+ ]
276
+ }
277
+ ```
278
+
279
+ What each `Sid` enforces:
280
+
281
+ | Sid | Enforces |
282
+ |---|---|
283
+ | `CreateSnapshotRequiresRemoTag` | The CreateSnapshot call must include `remo=true` in `TagSpecifications` (`remo` always does this). |
284
+ | `CreateSnapshotFromAnyVolume` | Companion — `CreateSnapshot` evaluates against both source-volume and new-snapshot ARNs; both have to be allowed. |
285
+ | `TagSnapshotOnCreate` | `CreateTags` is only allowed as part of a `CreateSnapshot` request — prevents the role from re-tagging arbitrary resources. |
286
+ | `DeleteOnlyRemoSnapshots` | Can only delete snapshots tagged `remo=true`. Manual snapshots someone took via the console with no tag stay safe. |
287
+ | `DescribeSnapshotsUnscopable` | Unavoidable — AWS doesn't allow Describe* scoping; this lets `list` work. |
288
+
289
+ For `snapshot restore`, also add `ec2:StopInstances`, `ec2:DetachVolume`, `ec2:CreateVolume`, `ec2:AttachVolume`, `ec2:StartInstances`. Those can be tag-scoped on the instance / volume too — e.g., `Condition: {"StringLike": {"ec2:ResourceTag/Name": "remo-*"}}` to constrain to `remo`-named instances.
290
+
291
+ If you want a looser starting point and don't need the tag-locking (e.g., you fully trust who can assume the role), `Resource: "*"` on `ec2:CreateSnapshot`, `ec2:DescribeSnapshots`, `ec2:DeleteSnapshot`, `ec2:CreateTags` is a one-statement minimal version.
292
+
183
293
  ### What's Created
184
294
 
185
295
  | Resource | Name Pattern | Description |
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "remo-cli"
7
- version = "2.0.0rc2"
7
+ version = "2.0.0rc4"
8
8
  description = "Remote development environment CLI"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -0,0 +1,36 @@
1
+ # Specification Quality Checklist: Provider Snapshots
2
+
3
+ **Purpose**: Validate specification completeness and quality before proceeding to planning
4
+ **Created**: 2026-05-24
5
+ **Feature**: [spec.md](../spec.md)
6
+
7
+ ## Content Quality
8
+
9
+ - [x] No implementation details (languages, frameworks, APIs)
10
+ - [x] Focused on user value and business needs
11
+ - [x] Written for non-technical stakeholders
12
+ - [x] All mandatory sections completed
13
+
14
+ ## Requirement Completeness
15
+
16
+ - [x] No [NEEDS CLARIFICATION] markers remain
17
+ - [x] Requirements are testable and unambiguous
18
+ - [x] Success criteria are measurable
19
+ - [x] Success criteria are technology-agnostic (no implementation details)
20
+ - [x] All acceptance scenarios are defined
21
+ - [x] Edge cases are identified
22
+ - [x] Scope is clearly bounded
23
+ - [x] Dependencies and assumptions identified
24
+
25
+ ## Feature Readiness
26
+
27
+ - [x] All functional requirements have clear acceptance criteria
28
+ - [x] User scenarios cover primary flows
29
+ - [x] Feature meets measurable outcomes defined in Success Criteria
30
+ - [x] No implementation details leak into specification
31
+
32
+ ## Notes
33
+
34
+ - Items marked incomplete require spec updates before `/speckit.clarify` or `/speckit.plan`
35
+ - The spec mentions provider names (Incus, Proxmox, AWS, Hetzner) and provider-specific concepts (EBS, hcloud, pct, `--vmstate`, `dir` storage). These are kept because they describe the *user-facing scope* of the feature (which backends are covered) and the *user-visible behavioral differences* (latency, cost, downtime), not implementation choices. They are not framework or library references.
36
+ - Cost numbers and rate-per-GB figures are intentionally left as "estimated" with a 10% tolerance in SC-003, so the spec doesn't pin a specific provider price that will drift over time.