fujin-cli 0.26.1__tar.gz → 0.27.1__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 (155) hide show
  1. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/CHANGELOG.md +18 -0
  2. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/PKG-INFO +1 -1
  3. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/plugins/fujin-secrets-1password/pyproject.toml +2 -2
  4. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/plugins/fujin-secrets-bitwarden/pyproject.toml +2 -2
  5. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/plugins/fujin-secrets-doppler/pyproject.toml +2 -2
  6. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/plugins/fujin-secrets-env/pyproject.toml +2 -2
  7. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/pyproject.toml +2 -2
  8. fujin_cli-0.27.1/src/fujin/__init__.py +1 -0
  9. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/src/fujin/_installer.py +20 -26
  10. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/src/fujin/commands/_base.py +1 -9
  11. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/src/fujin/commands/app.py +8 -7
  12. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/src/fujin/commands/audit.py +2 -1
  13. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/src/fujin/commands/deploy.py +3 -3
  14. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/src/fujin/commands/down.py +2 -1
  15. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/src/fujin/commands/init.py +11 -90
  16. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/src/fujin/commands/prune.py +2 -1
  17. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/src/fujin/commands/rollback.py +2 -1
  18. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/src/fujin/commands/server.py +9 -8
  19. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/src/fujin/connection.py +23 -8
  20. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/tests/conftest.py +1 -1
  21. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/tests/test_audit.py +5 -5
  22. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/tests/test_down.py +6 -6
  23. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/tests/test_init.py +1 -5
  24. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/tests/test_prune.py +6 -6
  25. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/tests/test_rollback.py +7 -7
  26. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/tests/test_server.py +9 -6
  27. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/uv.lock +5 -5
  28. fujin_cli-0.26.1/src/fujin/__init__.py +0 -1
  29. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/.github/FUNDING.yml +0 -0
  30. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/.github/workflows/publish.yml +0 -0
  31. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/.github/workflows/test.yml +0 -0
  32. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/.gitignore +0 -0
  33. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/.pre-commit-config.yaml +0 -0
  34. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/.readthedocs.yaml +0 -0
  35. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/CLAUDE.md +0 -0
  36. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/LICENSE.txt +0 -0
  37. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/README.md +0 -0
  38. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/Vagrantfile +0 -0
  39. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/_static/images/help/app-cat-help.png +0 -0
  40. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/_static/images/help/app-exec-help.png +0 -0
  41. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/_static/images/help/app-help.png +0 -0
  42. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/_static/images/help/app-logs-help.png +0 -0
  43. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/_static/images/help/app-restart-help.png +0 -0
  44. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/_static/images/help/app-scale-help.png +0 -0
  45. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/_static/images/help/app-shell-help.png +0 -0
  46. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/_static/images/help/app-start-help.png +0 -0
  47. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/_static/images/help/app-status-help.png +0 -0
  48. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/_static/images/help/app-stop-help.png +0 -0
  49. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/_static/images/help/audit-help.png +0 -0
  50. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/_static/images/help/deploy-help.png +0 -0
  51. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/_static/images/help/down-help.png +0 -0
  52. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/_static/images/help/fa-help.png +0 -0
  53. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/_static/images/help/fujin-help.png +0 -0
  54. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/_static/images/help/init-help.png +0 -0
  55. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/_static/images/help/migrate-help.png +0 -0
  56. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/_static/images/help/new-help.png +0 -0
  57. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/_static/images/help/prune-help.png +0 -0
  58. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/_static/images/help/rollback-help.png +0 -0
  59. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/_static/images/help/server-bootstrap-help.png +0 -0
  60. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/_static/images/help/server-create-user-help.png +0 -0
  61. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/_static/images/help/server-exec-help.png +0 -0
  62. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/_static/images/help/server-help.png +0 -0
  63. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/_static/images/help/server-setup-ssh-help.png +0 -0
  64. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/_static/images/help/server-status-help.png +0 -0
  65. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/_static/images/help/up-help.png +0 -0
  66. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/changelog.rst +0 -0
  67. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/commands/app.rst +0 -0
  68. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/commands/audit.rst +0 -0
  69. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/commands/deploy.rst +0 -0
  70. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/commands/down.rst +0 -0
  71. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/commands/index.rst +0 -0
  72. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/commands/init.rst +0 -0
  73. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/commands/migrate.rst +0 -0
  74. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/commands/new.rst +0 -0
  75. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/commands/prune.rst +0 -0
  76. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/commands/rollback.rst +0 -0
  77. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/commands/server.rst +0 -0
  78. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/commands/up.rst +0 -0
  79. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/conf.py +0 -0
  80. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/configuration.rst +0 -0
  81. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/guides/django-complete.rst +0 -0
  82. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/guides/index.rst +0 -0
  83. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/guides/templates.rst +0 -0
  84. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/howtos/binary.rst +0 -0
  85. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/howtos/django.rst +0 -0
  86. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/howtos/index.rst +0 -0
  87. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/index.rst +0 -0
  88. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/installation.rst +0 -0
  89. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/integrations.rst +0 -0
  90. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/requirements.txt +0 -0
  91. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/secrets.rst +0 -0
  92. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/docs/troubleshooting.rst +0 -0
  93. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/examples/django/bookstore/.fujin/Caddyfile +0 -0
  94. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/examples/django/bookstore/.fujin/systemd/health.service +0 -0
  95. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/examples/django/bookstore/.fujin/systemd/health.timer +0 -0
  96. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/examples/django/bookstore/.fujin/systemd/web.service +0 -0
  97. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/examples/django/bookstore/.fujin/systemd/worker@.service +0 -0
  98. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/examples/django/bookstore/README.md +0 -0
  99. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/examples/django/bookstore/bookstore/__init__.py +0 -0
  100. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/examples/django/bookstore/bookstore/__main__.py +0 -0
  101. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/examples/django/bookstore/bookstore/asgi.py +0 -0
  102. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/examples/django/bookstore/bookstore/management/commands/health.py +0 -0
  103. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/examples/django/bookstore/bookstore/settings.py +0 -0
  104. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/examples/django/bookstore/bookstore/urls.py +0 -0
  105. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/examples/django/bookstore/bookstore/wsgi.py +0 -0
  106. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/examples/django/bookstore/fujin.toml +0 -0
  107. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/examples/django/bookstore/manage.py +0 -0
  108. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/examples/django/bookstore/pyproject.toml +0 -0
  109. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/examples/django/bookstore/requirements.txt +0 -0
  110. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/justfile +0 -0
  111. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/plugins/fujin-secrets-1password/README.md +0 -0
  112. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/plugins/fujin-secrets-1password/src/fujin_secrets_1password/__init__.py +0 -0
  113. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/plugins/fujin-secrets-1password/src/fujin_secrets_1password/py.typed +0 -0
  114. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/plugins/fujin-secrets-bitwarden/README.md +0 -0
  115. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/plugins/fujin-secrets-bitwarden/src/fujin_secrets_bitwarden/__init__.py +0 -0
  116. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/plugins/fujin-secrets-bitwarden/src/fujin_secrets_bitwarden/py.typed +0 -0
  117. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/plugins/fujin-secrets-doppler/README.md +0 -0
  118. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/plugins/fujin-secrets-doppler/src/fujin_secrets_doppler/__init__.py +0 -0
  119. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/plugins/fujin-secrets-doppler/src/fujin_secrets_doppler/py.typed +0 -0
  120. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/plugins/fujin-secrets-env/README.md +0 -0
  121. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/plugins/fujin-secrets-env/src/fujin_secrets_env/__init__.py +0 -0
  122. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/src/fujin/__main__.py +0 -0
  123. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/src/fujin/audit.py +0 -0
  124. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/src/fujin/caddy.py +0 -0
  125. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/src/fujin/commands/__init__.py +0 -0
  126. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/src/fujin/commands/migrate.py +0 -0
  127. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/src/fujin/commands/new.py +0 -0
  128. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/src/fujin/commands/showenv.py +0 -0
  129. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/src/fujin/commands/up.py +0 -0
  130. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/src/fujin/config.py +0 -0
  131. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/src/fujin/discovery.py +0 -0
  132. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/src/fujin/errors.py +0 -0
  133. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/src/fujin/fa.py +0 -0
  134. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/src/fujin/formatting.py +0 -0
  135. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/src/fujin/secrets.py +0 -0
  136. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/src/fujin/templates.py +0 -0
  137. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/tests/__init__.py +0 -0
  138. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/tests/integration/Dockerfile +0 -0
  139. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/tests/integration/__init__.py +0 -0
  140. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/tests/integration/conftest.py +0 -0
  141. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/tests/integration/helpers.py +0 -0
  142. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/tests/integration/test_app_management.py +0 -0
  143. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/tests/integration/test_full_deploy.py +0 -0
  144. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/tests/integration/test_installation.py +0 -0
  145. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/tests/integration/test_server_bootstrap.py +0 -0
  146. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/tests/test_app.py +0 -0
  147. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/tests/test_caddy_domain.py +0 -0
  148. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/tests/test_config.py +0 -0
  149. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/tests/test_connection.py +0 -0
  150. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/tests/test_deploy.py +0 -0
  151. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/tests/test_discovery.py +0 -0
  152. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/tests/test_new.py +0 -0
  153. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/tests/test_scale.py +0 -0
  154. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/tests/test_secrets.py +0 -0
  155. {fujin_cli-0.26.1 → fujin_cli-0.27.1}/tests/test_up.py +0 -0
@@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.27.1] - 2026-06-25
8
+
9
+ ### 🐛 Bug Fixes
10
+
11
+ - Remove falco profile from test parametrization
12
+
13
+ ### ⚡ Performance
14
+
15
+ - Simplify installer
16
+ - Improve ssh connection handling
17
+
18
+ ## [0.27.0] - 2026-06-13
19
+
20
+ ### ⚙️ Miscellaneous Tasks
21
+
22
+ - Release v0.27.0
23
+ - Remove falco preset from init command
24
+
7
25
  ## [0.26.1] - 2026-06-09
8
26
 
9
27
  ### 🐛 Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fujin-cli
3
- Version: 0.26.1
3
+ Version: 0.27.1
4
4
  Summary: Get your project up and running in a few minutes on your own vps.
5
5
  Project-URL: Documentation, https://github.com/Tobi-De/fujin#readme
6
6
  Project-URL: Issues, https://github.com/Tobi-De/fujin/issues
@@ -4,7 +4,7 @@ requires = [ "uv-build>=0.9.18,<0.10" ]
4
4
 
5
5
  [project]
6
6
  name = "fujin-secrets-1password"
7
- version = "0.26.1"
7
+ version = "0.27.1"
8
8
  description = "1Password secret adapter for Fujin"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -20,7 +20,7 @@ classifiers = [
20
20
  "Programming Language :: Python :: 3.14",
21
21
  ]
22
22
  dependencies = [
23
- "fujin-cli>=0.26.1",
23
+ "fujin-cli>=0.27.1",
24
24
  "python-dotenv>=1.0.1",
25
25
  ]
26
26
 
@@ -4,7 +4,7 @@ requires = [ "uv-build>=0.9.18,<0.10" ]
4
4
 
5
5
  [project]
6
6
  name = "fujin-secrets-bitwarden"
7
- version = "0.26.1"
7
+ version = "0.27.1"
8
8
  description = "Bitwarden secret adapter for Fujin"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -20,7 +20,7 @@ classifiers = [
20
20
  "Programming Language :: Python :: 3.14",
21
21
  ]
22
22
  dependencies = [
23
- "fujin-cli>=0.26.1",
23
+ "fujin-cli>=0.27.1",
24
24
  "python-dotenv>=1.0.1",
25
25
  ]
26
26
 
@@ -4,7 +4,7 @@ requires = [ "uv-build>=0.9.18,<0.10" ]
4
4
 
5
5
  [project]
6
6
  name = "fujin-secrets-doppler"
7
- version = "0.26.1"
7
+ version = "0.27.1"
8
8
  description = "Doppler secret adapter for Fujin"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -20,7 +20,7 @@ classifiers = [
20
20
  "Programming Language :: Python :: 3.14",
21
21
  ]
22
22
  dependencies = [
23
- "fujin-cli>=0.26.1",
23
+ "fujin-cli>=0.27.1",
24
24
  "python-dotenv>=1.0.1",
25
25
  ]
26
26
 
@@ -4,7 +4,7 @@ requires = [ "uv-build>=0.9.18,<0.10" ]
4
4
 
5
5
  [project]
6
6
  name = "fujin-secrets-env"
7
- version = "0.26.1"
7
+ version = "0.27.1"
8
8
  description = "Environment variable secret adapter for Fujin"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -20,7 +20,7 @@ classifiers = [
20
20
  "Programming Language :: Python :: 3.14",
21
21
  ]
22
22
  dependencies = [
23
- "fujin-cli>=0.26.1",
23
+ "fujin-cli>=0.27.1",
24
24
  "python-dotenv>=1.0.1",
25
25
  ]
26
26
 
@@ -5,7 +5,7 @@ requires = [ "hatchling" ]
5
5
 
6
6
  [project]
7
7
  name = "fujin-cli"
8
- version = "0.26.1"
8
+ version = "0.27.1"
9
9
  description = "Get your project up and running in a few minutes on your own vps."
10
10
  readme = "README.md"
11
11
  keywords = [
@@ -151,7 +151,7 @@ markers = [
151
151
  ]
152
152
 
153
153
  [tool.bumpversion]
154
- current_version = "0.26.1"
154
+ current_version = "0.27.1"
155
155
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
156
156
  serialize = [ "{major}.{minor}.{patch}" ]
157
157
  search = "{current_version}"
@@ -0,0 +1 @@
1
+ __version__ = "0.27.1"
@@ -203,15 +203,15 @@ export -f {config.app_name}
203
203
  logger.debug("Virtual environment already exists")
204
204
 
205
205
  logger.debug("Installing package: %s", config.distfile_name)
206
- dist_install = f"UV_COMPILE_BYTECODE=1 {uv_python_install_dir} {config.uv_path} pip install {distfile_path}"
206
+ # Combine distfile + requirements into a single uv invocation.
207
+ # --no-deps applies to the distfile; -r installs deps from requirements.
208
+ install_cmd = (
209
+ f"UV_COMPILE_BYTECODE=1 {uv_python_install_dir} "
210
+ f"{config.uv_path} pip install {distfile_path} --no-deps"
211
+ )
207
212
  if config.requirements:
208
- requirements_path = bundle_dir / "requirements.txt"
209
- logger.debug("Installing with requirements file")
210
- run(
211
- f"{dist_install} --no-deps && {config.uv_path} pip install -r {requirements_path} ",
212
- )
213
- else:
214
- run(dist_install)
213
+ install_cmd += f" -r {bundle_dir / 'requirements.txt'}"
214
+ run(install_cmd)
215
215
 
216
216
  else:
217
217
  logger.debug("Installation mode: binary")
@@ -387,35 +387,29 @@ export -f {config.app_name}
387
387
  if unit["template_timer_name"]:
388
388
  active_units.append(unit["template_timer_name"])
389
389
 
390
- # Validate all unit files before enabling/starting (catch errors early)
391
- logger.info("Validating systemd unit files...")
392
- validation_issues = 0
393
- for unit in config.deployed_units:
394
- unit_path = SYSTEMD_SYSTEM_DIR / unit["template_service_name"]
395
- if not unit_path.exists():
396
- continue
390
+ # Validate all unit files before enabling/starting (catch errors early).
391
+ # systemd-analyze accepts multiple paths and reports which files have issues.
392
+ unit_paths = [
393
+ SYSTEMD_SYSTEM_DIR / unit["template_service_name"]
394
+ for unit in config.deployed_units
395
+ if (SYSTEMD_SYSTEM_DIR / unit["template_service_name"]).exists()
396
+ ]
397
+ if unit_paths:
398
+ logger.info("Validating systemd unit files...")
399
+ paths_arg = " ".join(shlex.quote(str(p)) for p in unit_paths)
397
400
  result = subprocess.run(
398
- f"systemd-analyze verify {shlex.quote(str(unit_path))}",
401
+ f"systemd-analyze verify {paths_arg}",
399
402
  shell=True,
400
403
  capture_output=True,
401
404
  text=True,
402
405
  )
403
406
  if result.returncode != 0 or result.stderr.strip():
404
- validation_issues += 1
405
407
  logger.warning(
406
- "Validation issues in %s (exit code %d):",
407
- unit_path.name,
408
- result.returncode,
408
+ "Validation issues detected (exit code %d):", result.returncode
409
409
  )
410
410
  for line in result.stderr.splitlines():
411
411
  logger.warning(" %s", line)
412
412
 
413
- if validation_issues:
414
- logger.warning(
415
- "%d unit(s) have validation warnings - review above before continuing",
416
- validation_issues,
417
- )
418
-
419
413
  if not active_units:
420
414
  run("systemctl daemon-reload")
421
415
  restart_result = run("true")
@@ -1,16 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- from contextlib import contextmanager
5
4
  from dataclasses import dataclass
6
5
  from functools import cached_property
7
- from typing import Annotated, Generator
6
+ from typing import Annotated
8
7
 
9
8
  import cappa
10
9
 
11
10
  from fujin.config import Config, HostConfig
12
- from fujin.connection import SSH2Connection
13
- from fujin.connection import connection as host_connection
14
11
 
15
12
  _logging_configured = False
16
13
 
@@ -74,11 +71,6 @@ class BaseCommand:
74
71
  def output(self) -> MessageFormatter:
75
72
  return MessageFormatter(cappa.Output())
76
73
 
77
- @contextmanager
78
- def connection(self) -> Generator[SSH2Connection, None, None]:
79
- with host_connection(host=self.selected_host) as conn:
80
- yield conn
81
-
82
74
 
83
75
  class MessageFormatter:
84
76
  """Enhanced output with built-in color formatting for consistent CLI messaging."""
@@ -12,6 +12,7 @@ from rich.table import Table
12
12
 
13
13
  from fujin.commands import BaseCommand
14
14
  from fujin.config import tomllib
15
+ from fujin import connection
15
16
  from fujin.discovery import DeployedUnit
16
17
 
17
18
 
@@ -54,7 +55,7 @@ class App(BaseCommand):
54
55
  if timer_name:
55
56
  names.append(timer_name)
56
57
 
57
- with self.connection() as conn:
58
+ with connection.connection(host=self.selected_host) as conn:
58
59
  shlex.quote(self.config.app_dir)
59
60
  fujin_dir = shlex.quote(self.config.install_dir)
60
61
  remote_version, _ = conn.run(
@@ -177,7 +178,7 @@ class App(BaseCommand):
177
178
  self.output.output(f"[bold]Replicas:[/bold] {deployed_unit.replicas}")
178
179
 
179
180
  # Get status from server
180
- with self.connection() as conn:
181
+ with connection.connection(host=self.selected_host) as conn:
181
182
  # Get detailed status for each instance
182
183
  self.output.output("\n[bold]Status:[/bold]")
183
184
  for unit_name in deployed_unit.service_instances():
@@ -257,7 +258,7 @@ class App(BaseCommand):
257
258
  self,
258
259
  command: Annotated[str, cappa.Arg(help="Command to execute")],
259
260
  ):
260
- with self.connection() as conn:
261
+ with connection.connection(host=self.selected_host) as conn:
261
262
  cmd = f"cd {self.config.app_dir} && source .install/.appenv && {self.config.app_bin} {command}"
262
263
  conn.run(
263
264
  f"sudo -u {self.config.app_user} bash -c {shlex.quote(cmd)}",
@@ -307,7 +308,7 @@ class App(BaseCommand):
307
308
  self._run_service_command("stop", names)
308
309
 
309
310
  def _run_service_command(self, command: str, names: list[str] | None):
310
- with self.connection() as conn:
311
+ with connection.connection(host=self.selected_host) as conn:
311
312
  # Use instances for start/stop/restart (operates on running services)
312
313
  units = self._get_runtime_units(names)
313
314
  if not units:
@@ -385,7 +386,7 @@ class App(BaseCommand):
385
386
  """
386
387
  Show last 50 lines for web process (default)
387
388
  """
388
- with self.connection() as conn:
389
+ with connection.connection(host=self.selected_host) as conn:
389
390
  # Use instances for logs (shows logs from running services)
390
391
  units = self._get_runtime_units(names)
391
392
 
@@ -425,7 +426,7 @@ class App(BaseCommand):
425
426
  self.output.output(self._get_available_options())
426
427
  return
427
428
 
428
- with self.connection() as conn:
429
+ with connection.connection(host=self.selected_host) as conn:
429
430
  if name == "caddy" and self.config.caddyfile_exists:
430
431
  self.output.output(f"[cyan]# {self.config.caddy_config_path}[/cyan]")
431
432
  print()
@@ -657,7 +658,7 @@ class App(BaseCommand):
657
658
  self.output.info(f"\nService already at {count} replica(s)")
658
659
 
659
660
  def _scale_on_server(self, service: str, old_count: int, new_count: int):
660
- with self.connection() as conn:
661
+ with connection.connection(host=self.selected_host) as conn:
661
662
  app_name = self.config.app_name
662
663
 
663
664
  if new_count > old_count:
@@ -11,6 +11,7 @@ from rich.markup import escape
11
11
 
12
12
  from fujin.audit import read_logs
13
13
  from fujin.commands import BaseCommand
14
+ from fujin import connection
14
15
 
15
16
 
16
17
  @cappa.command(
@@ -28,7 +29,7 @@ class Audit(BaseCommand):
28
29
  ] = 20
29
30
 
30
31
  def __call__(self):
31
- with self.connection() as conn:
32
+ with connection.connection(host=self.selected_host) as conn:
32
33
  records = read_logs(
33
34
  connection=conn,
34
35
  app_name=self.config.app_name,
@@ -24,6 +24,7 @@ from fujin.audit import log_operation
24
24
  from fujin.commands import BaseCommand
25
25
  from fujin.commands.rollback import Rollback
26
26
  from fujin.config import get_git_short_hash
27
+ from fujin import connection
27
28
  from fujin.connection import SSH2Connection
28
29
  from fujin.errors import (
29
30
  BuildError,
@@ -577,12 +578,11 @@ class Deploy(BaseCommand):
577
578
  Acquires the deploy lock on the remote server before yielding the
578
579
  connection, and releases it on exit (even on error).
579
580
  """
580
- with self.connection() as conn:
581
+ with connection.connection(host=self.selected_host, compress=False) as conn:
581
582
  install_dir_q = shlex.quote(self.config.install_dir)
582
583
  lock_file_q = shlex.quote(f"{self.config.install_dir}/.deploy_lock")
583
- conn.run(f"mkdir -p {install_dir_q}", hide=True)
584
584
  _, acquired = conn.run(
585
- f"(set -C; echo $$ > {lock_file_q}) 2>/dev/null",
585
+ f"mkdir -p {install_dir_q} && (set -C; echo $$ > {lock_file_q}) 2>/dev/null",
586
586
  warn=True,
587
587
  hide=True,
588
588
  )
@@ -10,6 +10,7 @@ from rich.prompt import Confirm
10
10
  from fujin import caddy
11
11
  from fujin.audit import log_operation
12
12
  from fujin.commands import BaseCommand
13
+ from fujin import connection
13
14
 
14
15
 
15
16
  @cappa.command(
@@ -48,7 +49,7 @@ class Down(BaseCommand):
48
49
  if not confirm:
49
50
  return
50
51
 
51
- with self.connection() as conn:
52
+ with connection.connection(host=self.selected_host) as conn:
52
53
  self.output.info("Tearing down project...")
53
54
 
54
55
  app_dir = shlex.quote(self.config.app_dir)
@@ -24,7 +24,7 @@ class Init(BaseCommand):
24
24
  profile: Annotated[
25
25
  str,
26
26
  cappa.Arg(
27
- choices=["simple", "falco", "binary", "django"],
27
+ choices=["simple", "binary", "django"],
28
28
  short="-p",
29
29
  long="--profile",
30
30
  help="Configuration profile to use",
@@ -54,7 +54,6 @@ class Init(BaseCommand):
54
54
  profile_generators = {
55
55
  "simple": self._generate_simple,
56
56
  "django": self._generate_django,
57
- "falco": self._generate_falco,
58
57
  "binary": self._generate_binary,
59
58
  }
60
59
 
@@ -172,11 +171,20 @@ class Init(BaseCommand):
172
171
  "# Main command - adjust to match your application\n", exec_start_pre_lines
173
172
  )
174
173
 
175
- # Customize ExecStart for gunicorn
174
+ # Customize ExecStart for gunicorn and add reload with migrations
176
175
  service_content = service_content.replace(
177
176
  "ExecStart={install_dir}/.venv/bin/python -m myapp.web",
178
177
  f"ExecStart={{install_dir}}/.venv/bin/gunicorn {app_name}.wsgi:application --bind 0.0.0.0:8000",
179
178
  )
179
+ exec_reload_lines = (
180
+ f"ExecReload={{install_dir}}/.venv/bin/{app_name} migrate\n"
181
+ f"ExecReload={{install_dir}}/.venv/bin/{app_name} collectstatic --no-input\n"
182
+ f"ExecReload=/bin/kill -s HUP $MAINPID\n"
183
+ )
184
+ service_content = service_content.replace(
185
+ "ExecStartPre=/bin/bash -c 'rsync -a --delete staticfiles/",
186
+ f"{exec_reload_lines}ExecStartPre=/bin/bash -c 'rsync -a --delete staticfiles/",
187
+ )
180
188
  web_service.write_text(service_content)
181
189
  self.output.success(f" Created {web_service}")
182
190
 
@@ -184,93 +192,6 @@ class Init(BaseCommand):
184
192
  fujin_dir, app_name, "localhost:8000", static_path="{app_dir}/staticfiles/"
185
193
  )
186
194
 
187
- def _generate_falco(self, app_name: str, fujin_dir: Path):
188
- """Generate Falco profile: web + worker services."""
189
- systemd_dir = fujin_dir / "systemd"
190
- dropin_dir = systemd_dir / "common.d"
191
- dropin_dir.mkdir(parents=True, exist_ok=True)
192
-
193
- dropin = dropin_dir / "base.conf"
194
- dropin.write_text(
195
- """
196
- [Service]
197
- User={{app_user}}
198
- WorkingDirectory={{app_dir}}
199
- EnvironmentFile={{install_dir}}/.env
200
- Restart=on-failure
201
- RestartSec=5s
202
-
203
- # Security Hardening
204
- NoNewPrivileges=true
205
- PrivateTmp=true
206
- ProtectSystem=strict
207
- ProtectHome=true
208
- ReadWritePaths={{app_dir}}
209
- """
210
- )
211
-
212
- web_service = systemd_dir / "web.service"
213
- web_service.write_text(
214
- f"""
215
- [Unit]
216
- Description={app_name} web
217
- After=network.target
218
- Wants={{setup}}
219
- After={{setup}}
220
-
221
- [Service]
222
- UMask=0002
223
- RuntimeDirectory={app_name}
224
- RuntimeDirectoryMode=0755
225
- ExecStart={{install_dir}}/.venv/bin/{app_name} prodserver --uds /run/{app_name}/web.sock
226
- ExecReload=/bin/kill -s HUP $MAINPID
227
- KillMode=mixed
228
- TimeoutStopSec=5
229
-
230
- [Install]
231
- WantedBy=multi-user.target
232
- """
233
- )
234
- self.output.success(f" Created {web_service}")
235
-
236
- setup_service = systemd_dir / "setup.service"
237
- setup_service.write_text(
238
- f"""
239
- [Unit]
240
- Description={app_name} setup
241
- After=network-online.target
242
-
243
- [Service]
244
- Type=oneshot
245
- RemainAfterExit=yes
246
- ExecStart={{install_dir}}/.venv/bin/{app_name} setup
247
-
248
- [Install]
249
- WantedBy=multi-user.target
250
- """
251
- )
252
- self.output.success(f" Created {setup_service}")
253
-
254
- worker_service = systemd_dir / "worker.service"
255
- worker_service.write_text(
256
- f"""
257
- [Unit]
258
- Description={app_name} worker
259
- Wants={{setup}}
260
- After={{setup}}
261
- After=network.target
262
-
263
- [Service]
264
- ExecStart={{install_dir}}/.venv/bin/{app_name} db_worker
265
-
266
- [Install]
267
- WantedBy=multi-user.target
268
- """
269
- )
270
- self.output.success(f" Created {worker_service}")
271
-
272
- self._create_caddyfile(fujin_dir, app_name, f"unix//run/{app_name}/web.sock")
273
-
274
195
  def _generate_binary(self, app_name: str, fujin_dir: Path):
275
196
  """Generate binary profile: single binary deployment."""
276
197
  systemd_dir = fujin_dir / "systemd"
@@ -5,6 +5,7 @@ import cappa
5
5
  from rich.prompt import Confirm
6
6
 
7
7
  from fujin.commands import BaseCommand
8
+ from fujin import connection
8
9
 
9
10
 
10
11
  @cappa.command(
@@ -30,7 +31,7 @@ class Prune(BaseCommand):
30
31
  raise cappa.Exit("The minimum value for the --keep option is 1", code=1)
31
32
 
32
33
  versions_dir = f"{self.config.install_dir}/.versions"
33
- with self.connection() as conn:
34
+ with connection.connection(host=self.selected_host) as conn:
34
35
  _, success = conn.run(f"test -d {versions_dir}", warn=True, hide=True)
35
36
  if not success:
36
37
  self.output.info("No versions directory found. Nothing to prune.")
@@ -10,6 +10,7 @@ from rich.prompt import Confirm, IntPrompt
10
10
 
11
11
  from fujin.audit import log_operation
12
12
  from fujin.commands import BaseCommand
13
+ from fujin import connection
13
14
 
14
15
 
15
16
  @cappa.command(
@@ -35,7 +36,7 @@ class Rollback(BaseCommand):
35
36
  ] = False
36
37
 
37
38
  def __call__(self):
38
- with self.connection() as conn:
39
+ with connection.connection(host=self.selected_host) as conn:
39
40
  shlex.quote(self.config.app_dir)
40
41
  fujin_dir = shlex.quote(self.config.install_dir)
41
42
  result, _ = conn.run(
@@ -15,6 +15,7 @@ from rich.table import Table
15
15
  from fujin import caddy
16
16
  from fujin.commands import BaseCommand
17
17
  from fujin.config import tomllib
18
+ from fujin import connection
18
19
  from fujin.errors import SSHKeyError
19
20
 
20
21
 
@@ -32,7 +33,7 @@ class Server(BaseCommand):
32
33
 
33
34
  @cappa.command(help="Display information about the host system")
34
35
  def status(self):
35
- with self.connection() as conn:
36
+ with connection.connection(host=self.selected_host) as conn:
36
37
  _, result_ok = conn.run("command -v fastfetch", warn=True, hide=True)
37
38
  if result_ok:
38
39
  conn.run("fastfetch", pty=True)
@@ -41,7 +42,7 @@ class Server(BaseCommand):
41
42
 
42
43
  @cappa.command(help="Setup uv, web proxy, and install necessary dependencies")
43
44
  def bootstrap(self):
44
- with self.connection() as conn:
45
+ with connection.connection(host=self.selected_host) as conn:
45
46
  self.output.info("Bootstrapping server...")
46
47
 
47
48
  self._upgrade_system(conn, packages="sqlite3 curl rsync")
@@ -91,7 +92,7 @@ class Server(BaseCommand):
91
92
  help="Upgrade server components (Caddy, uv, Python, system packages)"
92
93
  )
93
94
  def upgrade(self):
94
- with self.connection() as conn:
95
+ with connection.connection(host=self.selected_host) as conn:
95
96
  self.output.info("Upgrading server components...")
96
97
  all_success = True
97
98
  self._upgrade_system(conn)
@@ -204,7 +205,7 @@ class Server(BaseCommand):
204
205
  bool, cappa.Arg(long="--with-password")
205
206
  ] = False, # no short arg to force explicitness
206
207
  ):
207
- with self.connection() as conn:
208
+ with connection.connection(host=self.selected_host) as conn:
208
209
  commands = [
209
210
  f"sudo adduser --disabled-password --gecos '' {name}",
210
211
  f"sudo mkdir -p /home/{name}/.ssh",
@@ -336,7 +337,7 @@ class Server(BaseCommand):
336
337
  ),
337
338
  ] = False,
338
339
  ):
339
- with self.connection() as conn:
340
+ with connection.connection(host=self.selected_host) as conn:
340
341
  if not appenv:
341
342
  return conn.run(command, pty=True)
342
343
  cmd = f"cd {self.config.app_dir} && source .install/.appenv && {command}"
@@ -353,7 +354,7 @@ class Server(BaseCommand):
353
354
  cappa.Arg(long="--user", help="List keys for a specific user"),
354
355
  ] = None,
355
356
  ):
356
- with self.connection() as conn:
357
+ with connection.connection(host=self.selected_host) as conn:
357
358
  cat = "sudo cat" if user else "cat"
358
359
  path = (
359
360
  f"/home/{user}/.ssh/authorized_keys"
@@ -401,7 +402,7 @@ class Server(BaseCommand):
401
402
  cappa.Arg(long="--user", help="Add key for a specific user"),
402
403
  ] = None,
403
404
  ):
404
- with self.connection() as conn:
405
+ with connection.connection(host=self.selected_host) as conn:
405
406
  comment = f"{name} (fujin@{date.today().isoformat()})"
406
407
  temp_key = f"/tmp/fujin_key_{secrets.token_hex(8)}"
407
408
 
@@ -460,7 +461,7 @@ class Server(BaseCommand):
460
461
  cappa.Arg(long="--force", short="-f", help="Skip confirmation prompt"),
461
462
  ] = False,
462
463
  ):
463
- with self.connection() as conn:
464
+ with connection.connection(host=self.selected_host) as conn:
464
465
  cat = "sudo cat" if user else "cat"
465
466
  path = (
466
467
  f"/home/{user}/.ssh/authorized_keys"