remo-cli 1.0.3__tar.gz → 2.0.0__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 (230) hide show
  1. {remo_cli-1.0.3 → remo_cli-2.0.0}/.devcontainer/devcontainer.json +4 -2
  2. {remo_cli-1.0.3 → remo_cli-2.0.0}/.github/workflows/smoke-test.yml +220 -0
  3. {remo_cli-1.0.3 → remo_cli-2.0.0}/.gitignore +3 -0
  4. {remo_cli-1.0.3 → remo_cli-2.0.0}/CLAUDE.md +2 -1
  5. {remo_cli-1.0.3 → remo_cli-2.0.0}/PKG-INFO +77 -1
  6. {remo_cli-1.0.3 → remo_cli-2.0.0}/README.md +76 -0
  7. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/user_setup/tasks/main.yml +26 -2
  8. remo_cli-2.0.0/ansible/roles/user_setup/templates/project-launch.sh.j2 +147 -0
  9. {remo_cli-1.0.3 → remo_cli-2.0.0}/docs/aws.md +110 -0
  10. {remo_cli-1.0.3 → remo_cli-2.0.0}/pyproject.toml +1 -1
  11. remo_cli-2.0.0/specs/005-provider-snapshots/checklists/requirements.md +36 -0
  12. remo_cli-2.0.0/specs/005-provider-snapshots/contracts/cli-surface.md +160 -0
  13. remo_cli-2.0.0/specs/005-provider-snapshots/data-model.md +114 -0
  14. remo_cli-2.0.0/specs/005-provider-snapshots/plan.md +101 -0
  15. remo_cli-2.0.0/specs/005-provider-snapshots/quickstart.md +161 -0
  16. remo_cli-2.0.0/specs/005-provider-snapshots/research.md +132 -0
  17. remo_cli-2.0.0/specs/005-provider-snapshots/spec.md +181 -0
  18. remo_cli-2.0.0/specs/005-provider-snapshots/tasks.md +286 -0
  19. {remo_cli-1.0.3 → remo_cli-2.0.0}/src/remo_cli/cli/providers/aws.py +125 -0
  20. {remo_cli-1.0.3 → remo_cli-2.0.0}/src/remo_cli/cli/providers/hetzner.py +105 -0
  21. {remo_cli-1.0.3 → remo_cli-2.0.0}/src/remo_cli/cli/providers/incus.py +140 -2
  22. {remo_cli-1.0.3 → remo_cli-2.0.0}/src/remo_cli/cli/providers/proxmox.py +181 -2
  23. remo_cli-2.0.0/src/remo_cli/cli/shell.py +196 -0
  24. remo_cli-2.0.0/src/remo_cli/core/snapshot.py +170 -0
  25. {remo_cli-1.0.3 → remo_cli-2.0.0}/src/remo_cli/core/ssh.py +92 -15
  26. remo_cli-2.0.0/src/remo_cli/models/snapshot.py +43 -0
  27. {remo_cli-1.0.3 → remo_cli-2.0.0}/src/remo_cli/providers/aws.py +505 -0
  28. {remo_cli-1.0.3 → remo_cli-2.0.0}/src/remo_cli/providers/hetzner.py +335 -1
  29. {remo_cli-1.0.3 → remo_cli-2.0.0}/src/remo_cli/providers/incus.py +363 -3
  30. {remo_cli-1.0.3 → remo_cli-2.0.0}/src/remo_cli/providers/proxmox.py +366 -6
  31. remo_cli-2.0.0/tests/unit/cli/providers/test_aws_snapshot.py +126 -0
  32. remo_cli-2.0.0/tests/unit/cli/providers/test_hetzner_snapshot.py +129 -0
  33. remo_cli-2.0.0/tests/unit/cli/providers/test_incus_snapshot.py +212 -0
  34. remo_cli-2.0.0/tests/unit/cli/providers/test_proxmox_snapshot.py +158 -0
  35. remo_cli-2.0.0/tests/unit/cli/test_shell.py +403 -0
  36. remo_cli-2.0.0/tests/unit/core/test_snapshot.py +176 -0
  37. {remo_cli-1.0.3 → remo_cli-2.0.0}/tests/unit/core/test_ssh.py +44 -18
  38. remo_cli-2.0.0/tests/unit/providers/test_aws_snapshot.py +458 -0
  39. remo_cli-2.0.0/tests/unit/providers/test_hetzner_snapshot.py +372 -0
  40. remo_cli-2.0.0/tests/unit/providers/test_incus_snapshot.py +467 -0
  41. remo_cli-2.0.0/tests/unit/providers/test_proxmox_snapshot.py +465 -0
  42. remo_cli-2.0.0/tests/unit/test_ansible_templates.py +36 -0
  43. {remo_cli-1.0.3 → remo_cli-2.0.0}/uv.lock +1 -1
  44. remo_cli-1.0.3/src/remo_cli/cli/shell.py +0 -108
  45. remo_cli-1.0.3/tests/unit/cli/test_shell.py +0 -178
  46. {remo_cli-1.0.3 → remo_cli-2.0.0}/.claude/commands/speckit.analyze.md +0 -0
  47. {remo_cli-1.0.3 → remo_cli-2.0.0}/.claude/commands/speckit.checklist.md +0 -0
  48. {remo_cli-1.0.3 → remo_cli-2.0.0}/.claude/commands/speckit.clarify.md +0 -0
  49. {remo_cli-1.0.3 → remo_cli-2.0.0}/.claude/commands/speckit.constitution.md +0 -0
  50. {remo_cli-1.0.3 → remo_cli-2.0.0}/.claude/commands/speckit.implement.md +0 -0
  51. {remo_cli-1.0.3 → remo_cli-2.0.0}/.claude/commands/speckit.plan.md +0 -0
  52. {remo_cli-1.0.3 → remo_cli-2.0.0}/.claude/commands/speckit.specify.md +0 -0
  53. {remo_cli-1.0.3 → remo_cli-2.0.0}/.claude/commands/speckit.tasks.md +0 -0
  54. {remo_cli-1.0.3 → remo_cli-2.0.0}/.claude/commands/speckit.taskstoissues.md +0 -0
  55. {remo_cli-1.0.3 → remo_cli-2.0.0}/.dockerignore +0 -0
  56. {remo_cli-1.0.3 → remo_cli-2.0.0}/.github/dependabot.yml +0 -0
  57. {remo_cli-1.0.3 → remo_cli-2.0.0}/.github/workflows/ci.yml +0 -0
  58. {remo_cli-1.0.3 → remo_cli-2.0.0}/.github/workflows/provision.yml +0 -0
  59. {remo_cli-1.0.3 → remo_cli-2.0.0}/.github/workflows/release.yml +0 -0
  60. {remo_cli-1.0.3 → remo_cli-2.0.0}/.github/workflows/smoke-test-cleanup.yml +0 -0
  61. {remo_cli-1.0.3 → remo_cli-2.0.0}/.github/workflows/teardown.yml +0 -0
  62. {remo_cli-1.0.3 → remo_cli-2.0.0}/.maverick/checkpoints/feature/implementation_complete.json +0 -0
  63. {remo_cli-1.0.3 → remo_cli-2.0.0}/.maverick/checkpoints/feature/phase_${{ index }}_complete.json +0 -0
  64. {remo_cli-1.0.3 → remo_cli-2.0.0}/.maverick/checkpoints/feature/validation_complete.json +0 -0
  65. {remo_cli-1.0.3 → remo_cli-2.0.0}/.specify/memory/constitution.md +0 -0
  66. {remo_cli-1.0.3 → remo_cli-2.0.0}/.specify/scripts/bash/check-prerequisites.sh +0 -0
  67. {remo_cli-1.0.3 → remo_cli-2.0.0}/.specify/scripts/bash/common.sh +0 -0
  68. {remo_cli-1.0.3 → remo_cli-2.0.0}/.specify/scripts/bash/create-new-feature.sh +0 -0
  69. {remo_cli-1.0.3 → remo_cli-2.0.0}/.specify/scripts/bash/setup-plan.sh +0 -0
  70. {remo_cli-1.0.3 → remo_cli-2.0.0}/.specify/scripts/bash/update-agent-context.sh +0 -0
  71. {remo_cli-1.0.3 → remo_cli-2.0.0}/.specify/templates/agent-file-template.md +0 -0
  72. {remo_cli-1.0.3 → remo_cli-2.0.0}/.specify/templates/checklist-template.md +0 -0
  73. {remo_cli-1.0.3 → remo_cli-2.0.0}/.specify/templates/plan-template.md +0 -0
  74. {remo_cli-1.0.3 → remo_cli-2.0.0}/.specify/templates/spec-template.md +0 -0
  75. {remo_cli-1.0.3 → remo_cli-2.0.0}/.specify/templates/tasks-template.md +0 -0
  76. {remo_cli-1.0.3 → remo_cli-2.0.0}/CONTRIBUTING.md +0 -0
  77. {remo_cli-1.0.3 → remo_cli-2.0.0}/Dockerfile +0 -0
  78. {remo_cli-1.0.3 → remo_cli-2.0.0}/LICENSE +0 -0
  79. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/.ansible-lint +0 -0
  80. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/README.md +0 -0
  81. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/ansible.cfg +0 -0
  82. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/aws_configure.yml +0 -0
  83. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/aws_resize.yml +0 -0
  84. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/aws_site.yml +0 -0
  85. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/aws_teardown.yml +0 -0
  86. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/group_vars/all.yml +0 -0
  87. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/group_vars/incus_containers.yml +0 -0
  88. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/group_vars/proxmox_containers.yml +0 -0
  89. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/hetzner_configure.yml +0 -0
  90. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/hetzner_provision.yml +0 -0
  91. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/hetzner_resize.yml +0 -0
  92. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/hetzner_site.yml +0 -0
  93. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/hetzner_teardown.yml +0 -0
  94. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/incus_bootstrap.yml +0 -0
  95. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/incus_configure.yml +0 -0
  96. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/incus_provision.yml +0 -0
  97. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/incus_resize.yml +0 -0
  98. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/incus_site.yml +0 -0
  99. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/incus_teardown.yml +0 -0
  100. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/inventory/hosts.yml +0 -0
  101. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/inventory/incus_containers.yml +0 -0
  102. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/proxmox_bootstrap.yml +0 -0
  103. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/proxmox_configure.yml +0 -0
  104. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/proxmox_provision.yml +0 -0
  105. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/proxmox_resize.yml +0 -0
  106. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/proxmox_site.yml +0 -0
  107. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/proxmox_teardown.yml +0 -0
  108. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/requirements.yml +0 -0
  109. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/aws_server/defaults/main.yml +0 -0
  110. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/aws_server/tasks/ebs.yml +0 -0
  111. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/aws_server/tasks/ec2.yml +0 -0
  112. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/aws_server/tasks/main.yml +0 -0
  113. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/aws_server/tasks/patch_manager.yml +0 -0
  114. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/aws_server/tasks/preflight.yml +0 -0
  115. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/aws_server/tasks/security_group.yml +0 -0
  116. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/devcontainers/tasks/main.yml +0 -0
  117. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/docker/handlers/main.yml +0 -0
  118. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/docker/tasks/main.yml +0 -0
  119. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/fzf/tasks/main.yml +0 -0
  120. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/github_cli/tasks/main.yml +0 -0
  121. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/hetzner_server/defaults/main.yml +0 -0
  122. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/hetzner_server/tasks/main.yml +0 -0
  123. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/incus_bootstrap/defaults/main.yml +0 -0
  124. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/incus_bootstrap/handlers/main.yml +0 -0
  125. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/incus_bootstrap/tasks/main.yml +0 -0
  126. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/incus_container/defaults/main.yml +0 -0
  127. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/incus_container/handlers/main.yml +0 -0
  128. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/incus_container/meta/main.yml +0 -0
  129. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/incus_container/tasks/main.yml +0 -0
  130. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/incus_container/tasks/preflight.yml +0 -0
  131. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/incus_container_teardown/defaults/main.yml +0 -0
  132. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/incus_container_teardown/meta/main.yml +0 -0
  133. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/incus_container_teardown/tasks/main.yml +0 -0
  134. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/nodejs/defaults/main.yml +0 -0
  135. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/nodejs/tasks/main.yml +0 -0
  136. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/proxmox_bootstrap/defaults/main.yml +0 -0
  137. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/proxmox_bootstrap/meta/main.yml +0 -0
  138. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/proxmox_bootstrap/tasks/main.yml +0 -0
  139. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/proxmox_container/defaults/main.yml +0 -0
  140. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/proxmox_container/handlers/main.yml +0 -0
  141. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/proxmox_container/meta/main.yml +0 -0
  142. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/proxmox_container/tasks/main.yml +0 -0
  143. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/proxmox_container/tasks/preflight.yml +0 -0
  144. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/proxmox_container_teardown/defaults/main.yml +0 -0
  145. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/proxmox_container_teardown/meta/main.yml +0 -0
  146. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/proxmox_container_teardown/tasks/main.yml +0 -0
  147. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/user_setup/defaults/main.yml +0 -0
  148. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/user_setup/templates/devshell.sh.j2 +0 -0
  149. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/user_setup/templates/project-menu.sh.j2 +0 -0
  150. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/zellij/defaults/main.yml +0 -0
  151. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/zellij/tasks/main.yml +0 -0
  152. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/zellij/templates/config.kdl.j2 +0 -0
  153. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/roles/zellij/templates/layouts/minimal.kdl.j2 +0 -0
  154. {remo_cli-1.0.3 → remo_cli-2.0.0}/ansible/tasks/configure_dev_tools.yml +0 -0
  155. {remo_cli-1.0.3 → remo_cli-2.0.0}/docs/hetzner.md +0 -0
  156. {remo_cli-1.0.3 → remo_cli-2.0.0}/docs/incus.md +0 -0
  157. {remo_cli-1.0.3 → remo_cli-2.0.0}/docs/install.sh +0 -0
  158. {remo_cli-1.0.3 → remo_cli-2.0.0}/docs/proxmox.md +0 -0
  159. {remo_cli-1.0.3 → remo_cli-2.0.0}/install.sh +0 -0
  160. {remo_cli-1.0.3 → remo_cli-2.0.0}/maverick.yaml +0 -0
  161. {remo_cli-1.0.3 → remo_cli-2.0.0}/project-menu.sh +0 -0
  162. {remo_cli-1.0.3 → remo_cli-2.0.0}/remo +0 -0
  163. {remo_cli-1.0.3 → remo_cli-2.0.0}/run.sh +0 -0
  164. {remo_cli-1.0.3 → remo_cli-2.0.0}/scripts/setup-aws-oidc.sh +0 -0
  165. {remo_cli-1.0.3 → remo_cli-2.0.0}/specs/001-bootstrap-incus-host/checklists/requirements.md +0 -0
  166. {remo_cli-1.0.3 → remo_cli-2.0.0}/specs/001-bootstrap-incus-host/checklists/validation.md +0 -0
  167. {remo_cli-1.0.3 → remo_cli-2.0.0}/specs/001-bootstrap-incus-host/contracts/playbook-interface.yaml +0 -0
  168. {remo_cli-1.0.3 → remo_cli-2.0.0}/specs/001-bootstrap-incus-host/contracts/role-interface.yaml +0 -0
  169. {remo_cli-1.0.3 → remo_cli-2.0.0}/specs/001-bootstrap-incus-host/data-model.md +0 -0
  170. {remo_cli-1.0.3 → remo_cli-2.0.0}/specs/001-bootstrap-incus-host/plan.md +0 -0
  171. {remo_cli-1.0.3 → remo_cli-2.0.0}/specs/001-bootstrap-incus-host/quickstart.md +0 -0
  172. {remo_cli-1.0.3 → remo_cli-2.0.0}/specs/001-bootstrap-incus-host/research.md +0 -0
  173. {remo_cli-1.0.3 → remo_cli-2.0.0}/specs/001-bootstrap-incus-host/spec.md +0 -0
  174. {remo_cli-1.0.3 → remo_cli-2.0.0}/specs/001-bootstrap-incus-host/tasks.md +0 -0
  175. {remo_cli-1.0.3 → remo_cli-2.0.0}/specs/002-incus-container-support/checklists/requirements.md +0 -0
  176. {remo_cli-1.0.3 → remo_cli-2.0.0}/specs/002-incus-container-support/contracts/playbook-interface.yaml +0 -0
  177. {remo_cli-1.0.3 → remo_cli-2.0.0}/specs/002-incus-container-support/contracts/role-interface.yaml +0 -0
  178. {remo_cli-1.0.3 → remo_cli-2.0.0}/specs/002-incus-container-support/data-model.md +0 -0
  179. {remo_cli-1.0.3 → remo_cli-2.0.0}/specs/002-incus-container-support/plan.md +0 -0
  180. {remo_cli-1.0.3 → remo_cli-2.0.0}/specs/002-incus-container-support/quickstart.md +0 -0
  181. {remo_cli-1.0.3 → remo_cli-2.0.0}/specs/002-incus-container-support/research.md +0 -0
  182. {remo_cli-1.0.3 → remo_cli-2.0.0}/specs/002-incus-container-support/spec.md +0 -0
  183. {remo_cli-1.0.3 → remo_cli-2.0.0}/specs/002-incus-container-support/tasks.md +0 -0
  184. {remo_cli-1.0.3 → remo_cli-2.0.0}/specs/003-python-cli-rewrite/checklists/requirements.md +0 -0
  185. {remo_cli-1.0.3 → remo_cli-2.0.0}/specs/003-python-cli-rewrite/data-model.md +0 -0
  186. {remo_cli-1.0.3 → remo_cli-2.0.0}/specs/003-python-cli-rewrite/plan.md +0 -0
  187. {remo_cli-1.0.3 → remo_cli-2.0.0}/specs/003-python-cli-rewrite/quickstart.md +0 -0
  188. {remo_cli-1.0.3 → remo_cli-2.0.0}/specs/003-python-cli-rewrite/research.md +0 -0
  189. {remo_cli-1.0.3 → remo_cli-2.0.0}/specs/003-python-cli-rewrite/spec.md +0 -0
  190. {remo_cli-1.0.3 → remo_cli-2.0.0}/specs/003-python-cli-rewrite/tasks.md +0 -0
  191. {remo_cli-1.0.3 → remo_cli-2.0.0}/specs/004-proxmox-container-support/spec.md +0 -0
  192. {remo_cli-1.0.3 → remo_cli-2.0.0}/src/remo_cli/__init__.py +0 -0
  193. {remo_cli-1.0.3 → remo_cli-2.0.0}/src/remo_cli/__main__.py +0 -0
  194. {remo_cli-1.0.3 → remo_cli-2.0.0}/src/remo_cli/cli/__init__.py +0 -0
  195. {remo_cli-1.0.3 → remo_cli-2.0.0}/src/remo_cli/cli/cp.py +0 -0
  196. {remo_cli-1.0.3 → remo_cli-2.0.0}/src/remo_cli/cli/main.py +0 -0
  197. {remo_cli-1.0.3 → remo_cli-2.0.0}/src/remo_cli/cli/providers/__init__.py +0 -0
  198. {remo_cli-1.0.3 → remo_cli-2.0.0}/src/remo_cli/core/__init__.py +0 -0
  199. {remo_cli-1.0.3 → remo_cli-2.0.0}/src/remo_cli/core/ansible_runner.py +0 -0
  200. {remo_cli-1.0.3 → remo_cli-2.0.0}/src/remo_cli/core/completion.py +0 -0
  201. {remo_cli-1.0.3 → remo_cli-2.0.0}/src/remo_cli/core/config.py +0 -0
  202. {remo_cli-1.0.3 → remo_cli-2.0.0}/src/remo_cli/core/known_hosts.py +0 -0
  203. {remo_cli-1.0.3 → remo_cli-2.0.0}/src/remo_cli/core/output.py +0 -0
  204. {remo_cli-1.0.3 → remo_cli-2.0.0}/src/remo_cli/core/picker.py +0 -0
  205. {remo_cli-1.0.3 → remo_cli-2.0.0}/src/remo_cli/core/rsync.py +0 -0
  206. {remo_cli-1.0.3 → remo_cli-2.0.0}/src/remo_cli/core/validation.py +0 -0
  207. {remo_cli-1.0.3 → remo_cli-2.0.0}/src/remo_cli/core/version.py +0 -0
  208. {remo_cli-1.0.3 → remo_cli-2.0.0}/src/remo_cli/models/__init__.py +0 -0
  209. {remo_cli-1.0.3 → remo_cli-2.0.0}/src/remo_cli/models/host.py +0 -0
  210. {remo_cli-1.0.3 → remo_cli-2.0.0}/src/remo_cli/providers/__init__.py +0 -0
  211. {remo_cli-1.0.3 → remo_cli-2.0.0}/tests/__init__.py +0 -0
  212. {remo_cli-1.0.3 → remo_cli-2.0.0}/tests/conftest.py +0 -0
  213. {remo_cli-1.0.3 → remo_cli-2.0.0}/tests/integration/__init__.py +0 -0
  214. {remo_cli-1.0.3 → remo_cli-2.0.0}/tests/integration/orbstack-cloud-init.yml +0 -0
  215. {remo_cli-1.0.3 → remo_cli-2.0.0}/tests/integration/orbstack.sh +0 -0
  216. {remo_cli-1.0.3 → remo_cli-2.0.0}/tests/unit/__init__.py +0 -0
  217. {remo_cli-1.0.3 → remo_cli-2.0.0}/tests/unit/cli/__init__.py +0 -0
  218. {remo_cli-1.0.3 → remo_cli-2.0.0}/tests/unit/cli/test_cp.py +0 -0
  219. {remo_cli-1.0.3 → remo_cli-2.0.0}/tests/unit/cli/test_main.py +0 -0
  220. {remo_cli-1.0.3 → remo_cli-2.0.0}/tests/unit/core/__init__.py +0 -0
  221. {remo_cli-1.0.3 → remo_cli-2.0.0}/tests/unit/core/test_ansible_runner.py +0 -0
  222. {remo_cli-1.0.3 → remo_cli-2.0.0}/tests/unit/core/test_config.py +0 -0
  223. {remo_cli-1.0.3 → remo_cli-2.0.0}/tests/unit/core/test_known_hosts.py +0 -0
  224. {remo_cli-1.0.3 → remo_cli-2.0.0}/tests/unit/core/test_output.py +0 -0
  225. {remo_cli-1.0.3 → remo_cli-2.0.0}/tests/unit/core/test_picker.py +0 -0
  226. {remo_cli-1.0.3 → remo_cli-2.0.0}/tests/unit/core/test_rsync.py +0 -0
  227. {remo_cli-1.0.3 → remo_cli-2.0.0}/tests/unit/core/test_validation.py +0 -0
  228. {remo_cli-1.0.3 → remo_cli-2.0.0}/tests/unit/core/test_version.py +0 -0
  229. {remo_cli-1.0.3 → remo_cli-2.0.0}/tests/unit/providers/__init__.py +0 -0
  230. {remo_cli-1.0.3 → remo_cli-2.0.0}/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": {},
@@ -40,7 +40,9 @@
40
40
  "source=${localEnv:HOME}/.config/gh,target=/home/vscode/.config/gh,type=bind,consistency=cached",
41
41
  "source=${localEnv:HOME}/.claude,target=/home/vscode/.claude,type=bind,consistency=cached",
42
42
  "source=${localEnv:HOME}/.aws,target=/home/vscode/.aws,type=bind,consistency=cached",
43
- "source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached"
43
+ "source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached",
44
+ "source=${localWorkspaceFolder}/../site,target=/workspaces/site,type=bind,consistency=cached",
45
+ "source=${localWorkspaceFolder}/../remo-broker,target=/workspaces/remo-broker,type=bind,consistency=cached"
44
46
  ],
45
47
  "postCreateCommand": "python3 --version && node --version"
46
48
  }
@@ -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)
@@ -628,6 +803,51 @@ jobs:
628
803
  $SSH_CMD "pkill -f 'python3 -m http.server 9090'" 2>/dev/null || true
629
804
  $SSH_CMD "pkill -f 'python3 -m http.server 3000'" 2>/dev/null || true
630
805
 
806
+ - name: Test remo shell -p / --exec / --detach (project-launch passthrough)
807
+ run: |
808
+ SSH_CMD="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
809
+ -o ConnectTimeout=30 -o BatchMode=yes -i ~/.ssh/id_rsa remo@$HOST"
810
+
811
+ # Plant a project dir without a .devcontainer so we exercise the
812
+ # host-side branches of project-launch (no docker dance needed in CI).
813
+ $SSH_CMD "mkdir -p ~/projects/smoke-proj && \
814
+ echo 'placeholder' > ~/projects/smoke-proj/README"
815
+
816
+ # --- Foreground --exec on a no-devcontainer project (Case 4) ---
817
+ OUT=$(uv run remo shell -p smoke-proj --exec 'echo hello-$REMO_PROJECT-$REMO_INSTANCE && pwd' 2>&1)
818
+ echo "$OUT"
819
+ echo "$OUT" | grep -q "hello-smoke-proj-" || {
820
+ echo "FAIL: --exec did not run / env vars missing"; exit 1; }
821
+ echo "$OUT" | grep -q "/projects/smoke-proj" || {
822
+ echo "FAIL: --exec did not cd into project dir"; exit 1; }
823
+ echo "remo shell -p --exec (foreground): PASS"
824
+
825
+ # --- Detached --exec writes log file and returns immediately ---
826
+ DETACH_OUT=$(uv run remo shell -p smoke-proj --detach \
827
+ --exec 'sleep 1 && echo detached-marker-$REMO_PROJECT' 2>&1)
828
+ echo "$DETACH_OUT"
829
+ echo "$DETACH_OUT" | grep -q "Launched detached" || {
830
+ echo "FAIL: detach didn't print launched message"; exit 1; }
831
+ # Give the background command a moment to finish
832
+ sleep 3
833
+ LOG=$($SSH_CMD "cat ~/.local/state/remo/smoke-proj.log")
834
+ echo "log contents:"; echo "$LOG"
835
+ echo "$LOG" | grep -q "detached-marker-smoke-proj" || {
836
+ echo "FAIL: detached command did not run / log missing marker"; exit 1; }
837
+ echo "remo shell -p --detach --exec: PASS"
838
+
839
+ # --- Validation: --exec without -p exits 2 ---
840
+ if uv run remo shell --exec 'true' 2>/dev/null; then
841
+ echo "FAIL: --exec without -p should have errored"; exit 1
842
+ fi
843
+ echo "validation: --exec without -p errors as expected"
844
+
845
+ # --- Validation: --detach without --exec exits 2 ---
846
+ if uv run remo shell -p smoke-proj --detach 2>/dev/null; then
847
+ echo "FAIL: --detach without --exec should have errored"; exit 1
848
+ fi
849
+ echo "validation: --detach without --exec errors as expected"
850
+
631
851
  - name: Teardown
632
852
  if: always()
633
853
  run: |
@@ -41,6 +41,9 @@ env/
41
41
  .vscode/
42
42
  *.sublime-*
43
43
 
44
+ # macOS
45
+ .DS_Store
46
+
44
47
  # Maverick workflow state (ignore everything except checkpoints)
45
48
  .maverick/*
46
49
  !.maverick/checkpoints/
@@ -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: 1.0.3
3
+ Version: 2.0.0
4
4
  Summary: Remote development environment CLI
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -113,6 +113,42 @@ The `fzf`-powered menu shows your projects from `~/projects`:
113
113
  - **c**: Clone a new repository
114
114
  - **x**: Exit to shell
115
115
 
116
+ ### Jump Straight to a Project
117
+
118
+ Skip the menu and land directly in a project (devcontainer auto-launches):
119
+
120
+ ```bash
121
+ remo shell -p my-app
122
+ ```
123
+
124
+ Run a one-shot command inside the project's devcontainer instead of opening
125
+ a shell — quote the command as a single string:
126
+
127
+ ```bash
128
+ remo shell -p my-app --exec 'pytest -x'
129
+ remo shell -p my-app --exec 'claude --remote-control'
130
+ ```
131
+
132
+ Fire-and-forget — kick off a command on the remote and exit SSH immediately:
133
+
134
+ ```bash
135
+ remo shell -p my-app --detach --exec 'claude remote-control --name remo-rc'
136
+ remo shell -p my-app --detach --exec './long-build.sh'
137
+ ```
138
+
139
+ Detached output is captured to `~/.local/state/remo/<project>.log` on the
140
+ remote, so you can tail it later (`remo shell -p my-app --exec 'tail -f
141
+ ~/.local/state/remo/my-app.log'`). The command's environment gets
142
+ `REMO_INSTANCE` and `REMO_PROJECT` exported automatically — handy for
143
+ deterministic naming, e.g.:
144
+
145
+ ```bash
146
+ remo shell -p my-app --detach --exec \
147
+ 'claude remote-control --name "remo-$REMO_INSTANCE-$REMO_PROJECT"'
148
+ ```
149
+
150
+ Then on your phone, open claude.ai/code and pick the session by name.
151
+
116
152
  ### Port Forwarding
117
153
 
118
154
  Forward remote ports to your local machine during SSH sessions:
@@ -160,12 +196,43 @@ Every remo environment includes:
160
196
 
161
197
  ---
162
198
 
199
+ ## Snapshots
200
+
201
+ Capture a point-in-time copy of an instance before a risky change, then roll
202
+ back if it breaks. Available on every provider, with the same command surface:
203
+
204
+ ```bash
205
+ remo <provider> snapshot create <instance> [--name NAME] [--description TEXT]
206
+ remo <provider> snapshot list [INSTANCE]
207
+ remo <provider> snapshot restore <instance> <snapshot> [-y]
208
+ remo <provider> snapshot delete <instance> <snapshot> [-y]
209
+ ```
210
+
211
+ `--name` defaults to `remo-YYYYMMDD-HHMMSS`. `-y` / `--yes` bypasses the
212
+ confirm prompt on destructive operations.
213
+
214
+ | Provider | Create | Restore | Notes |
215
+ |---|---|---|---|
216
+ | **Incus** | seconds | in-place rollback (container stopped briefly) | Free; uses native `incus snapshot`. |
217
+ | **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. |
218
+ | **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. |
219
+ | **Hetzner** | async — several minutes | server rebuild from the snapshot image, in-place. Typically 1-2 min downtime. | Costs € per GB-month. |
220
+
221
+ `remo` does not estimate storage cost — check your provider's billing console.
222
+
223
+ The `destroy` command on each provider checks for existing snapshots first and
224
+ offers to clean them up. Decline and the snapshots remain (you'll be warned
225
+ they continue to incur storage costs on AWS/Hetzner).
226
+
163
227
  ## CLI Reference
164
228
 
165
229
  ```bash
166
230
  # Connect to environment
167
231
  remo shell # Auto-connect (or picker if multiple)
168
232
  remo shell my-env # Connect to a specific environment
233
+ remo shell -p my-app # Skip the menu, jump to ~/projects/my-app
234
+ remo shell -p my-app --exec 'pytest -x' # Run command in devcontainer
235
+ remo shell -p my-app --detach --exec 'claude remote-control --name rc' # Fire and exit
169
236
  remo shell -L 8080 # Shell + forward remote :8080 to local :8080
170
237
  remo shell -L 9000:8080 # Shell + forward remote :8080 to local :9000
171
238
  remo shell -L 8080 -L 3000 # Shell + forward multiple ports
@@ -223,6 +290,15 @@ remo proxmox update --name <n> --volume-size 40 --cores 4 --memory 4096
223
290
  remo proxmox destroy --name <n> [--yes] [--purge] # Destroy container
224
291
  remo proxmox bootstrap --host <node> # Verify node + download LXC template
225
292
 
293
+ # Snapshots (all four providers)
294
+ remo <provider> snapshot create <instance> # Auto-named
295
+ remo <provider> snapshot create <instance> --name pre-x --description "before upgrade"
296
+ remo <provider> snapshot list # All instances
297
+ remo <provider> snapshot list <instance> # One instance
298
+ remo <provider> snapshot restore <instance> <snap-name> [-y] # In-place rollback
299
+ remo <provider> snapshot delete <instance> <snap-name> [-y] # Remove
300
+ # `<provider> destroy` will list existing snapshots and offer to clean them up first.
301
+
226
302
  # Updates
227
303
  uv tool upgrade remo-cli # Update CLI to latest version
228
304
  remo <platform> update # Update dev tools on remote
@@ -94,6 +94,42 @@ The `fzf`-powered menu shows your projects from `~/projects`:
94
94
  - **c**: Clone a new repository
95
95
  - **x**: Exit to shell
96
96
 
97
+ ### Jump Straight to a Project
98
+
99
+ Skip the menu and land directly in a project (devcontainer auto-launches):
100
+
101
+ ```bash
102
+ remo shell -p my-app
103
+ ```
104
+
105
+ Run a one-shot command inside the project's devcontainer instead of opening
106
+ a shell — quote the command as a single string:
107
+
108
+ ```bash
109
+ remo shell -p my-app --exec 'pytest -x'
110
+ remo shell -p my-app --exec 'claude --remote-control'
111
+ ```
112
+
113
+ Fire-and-forget — kick off a command on the remote and exit SSH immediately:
114
+
115
+ ```bash
116
+ remo shell -p my-app --detach --exec 'claude remote-control --name remo-rc'
117
+ remo shell -p my-app --detach --exec './long-build.sh'
118
+ ```
119
+
120
+ Detached output is captured to `~/.local/state/remo/<project>.log` on the
121
+ remote, so you can tail it later (`remo shell -p my-app --exec 'tail -f
122
+ ~/.local/state/remo/my-app.log'`). The command's environment gets
123
+ `REMO_INSTANCE` and `REMO_PROJECT` exported automatically — handy for
124
+ deterministic naming, e.g.:
125
+
126
+ ```bash
127
+ remo shell -p my-app --detach --exec \
128
+ 'claude remote-control --name "remo-$REMO_INSTANCE-$REMO_PROJECT"'
129
+ ```
130
+
131
+ Then on your phone, open claude.ai/code and pick the session by name.
132
+
97
133
  ### Port Forwarding
98
134
 
99
135
  Forward remote ports to your local machine during SSH sessions:
@@ -141,12 +177,43 @@ Every remo environment includes:
141
177
 
142
178
  ---
143
179
 
180
+ ## Snapshots
181
+
182
+ Capture a point-in-time copy of an instance before a risky change, then roll
183
+ back if it breaks. Available on every provider, with the same command surface:
184
+
185
+ ```bash
186
+ remo <provider> snapshot create <instance> [--name NAME] [--description TEXT]
187
+ remo <provider> snapshot list [INSTANCE]
188
+ remo <provider> snapshot restore <instance> <snapshot> [-y]
189
+ remo <provider> snapshot delete <instance> <snapshot> [-y]
190
+ ```
191
+
192
+ `--name` defaults to `remo-YYYYMMDD-HHMMSS`. `-y` / `--yes` bypasses the
193
+ confirm prompt on destructive operations.
194
+
195
+ | Provider | Create | Restore | Notes |
196
+ |---|---|---|---|
197
+ | **Incus** | seconds | in-place rollback (container stopped briefly) | Free; uses native `incus snapshot`. |
198
+ | **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. |
199
+ | **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. |
200
+ | **Hetzner** | async — several minutes | server rebuild from the snapshot image, in-place. Typically 1-2 min downtime. | Costs € per GB-month. |
201
+
202
+ `remo` does not estimate storage cost — check your provider's billing console.
203
+
204
+ The `destroy` command on each provider checks for existing snapshots first and
205
+ offers to clean them up. Decline and the snapshots remain (you'll be warned
206
+ they continue to incur storage costs on AWS/Hetzner).
207
+
144
208
  ## CLI Reference
145
209
 
146
210
  ```bash
147
211
  # Connect to environment
148
212
  remo shell # Auto-connect (or picker if multiple)
149
213
  remo shell my-env # Connect to a specific environment
214
+ remo shell -p my-app # Skip the menu, jump to ~/projects/my-app
215
+ remo shell -p my-app --exec 'pytest -x' # Run command in devcontainer
216
+ remo shell -p my-app --detach --exec 'claude remote-control --name rc' # Fire and exit
150
217
  remo shell -L 8080 # Shell + forward remote :8080 to local :8080
151
218
  remo shell -L 9000:8080 # Shell + forward remote :8080 to local :9000
152
219
  remo shell -L 8080 -L 3000 # Shell + forward multiple ports
@@ -204,6 +271,15 @@ remo proxmox update --name <n> --volume-size 40 --cores 4 --memory 4096
204
271
  remo proxmox destroy --name <n> [--yes] [--purge] # Destroy container
205
272
  remo proxmox bootstrap --host <node> # Verify node + download LXC template
206
273
 
274
+ # Snapshots (all four providers)
275
+ remo <provider> snapshot create <instance> # Auto-named
276
+ remo <provider> snapshot create <instance> --name pre-x --description "before upgrade"
277
+ remo <provider> snapshot list # All instances
278
+ remo <provider> snapshot list <instance> # One instance
279
+ remo <provider> snapshot restore <instance> <snap-name> [-y] # In-place rollback
280
+ remo <provider> snapshot delete <instance> <snap-name> [-y] # Remove
281
+ # `<provider> destroy` will list existing snapshots and offer to clean them up first.
282
+
207
283
  # Updates
208
284
  uv tool upgrade remo-cli # Update CLI to latest version
209
285
  remo <platform> update # Update dev tools on remote
@@ -330,13 +330,29 @@
330
330
  echo "Entering devcontainer (exit to return to host shell)..."
331
331
  echo ""
332
332
 
333
+ # If REMO_DEVCONTAINER_CMD is set (project-launch passthrough),
334
+ # run that command inside the devcontainer instead of dropping
335
+ # into the user's shell. Wrapped in `bash -lc` so the user's
336
+ # command gets variable expansion, pipes, &&, and PATH from
337
+ # the devcontainer's login profile.
338
+ if [[ -n "$REMO_DEVCONTAINER_CMD" ]]; then
339
+ _bash_flags="-lc"
340
+ _exec_cmd="$REMO_DEVCONTAINER_CMD"
341
+ else
342
+ _bash_flags="-c"
343
+ _exec_cmd='if command -v zsh &> /dev/null; then exec zsh; else exec bash; fi'
344
+ fi
345
+
333
346
  # Run devcontainer shell (not exec, so we can stop container after)
334
- if ! devcontainer exec --workspace-folder "$_project_dir" /bin/bash -c \
335
- 'if command -v zsh &> /dev/null; then exec zsh; else exec bash; fi'; then
347
+ if ! devcontainer exec --workspace-folder "$_project_dir" \
348
+ env REMO_INSTANCE="${REMO_INSTANCE:-$(hostname)}" \
349
+ REMO_PROJECT="$ZELLIJ_SESSION_NAME" \
350
+ /bin/bash $_bash_flags "$_exec_cmd"; then
336
351
  echo ""
337
352
  echo "devcontainer exec failed. [Press any key to continue]"
338
353
  read -n 1 -s -r
339
354
  fi
355
+ unset _exec_cmd _bash_flags
340
356
 
341
357
  # After exiting devcontainer, stop the container to free resources
342
358
  echo ""
@@ -393,6 +409,14 @@
393
409
  group: "{{ remo_user }}"
394
410
  mode: '0755'
395
411
 
412
+ - name: Install project-launch script
413
+ ansible.builtin.template:
414
+ src: project-launch.sh.j2
415
+ dest: "/home/{{ remo_user }}/.local/bin/project-launch"
416
+ owner: "{{ remo_user }}"
417
+ group: "{{ remo_user }}"
418
+ mode: '0755'
419
+
396
420
  - name: Verify remo user exists
397
421
  ansible.builtin.command: id {{ remo_user }}
398
422
  register: user_setup_user_info