fujin-cli 0.22.2__tar.gz → 0.23.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/CHANGELOG.md +7 -0
  2. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/PKG-INFO +1 -1
  3. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/plugins/fujin-secrets-1password/pyproject.toml +2 -2
  4. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/plugins/fujin-secrets-bitwarden/pyproject.toml +2 -2
  5. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/plugins/fujin-secrets-doppler/pyproject.toml +2 -2
  6. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/pyproject.toml +2 -2
  7. fujin_cli-0.23.0/src/fujin/__init__.py +1 -0
  8. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/src/fujin/_installer.py +0 -8
  9. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/src/fujin/commands/deploy.py +49 -4
  10. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/uv.lock +4 -4
  11. fujin_cli-0.22.2/src/fujin/__init__.py +0 -1
  12. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/.github/FUNDING.yml +0 -0
  13. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/.github/workflows/publish.yml +0 -0
  14. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/.github/workflows/test.yml +0 -0
  15. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/.gitignore +0 -0
  16. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/.pre-commit-config.yaml +0 -0
  17. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/.readthedocs.yaml +0 -0
  18. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/CLAUDE.md +0 -0
  19. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/LICENSE.txt +0 -0
  20. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/README.md +0 -0
  21. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/Vagrantfile +0 -0
  22. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/_static/images/help/app-cat-help.png +0 -0
  23. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/_static/images/help/app-exec-help.png +0 -0
  24. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/_static/images/help/app-help.png +0 -0
  25. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/_static/images/help/app-logs-help.png +0 -0
  26. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/_static/images/help/app-restart-help.png +0 -0
  27. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/_static/images/help/app-scale-help.png +0 -0
  28. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/_static/images/help/app-shell-help.png +0 -0
  29. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/_static/images/help/app-start-help.png +0 -0
  30. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/_static/images/help/app-status-help.png +0 -0
  31. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/_static/images/help/app-stop-help.png +0 -0
  32. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/_static/images/help/audit-help.png +0 -0
  33. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/_static/images/help/deploy-help.png +0 -0
  34. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/_static/images/help/down-help.png +0 -0
  35. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/_static/images/help/fa-help.png +0 -0
  36. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/_static/images/help/fujin-help.png +0 -0
  37. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/_static/images/help/init-help.png +0 -0
  38. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/_static/images/help/migrate-help.png +0 -0
  39. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/_static/images/help/new-help.png +0 -0
  40. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/_static/images/help/prune-help.png +0 -0
  41. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/_static/images/help/rollback-help.png +0 -0
  42. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/_static/images/help/server-bootstrap-help.png +0 -0
  43. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/_static/images/help/server-create-user-help.png +0 -0
  44. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/_static/images/help/server-exec-help.png +0 -0
  45. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/_static/images/help/server-help.png +0 -0
  46. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/_static/images/help/server-setup-ssh-help.png +0 -0
  47. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/_static/images/help/server-status-help.png +0 -0
  48. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/_static/images/help/up-help.png +0 -0
  49. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/changelog.rst +0 -0
  50. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/commands/app.rst +0 -0
  51. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/commands/audit.rst +0 -0
  52. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/commands/deploy.rst +0 -0
  53. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/commands/down.rst +0 -0
  54. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/commands/index.rst +0 -0
  55. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/commands/init.rst +0 -0
  56. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/commands/migrate.rst +0 -0
  57. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/commands/new.rst +0 -0
  58. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/commands/prune.rst +0 -0
  59. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/commands/rollback.rst +0 -0
  60. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/commands/server.rst +0 -0
  61. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/commands/up.rst +0 -0
  62. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/conf.py +0 -0
  63. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/configuration.rst +0 -0
  64. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/guides/django-complete.rst +0 -0
  65. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/guides/index.rst +0 -0
  66. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/guides/templates.rst +0 -0
  67. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/howtos/binary.rst +0 -0
  68. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/howtos/django.rst +0 -0
  69. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/howtos/index.rst +0 -0
  70. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/index.rst +0 -0
  71. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/installation.rst +0 -0
  72. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/integrations.rst +0 -0
  73. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/requirements.txt +0 -0
  74. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/secrets.rst +0 -0
  75. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/docs/troubleshooting.rst +0 -0
  76. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/examples/django/bookstore/.fujin/Caddyfile +0 -0
  77. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/examples/django/bookstore/.fujin/systemd/health.service +0 -0
  78. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/examples/django/bookstore/.fujin/systemd/health.timer +0 -0
  79. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/examples/django/bookstore/.fujin/systemd/web.service +0 -0
  80. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/examples/django/bookstore/.fujin/systemd/worker@.service +0 -0
  81. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/examples/django/bookstore/README.md +0 -0
  82. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/examples/django/bookstore/bookstore/__init__.py +0 -0
  83. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/examples/django/bookstore/bookstore/__main__.py +0 -0
  84. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/examples/django/bookstore/bookstore/asgi.py +0 -0
  85. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/examples/django/bookstore/bookstore/management/commands/health.py +0 -0
  86. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/examples/django/bookstore/bookstore/settings.py +0 -0
  87. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/examples/django/bookstore/bookstore/urls.py +0 -0
  88. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/examples/django/bookstore/bookstore/wsgi.py +0 -0
  89. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/examples/django/bookstore/fujin.toml +0 -0
  90. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/examples/django/bookstore/manage.py +0 -0
  91. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/examples/django/bookstore/pyproject.toml +0 -0
  92. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/examples/django/bookstore/requirements.txt +0 -0
  93. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/justfile +0 -0
  94. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/plugins/fujin-secrets-1password/README.md +0 -0
  95. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/plugins/fujin-secrets-1password/src/fujin_secrets_1password/__init__.py +0 -0
  96. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/plugins/fujin-secrets-1password/src/fujin_secrets_1password/py.typed +0 -0
  97. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/plugins/fujin-secrets-bitwarden/README.md +0 -0
  98. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/plugins/fujin-secrets-bitwarden/src/fujin_secrets_bitwarden/__init__.py +0 -0
  99. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/plugins/fujin-secrets-bitwarden/src/fujin_secrets_bitwarden/py.typed +0 -0
  100. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/plugins/fujin-secrets-doppler/README.md +0 -0
  101. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/plugins/fujin-secrets-doppler/src/fujin_secrets_doppler/__init__.py +0 -0
  102. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/plugins/fujin-secrets-doppler/src/fujin_secrets_doppler/py.typed +0 -0
  103. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/src/fujin/__main__.py +0 -0
  104. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/src/fujin/audit.py +0 -0
  105. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/src/fujin/caddy.py +0 -0
  106. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/src/fujin/commands/__init__.py +0 -0
  107. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/src/fujin/commands/_base.py +0 -0
  108. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/src/fujin/commands/app.py +0 -0
  109. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/src/fujin/commands/audit.py +0 -0
  110. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/src/fujin/commands/down.py +0 -0
  111. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/src/fujin/commands/init.py +0 -0
  112. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/src/fujin/commands/migrate.py +0 -0
  113. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/src/fujin/commands/new.py +0 -0
  114. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/src/fujin/commands/prune.py +0 -0
  115. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/src/fujin/commands/rollback.py +0 -0
  116. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/src/fujin/commands/server.py +0 -0
  117. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/src/fujin/commands/showenv.py +0 -0
  118. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/src/fujin/commands/up.py +0 -0
  119. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/src/fujin/config.py +0 -0
  120. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/src/fujin/connection.py +0 -0
  121. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/src/fujin/discovery.py +0 -0
  122. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/src/fujin/errors.py +0 -0
  123. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/src/fujin/fa.py +0 -0
  124. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/src/fujin/formatting.py +0 -0
  125. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/src/fujin/secrets.py +0 -0
  126. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/src/fujin/templates.py +0 -0
  127. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/tests/__init__.py +0 -0
  128. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/tests/conftest.py +0 -0
  129. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/tests/integration/Dockerfile +0 -0
  130. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/tests/integration/__init__.py +0 -0
  131. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/tests/integration/conftest.py +0 -0
  132. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/tests/integration/helpers.py +0 -0
  133. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/tests/integration/test_app_management.py +0 -0
  134. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/tests/integration/test_full_deploy.py +0 -0
  135. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/tests/integration/test_installation.py +0 -0
  136. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/tests/integration/test_server_bootstrap.py +0 -0
  137. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/tests/test_app.py +0 -0
  138. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/tests/test_audit.py +0 -0
  139. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/tests/test_caddy_domain.py +0 -0
  140. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/tests/test_config.py +0 -0
  141. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/tests/test_connection.py +0 -0
  142. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/tests/test_deploy.py +0 -0
  143. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/tests/test_discovery.py +0 -0
  144. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/tests/test_down.py +0 -0
  145. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/tests/test_init.py +0 -0
  146. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/tests/test_new.py +0 -0
  147. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/tests/test_prune.py +0 -0
  148. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/tests/test_rollback.py +0 -0
  149. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/tests/test_scale.py +0 -0
  150. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/tests/test_secrets.py +0 -0
  151. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/tests/test_server.py +0 -0
  152. {fujin_cli-0.22.2 → fujin_cli-0.23.0}/tests/test_up.py +0 -0
@@ -4,6 +4,13 @@ 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.23.0] - 2026-03-21
8
+
9
+ ### 🚀 Features
10
+
11
+ - Stream .env directly to server instead of bundling
12
+ - Add --bundle-dir option to preserve deployment bundle
13
+
7
14
  ## [0.22.2] - 2026-03-21
8
15
 
9
16
  ### 🐛 Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fujin-cli
3
- Version: 0.22.2
3
+ Version: 0.23.0
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.22.2"
7
+ version = "0.23.0"
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.22.2",
23
+ "fujin-cli>=0.23",
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.22.2"
7
+ version = "0.23.0"
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.22.2",
23
+ "fujin-cli>=0.23",
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.22.2"
7
+ version = "0.23.0"
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.22.2",
23
+ "fujin-cli>=0.23",
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.22.2"
8
+ version = "0.23.0"
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 = [
@@ -149,7 +149,7 @@ markers = [
149
149
  ]
150
150
 
151
151
  [tool.bumpversion]
152
- current_version = "0.22.2"
152
+ current_version = "0.23.0"
153
153
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
154
154
  serialize = [ "{major}.{minor}.{patch}" ]
155
155
  search = "{current_version}"
@@ -0,0 +1 @@
1
+ __version__ = "0.23.0"
@@ -135,13 +135,6 @@ def install(
135
135
  install_dir = app_dir / ".install"
136
136
  install_dir.mkdir(exist_ok=True)
137
137
 
138
- # Move .env file to .install/
139
- env_file = bundle_dir / ".env"
140
- if env_file.exists():
141
- shutil.move(env_file, install_dir / ".env")
142
- env_file = install_dir / ".env"
143
- logger.debug("Moved .env to %s", env_file)
144
-
145
138
  # ==========================================================================
146
139
  # PHASE 2: INSTALLATION
147
140
  # ==========================================================================
@@ -221,7 +214,6 @@ export -f {config.app_name}
221
214
  run(f"chown -R {config.deploy_user}:{config.app_user} {install_dir}")
222
215
  # Make .install directory group-writable (deploy user can update, app user can read)
223
216
  install_dir.chmod(0o775)
224
- env_file.chmod(0o640)
225
217
 
226
218
  # .venv permissions: readable/executable by group, writable by owner
227
219
  # Use chmod -R with symbolic modes to traverse once instead of 3 find commands
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import base64
3
4
  import json
4
5
  import logging
5
6
  import shlex
@@ -8,9 +9,10 @@ import hashlib
8
9
  import subprocess
9
10
  import tempfile
10
11
  import zipapp
12
+ from contextlib import contextmanager
11
13
  from dataclasses import dataclass
12
14
  from pathlib import Path
13
- from typing import Annotated
15
+ from typing import Annotated, Generator
14
16
 
15
17
  import cappa
16
18
  from rich.console import Console
@@ -61,6 +63,13 @@ class Deploy(BaseCommand):
61
63
  help="Disable automatic rollback on deployment failure",
62
64
  ),
63
65
  ] = False
66
+ bundle_dir: Annotated[
67
+ Path | None,
68
+ cappa.Arg(
69
+ long="--bundle-dir",
70
+ help="Directory to create bundle in (preserved after deploy, fails if exists)",
71
+ ),
72
+ ] = None
64
73
 
65
74
  def __call__(self):
66
75
  logger.debug("Starting deployment for %s", self.config.app_name)
@@ -124,7 +133,7 @@ class Deploy(BaseCommand):
124
133
  if not self.config.deployed_units:
125
134
  raise DeploymentError("No systemd units found, nothing to deploy")
126
135
 
127
- with tempfile.TemporaryDirectory() as tmpdir:
136
+ with self._bundle_directory() as tmpdir:
128
137
  self.output.info("Preparing deployment bundle...")
129
138
  zipapp_dir = Path(tmpdir) / "zipapp_source"
130
139
  zipapp_dir.mkdir()
@@ -162,10 +171,9 @@ class Deploy(BaseCommand):
162
171
  # Track unresolved variables across all files
163
172
  all_unresolved = set()
164
173
 
165
- # resolve and copy env file
174
+ # Resolve env file (uploaded separately, not bundled)
166
175
  resolved_env, unresolved = safe_format(parsed_env, **context)
167
176
  all_unresolved.update(unresolved)
168
- (zipapp_dir / ".env").write_text(resolved_env)
169
177
 
170
178
  logger.debug("Validating and resolving systemd units")
171
179
  systemd_dir = zipapp_dir / "systemd"
@@ -351,6 +359,23 @@ class Deploy(BaseCommand):
351
359
  conn.put(str(zipapp_path), remote_bundle_path_q, verify=True)
352
360
 
353
361
  self.output.success("Bundle uploaded successfully.")
362
+
363
+ # Write .env file directly (not bundled for security)
364
+ remote_env_path = f"{self.config.install_dir}/.env"
365
+ remote_env_path_q = shlex.quote(remote_env_path)
366
+ install_dir_q = shlex.quote(self.config.install_dir)
367
+ logger.debug("Writing .env to %s", remote_env_path)
368
+
369
+ # Use base64 encoding to safely transfer content with special chars
370
+ encoded_env = base64.b64encode(resolved_env.encode()).decode()
371
+ conn.run(
372
+ f"mkdir -p {install_dir_q} && "
373
+ f"echo {shlex.quote(encoded_env)} | base64 -d > {remote_env_path_q} && "
374
+ f"chmod 640 {remote_env_path_q} && "
375
+ f"chown {self.selected_host.user}:{self.config.app_user} {remote_env_path_q}",
376
+ hide=True,
377
+ )
378
+
354
379
  self.output.info("Executing remote installation...")
355
380
 
356
381
  rollback_ran = False
@@ -425,6 +450,26 @@ class Deploy(BaseCommand):
425
450
  url = f"https://{domain}"
426
451
  self.output.info(f"Application is available at: {url}")
427
452
 
453
+ @contextmanager
454
+ def _bundle_directory(self) -> Generator[Path, None, None]:
455
+ """Context manager for bundle directory.
456
+
457
+ If bundle_dir is specified, uses that directory (fails if exists).
458
+ Otherwise, creates a temporary directory that is cleaned up on exit.
459
+ """
460
+ if self.bundle_dir:
461
+ if self.bundle_dir.exists():
462
+ raise DeploymentError(
463
+ f"Bundle directory already exists: {self.bundle_dir}"
464
+ )
465
+ self.bundle_dir.mkdir(parents=True)
466
+ logger.debug("Using bundle directory: %s", self.bundle_dir)
467
+ yield self.bundle_dir
468
+ self.output.info(f"Bundle preserved in: {self.bundle_dir}")
469
+ else:
470
+ with tempfile.TemporaryDirectory() as tmpdir:
471
+ yield Path(tmpdir)
472
+
428
473
  def _show_deployment_summary(self, bundle_size: int, bundle_version: str):
429
474
  console = Console()
430
475
 
@@ -345,7 +345,7 @@ wheels = [
345
345
 
346
346
  [[package]]
347
347
  name = "fujin-cli"
348
- version = "0.22.0"
348
+ version = "0.22.2"
349
349
  source = { editable = "." }
350
350
  dependencies = [
351
351
  { name = "cappa" },
@@ -401,7 +401,7 @@ docs = [
401
401
 
402
402
  [[package]]
403
403
  name = "fujin-secrets-1password"
404
- version = "0.22.0"
404
+ version = "0.22.2"
405
405
  source = { editable = "plugins/fujin-secrets-1password" }
406
406
  dependencies = [
407
407
  { name = "fujin-cli" },
@@ -416,7 +416,7 @@ requires-dist = [
416
416
 
417
417
  [[package]]
418
418
  name = "fujin-secrets-bitwarden"
419
- version = "0.22.0"
419
+ version = "0.22.2"
420
420
  source = { editable = "plugins/fujin-secrets-bitwarden" }
421
421
  dependencies = [
422
422
  { name = "fujin-cli" },
@@ -431,7 +431,7 @@ requires-dist = [
431
431
 
432
432
  [[package]]
433
433
  name = "fujin-secrets-doppler"
434
- version = "0.22.0"
434
+ version = "0.22.2"
435
435
  source = { editable = "plugins/fujin-secrets-doppler" }
436
436
  dependencies = [
437
437
  { name = "fujin-cli" },
@@ -1 +0,0 @@
1
- __version__ = "0.22.2"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes