portacode 1.4.15.dev0__tar.gz → 1.4.15.dev2__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 (383) hide show
  1. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/PKG-INFO +1 -1
  2. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/_version.py +2 -2
  3. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +87 -0
  4. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/proxmox_infra.py +331 -0
  5. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode.egg-info/PKG-INFO +1 -1
  6. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/.claude/agents/communication-manager.md +0 -0
  7. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/.claude/settings.local.json +0 -0
  8. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/.gitignore +0 -0
  9. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/.gitmodules +0 -0
  10. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/LICENSE +0 -0
  11. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/MANIFEST.in +0 -0
  12. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/Makefile +0 -0
  13. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/README.md +0 -0
  14. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/backup.sh +0 -0
  15. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/connect.py +0 -0
  16. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/connect.sh +0 -0
  17. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/docker-compose.yaml +0 -0
  18. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/docs/images/device-transfer-button.png +0 -0
  19. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/docs/images/device-transfer-modal.png +0 -0
  20. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/docs/images/pair-device-button.png +0 -0
  21. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/docs/images/pairing-request.png +0 -0
  22. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/docs/images/student-workspace.png +0 -0
  23. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/README.md +0 -0
  24. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/simple_device/Dockerfile +0 -0
  25. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/simple_device/README.md +0 -0
  26. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/simple_device/data/device-01/.local/share/portacode/keys/id_portacode +0 -0
  27. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/simple_device/data/device-01/.local/share/portacode/keys/id_portacode.pub +0 -0
  28. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/simple_device/docker-compose.yaml +0 -0
  29. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/Dockerfile +0 -0
  30. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/README.md +0 -0
  31. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/.local/share/portacode/keys/id_portacode +0 -0
  32. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/.local/share/portacode/keys/id_portacode.pub +0 -0
  33. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/.local/share/portacode/run/gateway.pid +0 -0
  34. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/.gitignore +0 -0
  35. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/README.md +0 -0
  36. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/db.sqlite3 +0 -0
  37. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__init__.py +0 -0
  38. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/__init__.cpython-311.pyc +0 -0
  39. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/settings.cpython-311.pyc +0 -0
  40. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/urls.cpython-311.pyc +0 -0
  41. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/wsgi.cpython-311.pyc +0 -0
  42. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/asgi.py +0 -0
  43. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/settings.py +0 -0
  44. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/urls.py +0 -0
  45. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/wsgi.py +0 -0
  46. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/manage.py +0 -0
  47. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/requirements.txt +0 -0
  48. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/templates/treats/home.html +0 -0
  49. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/__init__.py +0 -0
  50. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/__init__.cpython-311.pyc +0 -0
  51. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/admin.cpython-311.pyc +0 -0
  52. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/apps.cpython-311.pyc +0 -0
  53. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/menu.cpython-311.pyc +0 -0
  54. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/models.cpython-311.pyc +0 -0
  55. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/urls.cpython-311.pyc +0 -0
  56. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/views.cpython-311.pyc +0 -0
  57. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/admin.py +0 -0
  58. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/apps.py +0 -0
  59. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/menu.py +0 -0
  60. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/migrations/__init__.py +0 -0
  61. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/migrations/__pycache__/__init__.cpython-311.pyc +0 -0
  62. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/models.py +0 -0
  63. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/tests.py +0 -0
  64. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/urls.py +0 -0
  65. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/views.py +0 -0
  66. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/.local/share/portacode/keys/id_portacode +0 -0
  67. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/.local/share/portacode/keys/id_portacode.pub +0 -0
  68. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/README.md +0 -0
  69. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/__init__.py +0 -0
  70. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/asgi.py +0 -0
  71. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/settings.py +0 -0
  72. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/urls.py +0 -0
  73. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/wsgi.py +0 -0
  74. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/manage.py +0 -0
  75. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/requirements.txt +0 -0
  76. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/templates/treats/home.html +0 -0
  77. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/__init__.py +0 -0
  78. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/admin.py +0 -0
  79. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/apps.py +0 -0
  80. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/menu.py +0 -0
  81. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/migrations/__init__.py +0 -0
  82. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/models.py +0 -0
  83. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/tests.py +0 -0
  84. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/urls.py +0 -0
  85. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/views.py +0 -0
  86. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/.local/share/portacode/keys/id_portacode +0 -0
  87. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/.local/share/portacode/keys/id_portacode.pub +0 -0
  88. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/README.md +0 -0
  89. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/__init__.py +0 -0
  90. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/asgi.py +0 -0
  91. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/settings.py +0 -0
  92. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/urls.py +0 -0
  93. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/wsgi.py +0 -0
  94. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/manage.py +0 -0
  95. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/requirements.txt +0 -0
  96. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/templates/treats/home.html +0 -0
  97. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/__init__.py +0 -0
  98. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/admin.py +0 -0
  99. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/apps.py +0 -0
  100. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/menu.py +0 -0
  101. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/migrations/__init__.py +0 -0
  102. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/models.py +0 -0
  103. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/tests.py +0 -0
  104. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/urls.py +0 -0
  105. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/views.py +0 -0
  106. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/.local/share/portacode/keys/id_portacode +0 -0
  107. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/.local/share/portacode/keys/id_portacode.pub +0 -0
  108. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/README.md +0 -0
  109. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/__init__.py +0 -0
  110. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/asgi.py +0 -0
  111. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/settings.py +0 -0
  112. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/urls.py +0 -0
  113. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/wsgi.py +0 -0
  114. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/manage.py +0 -0
  115. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/requirements.txt +0 -0
  116. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/templates/treats/home.html +0 -0
  117. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/__init__.py +0 -0
  118. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/admin.py +0 -0
  119. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/apps.py +0 -0
  120. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/menu.py +0 -0
  121. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/migrations/__init__.py +0 -0
  122. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/models.py +0 -0
  123. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/tests.py +0 -0
  124. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/urls.py +0 -0
  125. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/views.py +0 -0
  126. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/.local/share/portacode/keys/id_portacode +0 -0
  127. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/.local/share/portacode/keys/id_portacode.pub +0 -0
  128. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/README.md +0 -0
  129. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/__init__.py +0 -0
  130. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/asgi.py +0 -0
  131. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/settings.py +0 -0
  132. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/urls.py +0 -0
  133. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/wsgi.py +0 -0
  134. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/manage.py +0 -0
  135. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/requirements.txt +0 -0
  136. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/templates/treats/home.html +0 -0
  137. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/__init__.py +0 -0
  138. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/admin.py +0 -0
  139. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/apps.py +0 -0
  140. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/menu.py +0 -0
  141. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/migrations/__init__.py +0 -0
  142. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/models.py +0 -0
  143. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/tests.py +0 -0
  144. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/urls.py +0 -0
  145. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/views.py +0 -0
  146. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/.local/share/portacode/keys/id_portacode +0 -0
  147. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/.local/share/portacode/keys/id_portacode.pub +0 -0
  148. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/README.md +0 -0
  149. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/__init__.py +0 -0
  150. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/asgi.py +0 -0
  151. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/settings.py +0 -0
  152. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/urls.py +0 -0
  153. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/wsgi.py +0 -0
  154. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/manage.py +0 -0
  155. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/requirements.txt +0 -0
  156. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/templates/treats/home.html +0 -0
  157. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/__init__.py +0 -0
  158. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/admin.py +0 -0
  159. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/apps.py +0 -0
  160. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/menu.py +0 -0
  161. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/migrations/__init__.py +0 -0
  162. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/models.py +0 -0
  163. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/tests.py +0 -0
  164. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/urls.py +0 -0
  165. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/views.py +0 -0
  166. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/.local/share/portacode/keys/id_portacode +0 -0
  167. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/.local/share/portacode/keys/id_portacode.pub +0 -0
  168. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/README.md +0 -0
  169. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/__init__.py +0 -0
  170. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/asgi.py +0 -0
  171. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/settings.py +0 -0
  172. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/urls.py +0 -0
  173. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/wsgi.py +0 -0
  174. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/manage.py +0 -0
  175. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/requirements.txt +0 -0
  176. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/templates/treats/home.html +0 -0
  177. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/__init__.py +0 -0
  178. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/admin.py +0 -0
  179. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/apps.py +0 -0
  180. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/menu.py +0 -0
  181. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/migrations/__init__.py +0 -0
  182. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/models.py +0 -0
  183. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/tests.py +0 -0
  184. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/urls.py +0 -0
  185. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/views.py +0 -0
  186. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/.local/share/portacode/keys/id_portacode +0 -0
  187. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/.local/share/portacode/keys/id_portacode.pub +0 -0
  188. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/README.md +0 -0
  189. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/__init__.py +0 -0
  190. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/asgi.py +0 -0
  191. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/settings.py +0 -0
  192. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/urls.py +0 -0
  193. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/wsgi.py +0 -0
  194. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/manage.py +0 -0
  195. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/requirements.txt +0 -0
  196. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/templates/treats/home.html +0 -0
  197. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/__init__.py +0 -0
  198. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/admin.py +0 -0
  199. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/apps.py +0 -0
  200. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/menu.py +0 -0
  201. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/migrations/__init__.py +0 -0
  202. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/models.py +0 -0
  203. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/tests.py +0 -0
  204. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/urls.py +0 -0
  205. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/views.py +0 -0
  206. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/.local/share/portacode/keys/id_portacode +0 -0
  207. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/.local/share/portacode/keys/id_portacode.pub +0 -0
  208. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/README.md +0 -0
  209. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/__init__.py +0 -0
  210. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/asgi.py +0 -0
  211. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/settings.py +0 -0
  212. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/urls.py +0 -0
  213. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/wsgi.py +0 -0
  214. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/manage.py +0 -0
  215. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/requirements.txt +0 -0
  216. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/templates/treats/home.html +0 -0
  217. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/__init__.py +0 -0
  218. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/admin.py +0 -0
  219. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/apps.py +0 -0
  220. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/menu.py +0 -0
  221. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/migrations/__init__.py +0 -0
  222. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/models.py +0 -0
  223. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/tests.py +0 -0
  224. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/urls.py +0 -0
  225. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/views.py +0 -0
  226. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/.local/share/portacode/keys/id_portacode +0 -0
  227. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/.local/share/portacode/keys/id_portacode.pub +0 -0
  228. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/README.md +0 -0
  229. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/__init__.py +0 -0
  230. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/asgi.py +0 -0
  231. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/settings.py +0 -0
  232. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/urls.py +0 -0
  233. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/wsgi.py +0 -0
  234. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/manage.py +0 -0
  235. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/requirements.txt +0 -0
  236. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/templates/treats/home.html +0 -0
  237. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/__init__.py +0 -0
  238. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/admin.py +0 -0
  239. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/apps.py +0 -0
  240. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/menu.py +0 -0
  241. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/migrations/__init__.py +0 -0
  242. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/models.py +0 -0
  243. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/tests.py +0 -0
  244. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/urls.py +0 -0
  245. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/views.py +0 -0
  246. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/docker-compose.yaml +0 -0
  247. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/README.md +0 -0
  248. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/galactic_bakeshop/__init__.py +0 -0
  249. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/galactic_bakeshop/asgi.py +0 -0
  250. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/galactic_bakeshop/settings.py +0 -0
  251. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/galactic_bakeshop/urls.py +0 -0
  252. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/galactic_bakeshop/wsgi.py +0 -0
  253. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/manage.py +0 -0
  254. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/requirements.txt +0 -0
  255. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/templates/treats/home.html +0 -0
  256. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/treats/__init__.py +0 -0
  257. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/treats/admin.py +0 -0
  258. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/treats/apps.py +0 -0
  259. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/treats/menu.py +0 -0
  260. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/treats/migrations/__init__.py +0 -0
  261. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/treats/models.py +0 -0
  262. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/treats/tests.py +0 -0
  263. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/treats/urls.py +0 -0
  264. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/treats/views.py +0 -0
  265. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/instructions/WELCOME.md +0 -0
  266. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/README.md +0 -0
  267. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/__init__.py +0 -0
  268. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/__main__.py +0 -0
  269. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/cli.py +0 -0
  270. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/README.md +0 -0
  271. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/__init__.py +0 -0
  272. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/client.py +0 -0
  273. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/README.md +0 -0
  274. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/__init__.py +0 -0
  275. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/base.py +0 -0
  276. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/chunked_content.py +0 -0
  277. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/diff_handlers.py +0 -0
  278. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/file_handlers.py +0 -0
  279. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/project_aware_file_handlers.py +0 -0
  280. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/project_state/README.md +0 -0
  281. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/project_state/__init__.py +0 -0
  282. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/project_state/file_system_watcher.py +0 -0
  283. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/project_state/git_manager.py +0 -0
  284. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/project_state/handlers.py +0 -0
  285. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/project_state/manager.py +0 -0
  286. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/project_state/models.py +0 -0
  287. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/project_state/utils.py +0 -0
  288. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/project_state_handlers.py +0 -0
  289. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/registry.py +0 -0
  290. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/session.py +0 -0
  291. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/system_handlers.py +0 -0
  292. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/tab_factory.py +0 -0
  293. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/terminal_handlers.py +0 -0
  294. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/test_proxmox_infra.py +0 -0
  295. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/update_handler.py +0 -0
  296. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/multiplex.py +0 -0
  297. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/terminal.py +0 -0
  298. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/data.py +0 -0
  299. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/keypair.py +0 -0
  300. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/__init__.py +0 -0
  301. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/__pycache__/__init__.cpython-311.pyc +0 -0
  302. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-311.pyc +0 -0
  303. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/elinks +0 -0
  304. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/gio-open +0 -0
  305. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/gnome-open +0 -0
  306. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/gvfs-open +0 -0
  307. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/kde-open +0 -0
  308. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/kfmclient +0 -0
  309. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/link_capture_exec.sh +0 -0
  310. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/link_capture_wrapper.py +0 -0
  311. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/links +0 -0
  312. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/links2 +0 -0
  313. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/lynx +0 -0
  314. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/mate-open +0 -0
  315. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/netsurf +0 -0
  316. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/sensible-browser +0 -0
  317. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/w3m +0 -0
  318. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/x-www-browser +0 -0
  319. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/xdg-open +0 -0
  320. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/logging_categories.py +0 -0
  321. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/pairing.py +0 -0
  322. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/service.py +0 -0
  323. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/static/js/test-ntp-clock.html +0 -0
  324. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/static/js/utils/ntp-clock.js +0 -0
  325. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/utils/NTP_ARCHITECTURE.md +0 -0
  326. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/utils/__init__.py +0 -0
  327. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/utils/diff_apply.py +0 -0
  328. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/utils/diff_renderer.py +0 -0
  329. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/utils/ntp_clock.py +0 -0
  330. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode.egg-info/SOURCES.txt +0 -0
  331. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode.egg-info/dependency_links.txt +0 -0
  332. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode.egg-info/entry_points.txt +0 -0
  333. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode.egg-info/requires.txt +0 -0
  334. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode.egg-info/top_level.txt +0 -0
  335. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/pyproject.toml +0 -0
  336. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/restore.sh +0 -0
  337. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/run_tests.py +0 -0
  338. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/setup.cfg +0 -0
  339. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/setup.py +0 -0
  340. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/test.sh +0 -0
  341. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/test_modules/README.md +0 -0
  342. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/test_modules/__init__.py +0 -0
  343. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/test_modules/test_device_online.py +0 -0
  344. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/test_modules/test_file_operations.py +0 -0
  345. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/test_modules/test_git_status_ui.py +0 -0
  346. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/test_modules/test_login_flow.py +0 -0
  347. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/test_modules/test_navigate_testing_folder.py +0 -0
  348. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/test_modules/test_play_store_screenshots.py +0 -0
  349. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/test_modules/test_terminal_buffer_performance.py +0 -0
  350. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/test_modules/test_terminal_interaction.py +0 -0
  351. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/test_modules/test_terminal_loading_race_condition.py +0 -0
  352. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/test_modules/test_terminal_start.py +0 -0
  353. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/test_request_id.py +0 -0
  354. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/testing_framework/.env.example +0 -0
  355. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/testing_framework/README.md +0 -0
  356. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/testing_framework/__init__.py +0 -0
  357. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/testing_framework/cli.py +0 -0
  358. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/testing_framework/core/__init__.py +0 -0
  359. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/testing_framework/core/base_test.py +0 -0
  360. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/testing_framework/core/cli_manager.py +0 -0
  361. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/testing_framework/core/hierarchical_runner.py +0 -0
  362. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/testing_framework/core/playwright_manager.py +0 -0
  363. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/testing_framework/core/runner.py +0 -0
  364. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/testing_framework/core/shared_cli_manager.py +0 -0
  365. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/testing_framework/core/test_discovery.py +0 -0
  366. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/testing_framework/requirements.txt +0 -0
  367. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/todo/UI_UX/opening_a_file_on_desktop_results_in_nothing.md +0 -0
  368. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/todo/agent_context_management.md +0 -0
  369. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/todo/django_server_time_sync.md +0 -0
  370. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/todo/issues/device_performance_degradation.md +0 -0
  371. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/todo/issues/git_data_not_captured_in_proxmox.md +0 -0
  372. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/todo/issues/indefinite_resource_loading.md +0 -0
  373. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/todo/issues/portacode_service_silently_down.md +0 -0
  374. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/todo/issues/premature_terminal_exit.md +0 -0
  375. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/todo/issues/project_cpu_hotspots.md +0 -0
  376. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/todo/issues/terminals_exit_upon_starting.md +0 -0
  377. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/todo/issues/wrong_item_classification_on_client_side.md +0 -0
  378. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/todo/smartphone_terminal_input_frustrations.md +0 -0
  379. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/tools/generate_play_store_assets.py +0 -0
  380. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/tools/pairing_tester.py +0 -0
  381. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/tools/run_screenshot_suite.sh +0 -0
  382. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/tools/test_python_ntp_clock.py +0 -0
  383. {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/validate.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: portacode
3
- Version: 1.4.15.dev0
3
+ Version: 1.4.15.dev2
4
4
  Summary: Portacode CLI client and SDK
5
5
  Home-page: https://github.com/portacode/portacode
6
6
  Author: Meena Erian
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '1.4.15.dev0'
32
- __version_tuple__ = version_tuple = (1, 4, 15, 'dev0')
31
+ __version__ = version = '1.4.15.dev2'
32
+ __version_tuple__ = version_tuple = (1, 4, 15, 'dev2')
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -331,6 +331,8 @@ Configures a Proxmox node for Portacode infrastructure usage (API token validati
331
331
  * `verify_ssl` (boolean, optional): When true, the handler verifies SSL certificates; defaults to `false`. When omitted, the last configured value is preserved.
332
332
  * `cloudflare_api_token` (string, optional): Cloudflare API token the host can reuse later to provision tunnels.
333
333
 
334
+ The setup command also installs `cloudflared` on the node so Cloudflare tunnels can be created afterward.
335
+
334
336
  **Responses:**
335
337
 
336
338
  * On success, the device will emit a [`proxmox_infra_configured`](#proxmox_infra_configured-event) event with the persisted infra snapshot.
@@ -410,6 +412,51 @@ Deletes a managed container from Proxmox (stopping it first if necessary) and re
410
412
  * Emits a [`proxmox_container_action`](#proxmox_container_action-event) event with `action="remove"` and the refreshed infra snapshot after deletion.
411
413
  * Emits an [`error`](#error) event on failure.
412
414
 
415
+ ### `create_cloudflare_tunnel`
416
+
417
+ Creates or updates a Cloudflare tunnel in front of a managed container. The container must be running and a Cloudflare API token must already be configured via `setup_proxmox_infra`.
418
+
419
+ **Payload Fields:**
420
+
421
+ * `ctid` (string, required): Identifier of the container.
422
+ * `container_port` (integer, required): Port inside the container to expose through Cloudflare.
423
+ * `cloudflare_url` (string, optional): Hostname (e.g., `app.example.com`) that the tunnel should serve. Leave blank to let Cloudflare create a quick tunnel (`*.cfargotunnel.com`) automatically.
424
+ * `protocol` (string, optional): One of `http`, `https`, or `tcp` (defaults to `http`).
425
+
426
+ **Responses:**
427
+
428
+ * Emits a [`cloudflare_tunnel_created`](#cloudflare_tunnel_created-event) event with the tunnel metadata.
429
+ * Emits an [`error`](#error) event on failure.
430
+
431
+ ### `update_cloudflare_tunnel`
432
+
433
+ Refreshes the tunnel configuration for an existing tunnel (different port, URL, or protocol).
434
+
435
+ **Payload Fields:**
436
+
437
+ * `ctid` (string, required): Identifier of the container.
438
+ * `container_port` (integer, optional): New container port.
439
+ * `cloudflare_url` (string, optional): New hostname (leave blank to keep the current hostname or let Cloudflare assign a quick tunnel).
440
+ * `protocol` (string, optional): New protocol (`http`, `https`, or `tcp`).
441
+
442
+ **Responses:**
443
+
444
+ * Emits a [`cloudflare_tunnel_updated`](#cloudflare_tunnel_updated-event) event with the refreshed tunnel metadata.
445
+ * Emits an [`error`](#error) event on failure.
446
+
447
+ ### `remove_cloudflare_tunnel`
448
+
449
+ Stops and removes any tunnel metadata associated with a container.
450
+
451
+ **Payload Fields:**
452
+
453
+ * `ctid` (string, required): Identifier of the container.
454
+
455
+ **Responses:**
456
+
457
+ * Emits a [`cloudflare_tunnel_removed`](#cloudflare_tunnel_removed-event) event.
458
+ * Emits an [`error`](#error) event on failure.
459
+
413
460
  ### `proxmox_container_created`
414
461
 
415
462
  Emitted after a successful `create_proxmox_container` action. Contains the new container ID, the Portacode public key produced inside the container, and the bootstrap logs.
@@ -1146,6 +1193,11 @@ Provides system information in response to a `system_info` action. Handled by [`
1146
1193
  * `cpu_share` (number): vCPU-equivalent share requested at creation.
1147
1194
  * `status` (string): Lowercase lifecycle status (e.g., `running`, `stopped`, `deleted`).
1148
1195
  * `created_at` (string|null): ISO timestamp recorded when the CT was provisioned.
1196
+ * `tunnel` (object|null): Tunnel metadata that includes:
1197
+ * `url` (string): Public hostname assigned for this tunnel.
1198
+ * `container_port` (integer): Container port exposed via the tunnel.
1199
+ * `protocol` (string): Protocol advertised when the tunnel was configured.
1200
+ * `status` (string): `running`, `stopped`, or `unknown`.
1149
1201
  * `portacode_version` (string): Installed CLI version returned by `portacode.__version__`.
1150
1202
 
1151
1203
  ### `proxmox_infra_configured`
@@ -1211,6 +1263,41 @@ Emitted after `start_proxmox_container`, `stop_proxmox_container`, or `remove_pr
1211
1263
  * `details` (object, optional): Exit status information (e.g., `exitstatus`, `stop_exitstatus`, `delete_exitstatus`).
1212
1264
  * `infra` (object): Same snapshot described under [`system_info`](#system_info-event) `proxmox.infra`, including the updated `managed_containers` summary.
1213
1265
 
1266
+ ### `cloudflare_tunnel_created`
1267
+
1268
+ Submitted after a successful `create_cloudflare_tunnel` action.
1269
+
1270
+ **Event Fields:**
1271
+
1272
+ * `event` (string): `cloudflare_tunnel_created`.
1273
+ * `ctid` (string): Container ID associated with the tunnel.
1274
+ * `success` (boolean): True when the tunnel is running.
1275
+ * `message` (string): Summary text.
1276
+ * `tunnel` (object): Tunnel metadata matching the manager view. When using Cloudflare Quick Tunnel (no hostname supplied), `tunnel.url` holds the autogenerated `*.cfargotunnel.com` hostname.
1277
+
1278
+ ### `cloudflare_tunnel_updated`
1279
+
1280
+ Sent when `update_cloudflare_tunnel` completes.
1281
+
1282
+ **Event Fields:**
1283
+
1284
+ * `event` (string): `cloudflare_tunnel_updated`.
1285
+ * `ctid` (string): Container ID associated with the tunnel.
1286
+ * `success` (boolean): True on success.
1287
+ * `message` (string): Summary text.
1288
+ * `tunnel` (object): Updated tunnel metadata.
1289
+
1290
+ ### `cloudflare_tunnel_removed`
1291
+
1292
+ Sent after a tunnel has been removed from a container record.
1293
+
1294
+ **Event Fields:**
1295
+
1296
+ * `event` (string): `cloudflare_tunnel_removed`.
1297
+ * `ctid` (string): Container ID that no longer has a tunnel.
1298
+ * `success` (boolean): True on success.
1299
+ * `message` (string): Summary text.
1300
+
1214
1301
  ### <a name="clock_sync_response"></a>`clock_sync_response`
1215
1302
 
1216
1303
  Reply sent by the gateway immediately after receiving a `clock_sync_request`. Devices use this event plus the measured round-trip time to keep their local `ntp_clock` offset accurate.
@@ -8,6 +8,8 @@ import logging
8
8
  import os
9
9
  import secrets
10
10
  import shlex
11
+ import re
12
+ import select
11
13
  import shutil
12
14
  import stat
13
15
  import subprocess
@@ -15,6 +17,7 @@ import sys
15
17
  import tempfile
16
18
  import time
17
19
  import threading
20
+ import urllib.request
18
21
  from datetime import datetime
19
22
  from pathlib import Path
20
23
  from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple
@@ -40,12 +43,15 @@ BRIDGE_IP = SUBNET_CIDR.split("/", 1)[0]
40
43
  DHCP_START = "10.10.0.100"
41
44
  DHCP_END = "10.10.0.200"
42
45
  DNS_SERVER = "1.1.1.1"
46
+ CLOUDFLARE_DEB_URL = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb"
43
47
  IFACES_PATH = Path("/etc/network/interfaces")
44
48
  SYSCTL_PATH = Path("/etc/sysctl.d/99-portacode-forward.conf")
45
49
  UNIT_DIR = Path("/etc/systemd/system")
46
50
  _MANAGED_CONTAINERS_CACHE_TTL_S = 30.0
47
51
  _MANAGED_CONTAINERS_CACHE: Dict[str, Any] = {"timestamp": 0.0, "summary": None}
48
52
  _MANAGED_CONTAINERS_CACHE_LOCK = threading.Lock()
53
+ _CLOUDFLARE_TUNNEL_PROCESSES: Dict[str, subprocess.Popen] = {}
54
+ _CLOUDFLARE_TUNNELS_LOCK = threading.Lock()
49
55
 
50
56
  ProgressCallback = Callable[[int, int, Dict[str, Any], str, Optional[Dict[str, Any]]], None]
51
57
 
@@ -279,6 +285,25 @@ def _ensure_bridge(bridge: str = DEFAULT_BRIDGE) -> Dict[str, Any]:
279
285
  return {"applied": True, "bridge": bridge, "message": f"Bridge {bridge} configured"}
280
286
 
281
287
 
288
+ def _ensure_cloudflared_installed() -> None:
289
+ if shutil.which("cloudflared"):
290
+ return
291
+ apt = shutil.which("apt-get")
292
+ if not apt:
293
+ raise RuntimeError("cloudflared is missing and apt-get is unavailable to install it")
294
+ download_dir = Path(tempfile.mkdtemp())
295
+ deb_path = download_dir / "cloudflared.deb"
296
+ try:
297
+ urllib.request.urlretrieve(CLOUDFLARE_DEB_URL, deb_path)
298
+ try:
299
+ _call_subprocess(["dpkg", "-i", str(deb_path)], check=True)
300
+ except subprocess.CalledProcessError:
301
+ _call_subprocess([apt, "install", "-f", "-y"], check=True)
302
+ _call_subprocess(["dpkg", "-i", str(deb_path)], check=True)
303
+ finally:
304
+ shutil.rmtree(download_dir, ignore_errors=True)
305
+
306
+
282
307
  def _verify_connectivity(timeout: float = 5.0) -> bool:
283
308
  try:
284
309
  _call_subprocess(["/bin/ping", "-c", "2", "1.1.1.1"], check=True, timeout=timeout)
@@ -355,6 +380,7 @@ def _build_managed_containers_summary(records: List[Dict[str, Any]]) -> Dict[str
355
380
  "cpu_share": cpu_share,
356
381
  "created_at": record.get("created_at"),
357
382
  "status": status,
383
+ "tunnel": record.get("tunnel"),
358
384
  }
359
385
  )
360
386
 
@@ -592,6 +618,89 @@ def _remove_container_record(vmid: int) -> None:
592
618
  _invalidate_managed_containers_cache()
593
619
 
594
620
 
621
+ def _update_container_tunnel(vmid: int, tunnel: Optional[Dict[str, Any]]) -> None:
622
+ record = _read_container_record(vmid)
623
+ if tunnel:
624
+ record["tunnel"] = tunnel
625
+ else:
626
+ record.pop("tunnel", None)
627
+ _write_container_record(vmid, record)
628
+
629
+
630
+ def _ensure_cloudflare_token(config: Dict[str, Any]) -> str:
631
+ cloudflare = config.get("cloudflare") or {}
632
+ token = cloudflare.get("api_token")
633
+ if not token:
634
+ raise RuntimeError("Cloudflare API token is not configured.")
635
+ return token
636
+
637
+
638
+ def _launch_container_tunnel(proxmox: Any, node: str, vmid: int, tunnel: Dict[str, Any]) -> Dict[str, Any]:
639
+ port = int(tunnel.get("container_port") or 0)
640
+ if not port:
641
+ raise ValueError("container_port is required to create a tunnel.")
642
+ requested_hostname = tunnel.get("url") or None
643
+ protocol = (tunnel.get("protocol") or "http").lower()
644
+ ip_address = _resolve_container_ip(proxmox, node, vmid)
645
+ target_url = f"{protocol}://{ip_address}:{port}"
646
+ name = tunnel.get("name") or _build_tunnel_name(vmid, port)
647
+ _stop_cloudflare_process(name)
648
+ proc, assigned_url = _start_cloudflare_process(name, target_url, hostname=requested_hostname)
649
+ if not assigned_url:
650
+ raise RuntimeError("Failed to determine Cloudflare hostname for the tunnel.")
651
+ updated = {
652
+ "name": name,
653
+ "container_port": port,
654
+ "url": assigned_url,
655
+ "protocol": protocol,
656
+ "status": "running",
657
+ "pid": proc.pid,
658
+ "target_ip": ip_address,
659
+ "target_url": target_url,
660
+ "last_updated": datetime.utcnow().isoformat() + "Z",
661
+ }
662
+ _update_container_tunnel(vmid, updated)
663
+ return updated
664
+
665
+
666
+ def _stop_container_tunnel(vmid: int) -> None:
667
+ try:
668
+ record = _read_container_record(vmid)
669
+ except FileNotFoundError:
670
+ return
671
+ tunnel = record.get("tunnel")
672
+ if not tunnel:
673
+ return
674
+ name = tunnel.get("name") or _build_tunnel_name(vmid, int(tunnel.get("container_port") or 0))
675
+ stopped = _stop_cloudflare_process(name)
676
+ if not stopped and tunnel.get("status") == "stopped":
677
+ return
678
+ tunnel_update = {
679
+ **tunnel,
680
+ "status": "stopped",
681
+ "pid": None,
682
+ "last_updated": datetime.utcnow().isoformat() + "Z",
683
+ }
684
+ _update_container_tunnel(vmid, tunnel_update)
685
+
686
+
687
+ def _remove_container_tunnel_state(vmid: int) -> None:
688
+ _stop_container_tunnel(vmid)
689
+ _update_container_tunnel(vmid, None)
690
+
691
+
692
+ def _ensure_container_tunnel_running(proxmox: Any, node: str, vmid: int) -> None:
693
+ try:
694
+ record = _read_container_record(vmid)
695
+ except FileNotFoundError:
696
+ return
697
+ tunnel = record.get("tunnel")
698
+ if not tunnel:
699
+ return
700
+ _ensure_cloudflare_token(_load_config())
701
+ _launch_container_tunnel(proxmox, node, vmid, tunnel)
702
+
703
+
595
704
  def _build_container_payload(message: Dict[str, Any], config: Dict[str, Any]) -> Dict[str, Any]:
596
705
  templates = config.get("templates") or []
597
706
  default_template = templates[0] if templates else ""
@@ -707,6 +816,116 @@ def _run_pct_push(vmid: int, src: str, dest: str) -> subprocess.CompletedProcess
707
816
  return _call_subprocess(["pct", "push", str(vmid), src, dest])
708
817
 
709
818
 
819
+ def _build_tunnel_name(vmid: int, port: int) -> str:
820
+ return f"portacode-ct{vmid}-{port}"
821
+
822
+
823
+ def _get_cloudflared_binary() -> str:
824
+ binary = shutil.which("cloudflared")
825
+ if not binary:
826
+ raise RuntimeError(
827
+ "cloudflared is required for Cloudflare tunnels but was not found on PATH. "
828
+ "Install cloudflared and run 'cloudflared tunnel login' before creating tunnels."
829
+ )
830
+ return binary
831
+
832
+
833
+ def _drain_stream(stream: Optional[Any]) -> None:
834
+ if stream is None:
835
+ return
836
+ try:
837
+ for _ in iter(stream.readline, ""):
838
+ continue
839
+ except Exception:
840
+ pass
841
+ finally:
842
+ try:
843
+ stream.close()
844
+ except Exception:
845
+ pass
846
+
847
+
848
+ def _await_quick_tunnel_url(proc: subprocess.Popen, timeout: float = 15.0) -> Optional[str]:
849
+ if not proc.stdout:
850
+ return None
851
+ cf_re = re.compile(r"https://[A-Za-z0-9\-.]+\.cfargotunnel\.com")
852
+ deadline = time.time() + timeout
853
+ while time.time() < deadline:
854
+ ready, _, _ = select.select([proc.stdout], [], [], 1)
855
+ if not ready:
856
+ continue
857
+ line = proc.stdout.readline()
858
+ if not line:
859
+ continue
860
+ match = cf_re.search(line)
861
+ if match:
862
+ return match.group(0)
863
+ return None
864
+
865
+
866
+ def _start_cloudflare_process(name: str, target_url: str, hostname: Optional[str] = None) -> Tuple[subprocess.Popen, Optional[str]]:
867
+ binary = _get_cloudflared_binary()
868
+ cmd = [
869
+ binary,
870
+ "tunnel",
871
+ "--url",
872
+ target_url,
873
+ "--no-autoupdate",
874
+ ]
875
+ if hostname:
876
+ cmd.extend(["--hostname", hostname])
877
+ stdout_target = subprocess.DEVNULL
878
+ else:
879
+ stdout_target = subprocess.PIPE
880
+ proc = subprocess.Popen(
881
+ cmd,
882
+ stdout=stdout_target,
883
+ stderr=subprocess.PIPE,
884
+ text=True,
885
+ )
886
+ with _CLOUDFLARE_TUNNELS_LOCK:
887
+ _CLOUDFLARE_TUNNEL_PROCESSES[name] = proc
888
+ assigned_url = hostname
889
+ if not hostname:
890
+ assigned_url = _await_quick_tunnel_url(proc)
891
+ threading.Thread(target=_drain_stream, args=(proc.stdout,), daemon=True).start()
892
+ threading.Thread(target=_drain_stream, args=(proc.stderr,), daemon=True).start()
893
+ return proc, assigned_url
894
+
895
+
896
+ def _stop_cloudflare_process(name: str) -> bool:
897
+ with _CLOUDFLARE_TUNNELS_LOCK:
898
+ proc = _CLOUDFLARE_TUNNEL_PROCESSES.pop(name, None)
899
+ if not proc:
900
+ return False
901
+ try:
902
+ proc.terminate()
903
+ proc.wait(timeout=5)
904
+ except subprocess.TimeoutExpired:
905
+ proc.kill()
906
+ proc.wait()
907
+ return True
908
+
909
+
910
+ def _resolve_container_ip(proxmox: Any, node: str, vmid: int) -> str:
911
+ status = proxmox.nodes(node).lxc(vmid).status.current.get()
912
+ if status:
913
+ ip_field = status.get("ip")
914
+ if isinstance(ip_field, list):
915
+ for entry in ip_field:
916
+ if isinstance(entry, str) and "." in entry:
917
+ return entry.split("/")[0]
918
+ elif isinstance(ip_field, str) and "." in ip_field:
919
+ return ip_field.split("/")[0]
920
+ res = _run_pct_exec(vmid, ["ip", "-4", "-o", "addr", "show", "eth0"])
921
+ line = res.stdout.splitlines()[0] if res.stdout else ""
922
+ parts = line.split()
923
+ if len(parts) >= 4:
924
+ addr = parts[3]
925
+ return addr.split("/")[0]
926
+ raise RuntimeError("Unable to determine container IP address")
927
+
928
+
710
929
  def _push_bytes_to_container(
711
930
  vmid: int, user: str, path: str, data: bytes, mode: int = 0o600
712
931
  ) -> None:
@@ -1091,6 +1310,7 @@ def configure_infrastructure(
1091
1310
  default_storage = _pick_storage(storages)
1092
1311
  templates = _list_templates(client, node, storages)
1093
1312
  network = dict(existing.get("network", {}) or {})
1313
+ _ensure_cloudflared_installed()
1094
1314
  if not network.get("applied"):
1095
1315
  try:
1096
1316
  network = _ensure_bridge()
@@ -1602,6 +1822,10 @@ class StartProxmoxContainerHandler(SyncHandler):
1602
1822
 
1603
1823
  status, elapsed = _start_container(proxmox, node, vmid)
1604
1824
  _update_container_record(vmid, {"status": "running"})
1825
+ try:
1826
+ _ensure_container_tunnel_running(proxmox, node, vmid)
1827
+ except Exception as exc:
1828
+ raise RuntimeError(f"Failed to start Cloudflare tunnel for container {vmid}: {exc}") from exc
1605
1829
 
1606
1830
  infra = get_infra_snapshot()
1607
1831
  return {
@@ -1631,6 +1855,7 @@ class StopProxmoxContainerHandler(SyncHandler):
1631
1855
  _ensure_container_managed(proxmox, node, vmid)
1632
1856
 
1633
1857
  status, elapsed = _stop_container(proxmox, node, vmid)
1858
+ _stop_container_tunnel(vmid)
1634
1859
  final_status = status.get("status") or "stopped"
1635
1860
  _update_container_record(vmid, {"status": final_status})
1636
1861
 
@@ -1666,8 +1891,13 @@ class RemoveProxmoxContainerHandler(SyncHandler):
1666
1891
  node = _get_node_from_config(config)
1667
1892
  _ensure_container_managed(proxmox, node, vmid)
1668
1893
 
1894
+ _stop_container_tunnel(vmid)
1669
1895
  stop_status, stop_elapsed = _stop_container(proxmox, node, vmid)
1670
1896
  delete_status, delete_elapsed = _delete_container(proxmox, node, vmid)
1897
+ try:
1898
+ _update_container_tunnel(vmid, None)
1899
+ except FileNotFoundError:
1900
+ pass
1671
1901
  _remove_container_record(vmid)
1672
1902
 
1673
1903
  infra = get_infra_snapshot()
@@ -1686,6 +1916,107 @@ class RemoveProxmoxContainerHandler(SyncHandler):
1686
1916
  }
1687
1917
 
1688
1918
 
1919
+ class CreateCloudflareTunnelHandler(SyncHandler):
1920
+ """Create a Cloudflare tunnel for a container."""
1921
+
1922
+ @property
1923
+ def command_name(self) -> str:
1924
+ return "create_cloudflare_tunnel"
1925
+
1926
+ def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
1927
+ vmid = _parse_ctid(message)
1928
+ container_port = int(message.get("container_port") or 0)
1929
+ if container_port <= 0:
1930
+ raise ValueError("container_port is required and must be greater than zero.")
1931
+ hostname = (message.get("cloudflare_url") or message.get("hostname") or "").strip()
1932
+ hostname = hostname or None
1933
+ protocol = (message.get("protocol") or "http").strip().lower()
1934
+ if protocol not in {"http", "https", "tcp"}:
1935
+ raise ValueError("protocol must be one of http, https, or tcp.")
1936
+ config = _ensure_infra_configured()
1937
+ _ensure_cloudflare_token(config)
1938
+ proxmox = _connect_proxmox(config)
1939
+ node = _get_node_from_config(config)
1940
+ _ensure_container_managed(proxmox, node, vmid)
1941
+ status = proxmox.nodes(node).lxc(vmid).status.current.get().get("status")
1942
+ if status != "running":
1943
+ raise RuntimeError("Container must be running to create a tunnel.")
1944
+ tunnel = {
1945
+ "container_port": container_port,
1946
+ "protocol": protocol,
1947
+ }
1948
+ if hostname:
1949
+ tunnel["url"] = hostname
1950
+ created = _launch_container_tunnel(proxmox, node, vmid, tunnel)
1951
+ return {
1952
+ "event": "cloudflare_tunnel_created",
1953
+ "ctid": str(vmid),
1954
+ "success": True,
1955
+ "message": f"Created Cloudflare tunnel for container {vmid}.",
1956
+ "tunnel": created,
1957
+ }
1958
+
1959
+
1960
+ class UpdateCloudflareTunnelHandler(SyncHandler):
1961
+ """Update an existing Cloudflare tunnel for a container."""
1962
+
1963
+ @property
1964
+ def command_name(self) -> str:
1965
+ return "update_cloudflare_tunnel"
1966
+
1967
+ def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
1968
+ vmid = _parse_ctid(message)
1969
+ config = _ensure_infra_configured()
1970
+ _ensure_cloudflare_token(config)
1971
+ proxmox = _connect_proxmox(config)
1972
+ node = _get_node_from_config(config)
1973
+ _ensure_container_managed(proxmox, node, vmid)
1974
+ record = _read_container_record(vmid)
1975
+ tunnel = record.get("tunnel")
1976
+ if not tunnel:
1977
+ raise RuntimeError("No Cloudflare tunnel configured for this container.")
1978
+ container_port = int(message.get("container_port") or tunnel.get("container_port") or 0)
1979
+ if container_port <= 0:
1980
+ raise ValueError("container_port must be greater than zero.")
1981
+ hostname = (message.get("cloudflare_url") or tunnel.get("url") or "").strip()
1982
+ hostname = hostname or None
1983
+ protocol = (message.get("protocol") or tunnel.get("protocol") or "http").strip().lower()
1984
+ if protocol not in {"http", "https", "tcp"}:
1985
+ raise ValueError("protocol must be one of http, https, or tcp.")
1986
+ updated_tunnel = {
1987
+ "container_port": container_port,
1988
+ "protocol": protocol,
1989
+ }
1990
+ if hostname:
1991
+ updated_tunnel["url"] = hostname
1992
+ result = _launch_container_tunnel(proxmox, node, vmid, updated_tunnel)
1993
+ return {
1994
+ "event": "cloudflare_tunnel_updated",
1995
+ "ctid": str(vmid),
1996
+ "success": True,
1997
+ "message": f"Updated Cloudflare tunnel for container {vmid}.",
1998
+ "tunnel": result,
1999
+ }
2000
+
2001
+
2002
+ class RemoveCloudflareTunnelHandler(SyncHandler):
2003
+ """Remove any Cloudflare tunnel associated with a container."""
2004
+
2005
+ @property
2006
+ def command_name(self) -> str:
2007
+ return "remove_cloudflare_tunnel"
2008
+
2009
+ def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
2010
+ vmid = _parse_ctid(message)
2011
+ _remove_container_tunnel_state(vmid)
2012
+ return {
2013
+ "event": "cloudflare_tunnel_removed",
2014
+ "ctid": str(vmid),
2015
+ "success": True,
2016
+ "message": f"Removed Cloudflare tunnel state for container {vmid}.",
2017
+ }
2018
+
2019
+
1689
2020
  class ConfigureProxmoxInfraHandler(SyncHandler):
1690
2021
  @property
1691
2022
  def command_name(self) -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: portacode
3
- Version: 1.4.15.dev0
3
+ Version: 1.4.15.dev2
4
4
  Summary: Portacode CLI client and SDK
5
5
  Home-page: https://github.com/portacode/portacode
6
6
  Author: Meena Erian
File without changes