flock-core 0.5.0b28__py3-none-any.whl → 0.5.56b0__py3-none-any.whl

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.

Potentially problematic release.


This version of flock-core might be problematic. Click here for more details.

Files changed (359) hide show
  1. flock/__init__.py +12 -217
  2. flock/agent.py +678 -0
  3. flock/api/themes.py +71 -0
  4. flock/artifacts.py +79 -0
  5. flock/cli.py +75 -0
  6. flock/components.py +173 -0
  7. flock/dashboard/__init__.py +28 -0
  8. flock/dashboard/collector.py +283 -0
  9. flock/dashboard/events.py +182 -0
  10. flock/dashboard/launcher.py +230 -0
  11. flock/dashboard/service.py +537 -0
  12. flock/dashboard/websocket.py +235 -0
  13. flock/engines/__init__.py +6 -0
  14. flock/engines/dspy_engine.py +856 -0
  15. flock/examples.py +128 -0
  16. flock/{core/util → helper}/cli_helper.py +4 -3
  17. flock/{core/logging → logging}/__init__.py +2 -3
  18. flock/{core/logging → logging}/formatters/enum_builder.py +3 -4
  19. flock/{core/logging → logging}/formatters/theme_builder.py +19 -44
  20. flock/{core/logging → logging}/formatters/themed_formatter.py +69 -115
  21. flock/{core/logging → logging}/logging.py +77 -61
  22. flock/{core/logging → logging}/telemetry.py +20 -26
  23. flock/{core/logging → logging}/telemetry_exporter/base_exporter.py +2 -2
  24. flock/{core/logging → logging}/telemetry_exporter/file_exporter.py +6 -9
  25. flock/{core/logging → logging}/telemetry_exporter/sqlite_exporter.py +2 -3
  26. flock/{core/logging → logging}/trace_and_logged.py +20 -24
  27. flock/mcp/__init__.py +91 -0
  28. flock/{core/mcp/mcp_client.py → mcp/client.py} +103 -154
  29. flock/{core/mcp/mcp_config.py → mcp/config.py} +62 -117
  30. flock/mcp/manager.py +255 -0
  31. flock/mcp/servers/sse/__init__.py +1 -1
  32. flock/mcp/servers/sse/flock_sse_server.py +11 -53
  33. flock/mcp/servers/stdio/__init__.py +1 -1
  34. flock/mcp/servers/stdio/flock_stdio_server.py +8 -48
  35. flock/mcp/servers/streamable_http/flock_streamable_http_server.py +17 -62
  36. flock/mcp/servers/websockets/flock_websocket_server.py +7 -40
  37. flock/{core/mcp/flock_mcp_tool.py → mcp/tool.py} +16 -26
  38. flock/mcp/types/__init__.py +42 -0
  39. flock/{core/mcp → mcp}/types/callbacks.py +9 -15
  40. flock/{core/mcp → mcp}/types/factories.py +7 -6
  41. flock/{core/mcp → mcp}/types/handlers.py +13 -18
  42. flock/{core/mcp → mcp}/types/types.py +70 -74
  43. flock/{core/mcp → mcp}/util/helpers.py +1 -1
  44. flock/orchestrator.py +645 -0
  45. flock/registry.py +148 -0
  46. flock/runtime.py +262 -0
  47. flock/service.py +140 -0
  48. flock/store.py +69 -0
  49. flock/subscription.py +111 -0
  50. flock/themes/andromeda.toml +1 -1
  51. flock/themes/apple-system-colors.toml +1 -1
  52. flock/themes/arcoiris.toml +1 -1
  53. flock/themes/atomonelight.toml +1 -1
  54. flock/themes/ayu copy.toml +1 -1
  55. flock/themes/ayu-light.toml +1 -1
  56. flock/themes/belafonte-day.toml +1 -1
  57. flock/themes/belafonte-night.toml +1 -1
  58. flock/themes/blulocodark.toml +1 -1
  59. flock/themes/breeze.toml +1 -1
  60. flock/themes/broadcast.toml +1 -1
  61. flock/themes/brogrammer.toml +1 -1
  62. flock/themes/builtin-dark.toml +1 -1
  63. flock/themes/builtin-pastel-dark.toml +1 -1
  64. flock/themes/catppuccin-latte.toml +1 -1
  65. flock/themes/catppuccin-macchiato.toml +1 -1
  66. flock/themes/catppuccin-mocha.toml +1 -1
  67. flock/themes/cga.toml +1 -1
  68. flock/themes/chalk.toml +1 -1
  69. flock/themes/ciapre.toml +1 -1
  70. flock/themes/coffee-theme.toml +1 -1
  71. flock/themes/cyberpunkscarletprotocol.toml +1 -1
  72. flock/themes/dark+.toml +1 -1
  73. flock/themes/darkermatrix.toml +1 -1
  74. flock/themes/darkside.toml +1 -1
  75. flock/themes/desert.toml +1 -1
  76. flock/themes/django.toml +1 -1
  77. flock/themes/djangosmooth.toml +1 -1
  78. flock/themes/doomone.toml +1 -1
  79. flock/themes/dotgov.toml +1 -1
  80. flock/themes/dracula+.toml +1 -1
  81. flock/themes/duckbones.toml +1 -1
  82. flock/themes/encom.toml +1 -1
  83. flock/themes/espresso.toml +1 -1
  84. flock/themes/everblush.toml +1 -1
  85. flock/themes/fairyfloss.toml +1 -1
  86. flock/themes/fideloper.toml +1 -1
  87. flock/themes/fishtank.toml +1 -1
  88. flock/themes/flexoki-light.toml +1 -1
  89. flock/themes/floraverse.toml +1 -1
  90. flock/themes/framer.toml +1 -1
  91. flock/themes/galizur.toml +1 -1
  92. flock/themes/github.toml +1 -1
  93. flock/themes/grass.toml +1 -1
  94. flock/themes/grey-green.toml +1 -1
  95. flock/themes/gruvboxlight.toml +1 -1
  96. flock/themes/guezwhoz.toml +1 -1
  97. flock/themes/harper.toml +1 -1
  98. flock/themes/hax0r-blue.toml +1 -1
  99. flock/themes/hopscotch.256.toml +1 -1
  100. flock/themes/ic-green-ppl.toml +1 -1
  101. flock/themes/iceberg-dark.toml +1 -1
  102. flock/themes/japanesque.toml +1 -1
  103. flock/themes/jubi.toml +1 -1
  104. flock/themes/kibble.toml +1 -1
  105. flock/themes/kolorit.toml +1 -1
  106. flock/themes/kurokula.toml +1 -1
  107. flock/themes/materialdesigncolors.toml +1 -1
  108. flock/themes/matrix.toml +1 -1
  109. flock/themes/mellifluous.toml +1 -1
  110. flock/themes/midnight-in-mojave.toml +1 -1
  111. flock/themes/monokai-remastered.toml +1 -1
  112. flock/themes/monokai-soda.toml +1 -1
  113. flock/themes/neon.toml +1 -1
  114. flock/themes/neopolitan.toml +1 -1
  115. flock/themes/nord-light.toml +1 -1
  116. flock/themes/ocean.toml +1 -1
  117. flock/themes/onehalfdark.toml +1 -1
  118. flock/themes/onehalflight.toml +1 -1
  119. flock/themes/palenighthc.toml +1 -1
  120. flock/themes/paulmillr.toml +1 -1
  121. flock/themes/pencildark.toml +1 -1
  122. flock/themes/pnevma.toml +1 -1
  123. flock/themes/purple-rain.toml +1 -1
  124. flock/themes/purplepeter.toml +1 -1
  125. flock/themes/raycast-dark.toml +1 -1
  126. flock/themes/red-sands.toml +1 -1
  127. flock/themes/relaxed.toml +1 -1
  128. flock/themes/retro.toml +1 -1
  129. flock/themes/rose-pine.toml +1 -1
  130. flock/themes/royal.toml +1 -1
  131. flock/themes/ryuuko.toml +1 -1
  132. flock/themes/sakura.toml +1 -1
  133. flock/themes/scarlet-protocol.toml +1 -1
  134. flock/themes/seoulbones-dark.toml +1 -1
  135. flock/themes/shades-of-purple.toml +1 -1
  136. flock/themes/smyck.toml +1 -1
  137. flock/themes/softserver.toml +1 -1
  138. flock/themes/solarized-darcula.toml +1 -1
  139. flock/themes/square.toml +1 -1
  140. flock/themes/sugarplum.toml +1 -1
  141. flock/themes/thayer-bright.toml +1 -1
  142. flock/themes/tokyonight.toml +1 -1
  143. flock/themes/tomorrow.toml +1 -1
  144. flock/themes/ubuntu.toml +1 -1
  145. flock/themes/ultradark.toml +1 -1
  146. flock/themes/ultraviolent.toml +1 -1
  147. flock/themes/unikitty.toml +1 -1
  148. flock/themes/urple.toml +1 -1
  149. flock/themes/vesper.toml +1 -1
  150. flock/themes/vimbones.toml +1 -1
  151. flock/themes/wildcherry.toml +1 -1
  152. flock/themes/wilmersdorf.toml +1 -1
  153. flock/themes/wryan.toml +1 -1
  154. flock/themes/xcodedarkhc.toml +1 -1
  155. flock/themes/xcodelight.toml +1 -1
  156. flock/themes/zenbones-light.toml +1 -1
  157. flock/themes/zenwritten-dark.toml +1 -1
  158. flock/utilities.py +301 -0
  159. flock/{components/utility → utility}/output_utility_component.py +68 -53
  160. flock/visibility.py +107 -0
  161. flock_core-0.5.56b0.dist-info/METADATA +747 -0
  162. flock_core-0.5.56b0.dist-info/RECORD +398 -0
  163. flock_core-0.5.56b0.dist-info/entry_points.txt +2 -0
  164. {flock_core-0.5.0b28.dist-info → flock_core-0.5.56b0.dist-info}/licenses/LICENSE +1 -1
  165. flock/adapter/__init__.py +0 -14
  166. flock/adapter/azure_adapter.py +0 -68
  167. flock/adapter/chroma_adapter.py +0 -73
  168. flock/adapter/faiss_adapter.py +0 -97
  169. flock/adapter/pinecone_adapter.py +0 -51
  170. flock/adapter/vector_base.py +0 -47
  171. flock/cli/assets/release_notes.md +0 -140
  172. flock/cli/config.py +0 -8
  173. flock/cli/constants.py +0 -36
  174. flock/cli/create_agent.py +0 -1
  175. flock/cli/create_flock.py +0 -280
  176. flock/cli/execute_flock.py +0 -620
  177. flock/cli/load_agent.py +0 -1
  178. flock/cli/load_examples.py +0 -1
  179. flock/cli/load_flock.py +0 -192
  180. flock/cli/load_release_notes.py +0 -20
  181. flock/cli/loaded_flock_cli.py +0 -254
  182. flock/cli/manage_agents.py +0 -459
  183. flock/cli/registry_management.py +0 -889
  184. flock/cli/runner.py +0 -41
  185. flock/cli/settings.py +0 -857
  186. flock/cli/utils.py +0 -135
  187. flock/cli/view_results.py +0 -29
  188. flock/cli/yaml_editor.py +0 -396
  189. flock/components/__init__.py +0 -30
  190. flock/components/evaluation/__init__.py +0 -9
  191. flock/components/evaluation/declarative_evaluation_component.py +0 -606
  192. flock/components/routing/__init__.py +0 -15
  193. flock/components/routing/conditional_routing_component.py +0 -494
  194. flock/components/routing/default_routing_component.py +0 -103
  195. flock/components/routing/llm_routing_component.py +0 -206
  196. flock/components/utility/__init__.py +0 -22
  197. flock/components/utility/example_utility_component.py +0 -250
  198. flock/components/utility/feedback_utility_component.py +0 -206
  199. flock/components/utility/memory_utility_component.py +0 -550
  200. flock/components/utility/metrics_utility_component.py +0 -700
  201. flock/config.py +0 -61
  202. flock/core/__init__.py +0 -110
  203. flock/core/agent/__init__.py +0 -16
  204. flock/core/agent/default_agent.py +0 -216
  205. flock/core/agent/flock_agent_components.py +0 -104
  206. flock/core/agent/flock_agent_execution.py +0 -101
  207. flock/core/agent/flock_agent_integration.py +0 -260
  208. flock/core/agent/flock_agent_lifecycle.py +0 -186
  209. flock/core/agent/flock_agent_serialization.py +0 -381
  210. flock/core/api/__init__.py +0 -10
  211. flock/core/api/custom_endpoint.py +0 -45
  212. flock/core/api/endpoints.py +0 -254
  213. flock/core/api/main.py +0 -162
  214. flock/core/api/models.py +0 -97
  215. flock/core/api/run_store.py +0 -224
  216. flock/core/api/runner.py +0 -44
  217. flock/core/api/service.py +0 -214
  218. flock/core/component/__init__.py +0 -15
  219. flock/core/component/agent_component_base.py +0 -309
  220. flock/core/component/evaluation_component.py +0 -62
  221. flock/core/component/routing_component.py +0 -74
  222. flock/core/component/utility_component.py +0 -69
  223. flock/core/config/flock_agent_config.py +0 -58
  224. flock/core/config/scheduled_agent_config.py +0 -40
  225. flock/core/context/context.py +0 -213
  226. flock/core/context/context_manager.py +0 -37
  227. flock/core/context/context_vars.py +0 -10
  228. flock/core/evaluation/utils.py +0 -396
  229. flock/core/execution/batch_executor.py +0 -369
  230. flock/core/execution/evaluation_executor.py +0 -438
  231. flock/core/execution/local_executor.py +0 -31
  232. flock/core/execution/opik_executor.py +0 -103
  233. flock/core/execution/temporal_executor.py +0 -164
  234. flock/core/flock.py +0 -634
  235. flock/core/flock_agent.py +0 -336
  236. flock/core/flock_factory.py +0 -613
  237. flock/core/flock_scheduler.py +0 -166
  238. flock/core/flock_server_manager.py +0 -136
  239. flock/core/interpreter/python_interpreter.py +0 -689
  240. flock/core/mcp/__init__.py +0 -1
  241. flock/core/mcp/flock_mcp_server.py +0 -680
  242. flock/core/mcp/mcp_client_manager.py +0 -201
  243. flock/core/mcp/types/__init__.py +0 -1
  244. flock/core/mixin/dspy_integration.py +0 -403
  245. flock/core/mixin/prompt_parser.py +0 -125
  246. flock/core/orchestration/__init__.py +0 -15
  247. flock/core/orchestration/flock_batch_processor.py +0 -94
  248. flock/core/orchestration/flock_evaluator.py +0 -113
  249. flock/core/orchestration/flock_execution.py +0 -295
  250. flock/core/orchestration/flock_initialization.py +0 -149
  251. flock/core/orchestration/flock_server_manager.py +0 -67
  252. flock/core/orchestration/flock_web_server.py +0 -117
  253. flock/core/registry/__init__.py +0 -45
  254. flock/core/registry/agent_registry.py +0 -69
  255. flock/core/registry/callable_registry.py +0 -139
  256. flock/core/registry/component_discovery.py +0 -142
  257. flock/core/registry/component_registry.py +0 -64
  258. flock/core/registry/config_mapping.py +0 -64
  259. flock/core/registry/decorators.py +0 -137
  260. flock/core/registry/registry_hub.py +0 -205
  261. flock/core/registry/server_registry.py +0 -57
  262. flock/core/registry/type_registry.py +0 -86
  263. flock/core/serialization/__init__.py +0 -13
  264. flock/core/serialization/callable_registry.py +0 -52
  265. flock/core/serialization/flock_serializer.py +0 -832
  266. flock/core/serialization/json_encoder.py +0 -41
  267. flock/core/serialization/secure_serializer.py +0 -175
  268. flock/core/serialization/serializable.py +0 -342
  269. flock/core/serialization/serialization_utils.py +0 -412
  270. flock/core/util/file_path_utils.py +0 -223
  271. flock/core/util/hydrator.py +0 -309
  272. flock/core/util/input_resolver.py +0 -164
  273. flock/core/util/loader.py +0 -59
  274. flock/core/util/splitter.py +0 -219
  275. flock/di.py +0 -27
  276. flock/platform/docker_tools.py +0 -49
  277. flock/platform/jaeger_install.py +0 -86
  278. flock/webapp/__init__.py +0 -1
  279. flock/webapp/app/__init__.py +0 -0
  280. flock/webapp/app/api/__init__.py +0 -0
  281. flock/webapp/app/api/agent_management.py +0 -241
  282. flock/webapp/app/api/execution.py +0 -709
  283. flock/webapp/app/api/flock_management.py +0 -129
  284. flock/webapp/app/api/registry_viewer.py +0 -30
  285. flock/webapp/app/chat.py +0 -665
  286. flock/webapp/app/config.py +0 -104
  287. flock/webapp/app/dependencies.py +0 -117
  288. flock/webapp/app/main.py +0 -1070
  289. flock/webapp/app/middleware.py +0 -113
  290. flock/webapp/app/models_ui.py +0 -7
  291. flock/webapp/app/services/__init__.py +0 -0
  292. flock/webapp/app/services/feedback_file_service.py +0 -363
  293. flock/webapp/app/services/flock_service.py +0 -337
  294. flock/webapp/app/services/sharing_models.py +0 -81
  295. flock/webapp/app/services/sharing_store.py +0 -762
  296. flock/webapp/app/templates/theme_mapper.html +0 -326
  297. flock/webapp/app/theme_mapper.py +0 -812
  298. flock/webapp/app/utils.py +0 -85
  299. flock/webapp/run.py +0 -215
  300. flock/webapp/static/css/chat.css +0 -301
  301. flock/webapp/static/css/components.css +0 -167
  302. flock/webapp/static/css/header.css +0 -39
  303. flock/webapp/static/css/layout.css +0 -46
  304. flock/webapp/static/css/sidebar.css +0 -127
  305. flock/webapp/static/css/two-pane.css +0 -48
  306. flock/webapp/templates/base.html +0 -200
  307. flock/webapp/templates/chat.html +0 -152
  308. flock/webapp/templates/chat_settings.html +0 -19
  309. flock/webapp/templates/flock_editor.html +0 -16
  310. flock/webapp/templates/index.html +0 -12
  311. flock/webapp/templates/partials/_agent_detail_form.html +0 -93
  312. flock/webapp/templates/partials/_agent_list.html +0 -18
  313. flock/webapp/templates/partials/_agent_manager_view.html +0 -51
  314. flock/webapp/templates/partials/_agent_tools_checklist.html +0 -14
  315. flock/webapp/templates/partials/_chat_container.html +0 -15
  316. flock/webapp/templates/partials/_chat_messages.html +0 -57
  317. flock/webapp/templates/partials/_chat_settings_form.html +0 -85
  318. flock/webapp/templates/partials/_create_flock_form.html +0 -50
  319. flock/webapp/templates/partials/_dashboard_flock_detail.html +0 -17
  320. flock/webapp/templates/partials/_dashboard_flock_file_list.html +0 -16
  321. flock/webapp/templates/partials/_dashboard_flock_properties_preview.html +0 -28
  322. flock/webapp/templates/partials/_dashboard_upload_flock_form.html +0 -16
  323. flock/webapp/templates/partials/_dynamic_input_form_content.html +0 -22
  324. flock/webapp/templates/partials/_env_vars_table.html +0 -23
  325. flock/webapp/templates/partials/_execution_form.html +0 -118
  326. flock/webapp/templates/partials/_execution_view_container.html +0 -28
  327. flock/webapp/templates/partials/_flock_file_list.html +0 -23
  328. flock/webapp/templates/partials/_flock_properties_form.html +0 -52
  329. flock/webapp/templates/partials/_flock_upload_form.html +0 -16
  330. flock/webapp/templates/partials/_header_flock_status.html +0 -5
  331. flock/webapp/templates/partials/_load_manager_view.html +0 -49
  332. flock/webapp/templates/partials/_registry_table.html +0 -25
  333. flock/webapp/templates/partials/_registry_viewer_content.html +0 -70
  334. flock/webapp/templates/partials/_results_display.html +0 -78
  335. flock/webapp/templates/partials/_settings_env_content.html +0 -9
  336. flock/webapp/templates/partials/_settings_theme_content.html +0 -14
  337. flock/webapp/templates/partials/_settings_view.html +0 -36
  338. flock/webapp/templates/partials/_share_chat_link_snippet.html +0 -11
  339. flock/webapp/templates/partials/_share_link_snippet.html +0 -35
  340. flock/webapp/templates/partials/_sidebar.html +0 -74
  341. flock/webapp/templates/partials/_streaming_results_container.html +0 -195
  342. flock/webapp/templates/partials/_structured_data_view.html +0 -40
  343. flock/webapp/templates/partials/_theme_preview.html +0 -36
  344. flock/webapp/templates/registry_viewer.html +0 -84
  345. flock/webapp/templates/shared_run_page.html +0 -140
  346. flock/workflow/__init__.py +0 -0
  347. flock/workflow/activities.py +0 -196
  348. flock/workflow/agent_activities.py +0 -24
  349. flock/workflow/agent_execution_activity.py +0 -202
  350. flock/workflow/flock_workflow.py +0 -214
  351. flock/workflow/temporal_config.py +0 -96
  352. flock/workflow/temporal_setup.py +0 -68
  353. flock_core-0.5.0b28.dist-info/METADATA +0 -274
  354. flock_core-0.5.0b28.dist-info/RECORD +0 -561
  355. flock_core-0.5.0b28.dist-info/entry_points.txt +0 -2
  356. /flock/{core/logging → logging}/formatters/themes.py +0 -0
  357. /flock/{core/logging → logging}/span_middleware/baggage_span_processor.py +0 -0
  358. /flock/{core/mcp → mcp}/util/__init__.py +0 -0
  359. {flock_core-0.5.0b28.dist-info → flock_core-0.5.56b0.dist-info}/WHEEL +0 -0
flock/cli/settings.py DELETED
@@ -1,857 +0,0 @@
1
- """Settings editor for the Flock CLI.
2
-
3
- This module provides functionality to view, edit, add, and delete
4
- environment variables in the .env file.
5
- """
6
-
7
- import os
8
- import shutil
9
- from pathlib import Path
10
- from typing import Dict, List, Optional, Tuple
11
- import math
12
-
13
- import questionary
14
- from rich.console import Console
15
- from rich.panel import Panel
16
- from rich.table import Table
17
- from rich.text import Text
18
-
19
- from flock.core.util.cli_helper import init_console
20
-
21
- # Constants
22
- ENV_FILE = ".env"
23
- ENV_TEMPLATE_FILE = ".env_template"
24
- ENV_PROFILE_PREFIX = ".env_"
25
- DEFAULT_PROFILE_COMMENT = "# Profile: {profile_name}"
26
- SHOW_SECRETS_KEY = "SHOW_SECRETS"
27
- VARS_PER_PAGE_KEY = "VARS_PER_PAGE"
28
- DEFAULT_VARS_PER_PAGE = 20
29
-
30
- console = Console()
31
-
32
-
33
- def settings_editor():
34
- """Main entry point for the settings editor."""
35
- while True:
36
- init_console()
37
- console.print(Panel("[bold green]Environment Settings Editor[/]"), justify="center")
38
-
39
- # Get current profile name
40
- current_profile = get_current_profile()
41
- if current_profile:
42
- console.print(f"Current Profile: [bold cyan]{current_profile}[/]")
43
- else:
44
- console.print("No profile detected")
45
-
46
- console.line()
47
-
48
- choice = questionary.select(
49
- "What would you like to do?",
50
- choices=[
51
- questionary.Separator(line=" "),
52
- "View all environment variables",
53
- "Edit an environment variable",
54
- "Add a new environment variable",
55
- "Delete an environment variable",
56
- questionary.Separator(),
57
- "Manage environment profiles",
58
- questionary.Separator(),
59
- "Toggle show secrets",
60
- "Change variables per page",
61
- questionary.Separator(),
62
- "Back to main menu",
63
- ],
64
- ).ask()
65
-
66
- if choice == "View all environment variables":
67
- view_env_variables()
68
- elif choice == "Edit an environment variable":
69
- edit_env_variable()
70
- elif choice == "Add a new environment variable":
71
- add_env_variable()
72
- elif choice == "Delete an environment variable":
73
- delete_env_variable()
74
- elif choice == "Manage environment profiles":
75
- manage_profiles()
76
- elif choice == "Toggle show secrets":
77
- toggle_show_secrets()
78
- elif choice == "Change variables per page":
79
- change_vars_per_page()
80
- elif choice == "Back to main menu":
81
- break
82
-
83
- if choice != "Back to main menu":
84
- input("\nPress Enter to continue...")
85
-
86
-
87
- def view_env_variables(page: int = 1, page_size: Optional[int] = None):
88
- """View all environment variables with pagination.
89
-
90
- Args:
91
- page: Page number to display
92
- page_size: Number of variables per page (if None, use the setting in .env)
93
- """
94
- env_vars = load_env_file()
95
-
96
- # If page_size is not specified, get it from settings
97
- if page_size is None:
98
- page_size = get_vars_per_page_setting(env_vars)
99
-
100
- # Calculate pagination
101
- total_vars = len(env_vars)
102
- total_pages = math.ceil(total_vars / page_size) if total_vars > 0 else 1
103
-
104
- # Validate page number
105
- page = min(max(1, page), total_pages)
106
-
107
- start_idx = (page - 1) * page_size
108
- end_idx = min(start_idx + page_size, total_vars)
109
-
110
- # Get current page variables
111
- current_page_vars = list(env_vars.items())[start_idx:end_idx]
112
-
113
- # Check if secrets should be shown
114
- show_secrets = get_show_secrets_setting(env_vars)
115
-
116
- # Create table
117
- table = Table(title=f"Environment Variables (Page {page}/{total_pages}, {page_size} per page)")
118
- table.add_column("Name", style="cyan")
119
- table.add_column("Value", style="green")
120
-
121
- # Show secrets status
122
- secrets_status = "[green]ON[/]" if show_secrets else "[red]OFF[/]"
123
- init_console()
124
- console.print(f"Show Secrets: {secrets_status}")
125
-
126
- for key, value in current_page_vars:
127
- # Skip lines that are comments or empty
128
- if key.startswith('#') or not key:
129
- continue
130
-
131
- # Mask sensitive values if show_secrets is False
132
- if is_sensitive(key) and not show_secrets:
133
- masked_value = mask_sensitive_value(value)
134
- table.add_row(key, masked_value)
135
- else:
136
- table.add_row(key, value)
137
-
138
- console.print(table)
139
-
140
- # Pagination controls with more intuitive shortcuts
141
- console.print("\nNavigation: ", end="")
142
- if page > 1:
143
- console.print("[bold]Previous (p)[/] | ", end="")
144
- if page < total_pages:
145
- console.print("[bold]Next (n)[/] | ", end="")
146
- if show_secrets:
147
- console.print("[bold]Hide secrets (h)[/] | ", end="")
148
- else:
149
- console.print("[bold]Show secrets (s)[/] | ", end="")
150
- console.print("[bold]Change variables per page (v)[/] | ", end="")
151
- console.print("[bold]Back (b)[/]")
152
-
153
- # Handle navigation
154
- while True:
155
- key = input("Enter option: ").lower()
156
- if key == 'p' and page > 1:
157
- view_env_variables(page - 1, page_size)
158
- break
159
- elif key == 'n' and page < total_pages:
160
- view_env_variables(page + 1, page_size)
161
- break
162
- elif key == 's' and not show_secrets:
163
- # Confirm showing secrets
164
- confirm = questionary.confirm("Are you sure you want to show sensitive values?").ask()
165
- if confirm:
166
- set_show_secrets_setting(True)
167
- view_env_variables(page, page_size)
168
- break
169
- elif key == 'h' and show_secrets:
170
- set_show_secrets_setting(False)
171
- view_env_variables(page, page_size)
172
- break
173
- elif key == 'v':
174
- new_page_size = change_vars_per_page()
175
- if new_page_size:
176
- view_env_variables(1, new_page_size) # Reset to first page with new page size
177
- break
178
- elif key == 'b':
179
- break
180
-
181
-
182
- def change_vars_per_page():
183
- """Change the number of variables displayed per page.
184
-
185
- Returns:
186
- The new page size or None if cancelled
187
- """
188
- env_vars = load_env_file()
189
- current_setting = get_vars_per_page_setting(env_vars)
190
-
191
- console.print(f"Current variables per page: [cyan]{current_setting}[/]")
192
-
193
- # Predefined options plus custom option
194
- page_size_options = ["10", "20", "30", "50", "Custom", "Cancel"]
195
-
196
- choice = questionary.select(
197
- "Select number of variables per page:",
198
- choices=page_size_options,
199
- ).ask()
200
-
201
- if choice == "Cancel":
202
- return None
203
-
204
- if choice == "Custom":
205
- while True:
206
- try:
207
- custom_size = questionary.text(
208
- "Enter custom page size (5-100):",
209
- default=str(current_setting)
210
- ).ask()
211
-
212
- if not custom_size:
213
- return None
214
-
215
- new_size = int(custom_size)
216
- if 5 <= new_size <= 100:
217
- break
218
- else:
219
- console.print("[yellow]Page size must be between 5 and 100.[/]")
220
- except ValueError:
221
- console.print("[yellow]Please enter a valid number.[/]")
222
- else:
223
- new_size = int(choice)
224
-
225
- # Save the setting
226
- set_vars_per_page_setting(new_size)
227
- console.print(f"[green]Variables per page set to {new_size}.[/]")
228
-
229
- return new_size
230
-
231
-
232
- def get_vars_per_page_setting(env_vars: Dict[str, str] = None) -> int:
233
- """Get the current variables per page setting.
234
-
235
- Args:
236
- env_vars: Optional dictionary of environment variables
237
-
238
- Returns:
239
- Number of variables per page
240
- """
241
- if env_vars is None:
242
- env_vars = load_env_file()
243
-
244
- if VARS_PER_PAGE_KEY in env_vars:
245
- try:
246
- page_size = int(env_vars[VARS_PER_PAGE_KEY])
247
- # Ensure the value is within reasonable bounds
248
- if 5 <= page_size <= 100:
249
- return page_size
250
- except ValueError:
251
- pass
252
-
253
- return DEFAULT_VARS_PER_PAGE
254
-
255
-
256
- def set_vars_per_page_setting(page_size: int):
257
- """Set the variables per page setting.
258
-
259
- Args:
260
- page_size: Number of variables to display per page
261
- """
262
- env_vars = load_env_file()
263
- env_vars[VARS_PER_PAGE_KEY] = str(page_size)
264
- save_env_file(env_vars)
265
-
266
-
267
- def toggle_show_secrets():
268
- """Toggle the show secrets setting."""
269
- env_vars = load_env_file()
270
- current_setting = get_show_secrets_setting(env_vars)
271
-
272
- if current_setting:
273
- console.print("Currently showing sensitive values. Do you want to hide them?")
274
- confirm = questionary.confirm("Hide sensitive values?").ask()
275
- if confirm:
276
- set_show_secrets_setting(False)
277
- console.print("[green]Sensitive values will now be masked.[/]")
278
- else:
279
- console.print("[yellow]Warning:[/] Showing sensitive values can expose sensitive information.")
280
- confirm = questionary.confirm("Are you sure you want to show sensitive values?").ask()
281
- if confirm:
282
- set_show_secrets_setting(True)
283
- console.print("[green]Sensitive values will now be shown.[/]")
284
-
285
-
286
- def get_show_secrets_setting(env_vars: Dict[str, str] = None) -> bool:
287
- """Get the current show secrets setting.
288
-
289
- Args:
290
- env_vars: Optional dictionary of environment variables
291
-
292
- Returns:
293
- True if secrets should be shown, False otherwise
294
- """
295
- if env_vars is None:
296
- env_vars = load_env_file()
297
-
298
- if SHOW_SECRETS_KEY in env_vars:
299
- return env_vars[SHOW_SECRETS_KEY].lower() == 'true'
300
-
301
- return False
302
-
303
-
304
- def set_show_secrets_setting(show_secrets: bool):
305
- """Set the show secrets setting.
306
-
307
- Args:
308
- show_secrets: Whether to show secrets
309
- """
310
- env_vars = load_env_file()
311
- env_vars[SHOW_SECRETS_KEY] = str(show_secrets)
312
- save_env_file(env_vars)
313
-
314
-
315
- def edit_env_variable():
316
- """Edit an environment variable."""
317
- # Get list of variables
318
- env_vars = load_env_file()
319
-
320
- if not env_vars:
321
- console.print("[yellow]No environment variables found to edit.[/]")
322
- return
323
-
324
- # Filter out comments
325
- variables = [k for k in env_vars.keys() if not k.startswith('#') and k]
326
-
327
- # Display variables with selection
328
- init_console()
329
- console.print("Select a variable to edit:")
330
-
331
- # Let user select a variable to edit
332
- var_name = questionary.select(
333
- "Select a variable to edit:",
334
- choices=variables + ["Cancel"],
335
- ).ask()
336
-
337
- if var_name == "Cancel":
338
- return
339
-
340
- current_value = env_vars[var_name]
341
- is_sensitive_var = is_sensitive(var_name)
342
-
343
- if is_sensitive_var:
344
- console.print(f"[yellow]Warning:[/] You are editing a sensitive value: {var_name}")
345
- confirm = questionary.confirm("Are you sure you want to continue?").ask()
346
- if not confirm:
347
- return
348
-
349
- # Show current value (masked if sensitive and show_secrets is False)
350
- show_secrets = get_show_secrets_setting(env_vars)
351
- if is_sensitive_var and not show_secrets:
352
- console.print(f"Current value: {mask_sensitive_value(current_value)}")
353
- else:
354
- console.print(f"Current value: {current_value}")
355
-
356
- # Get new value with hint
357
- console.print("[italic]Enter new value (or leave empty to cancel)[/]")
358
- new_value = questionary.text("Enter new value:", default=current_value).ask()
359
-
360
- if new_value is None:
361
- console.print("[yellow]Edit cancelled.[/]")
362
- return
363
-
364
- if new_value == "":
365
- # Confirm if user wants to set an empty value or cancel
366
- confirm = questionary.confirm("Do you want to set an empty value? Select No to cancel.", default=False).ask()
367
- if not confirm:
368
- console.print("[yellow]Edit cancelled.[/]")
369
- return
370
-
371
- if new_value == current_value:
372
- console.print("[yellow]No changes made.[/]")
373
- return
374
-
375
- # Update the value
376
- env_vars[var_name] = new_value
377
- save_env_file(env_vars)
378
- console.print(f"[green]Updated {var_name} successfully.[/]")
379
-
380
-
381
- def add_env_variable():
382
- """Add a new environment variable."""
383
- env_vars = load_env_file()
384
-
385
- console.print("[italic]Enter variable name (or leave empty to go back)[/]")
386
-
387
- # Get variable name
388
- while True:
389
- var_name = questionary.text("Enter variable name:").ask()
390
-
391
- if not var_name:
392
- # Ask if user wants to go back
393
- go_back = questionary.confirm("Do you want to go back to the settings menu?", default=True).ask()
394
- if go_back:
395
- return
396
- else:
397
- console.print("[italic]Please enter a variable name (or leave empty to go back)[/]")
398
- continue
399
-
400
- if var_name in env_vars and not var_name.startswith('#'):
401
- console.print(f"[yellow]Variable {var_name} already exists. Please use edit instead.[/]")
402
- continue
403
-
404
- break
405
-
406
- # Get variable value
407
- var_value = questionary.text("Enter variable value:").ask()
408
-
409
- # Add to env_vars
410
- env_vars[var_name] = var_value
411
- save_env_file(env_vars)
412
- console.print(f"[green]Added {var_name} successfully.[/]")
413
-
414
-
415
- def delete_env_variable():
416
- """Delete an environment variable."""
417
- # Get list of variables
418
- env_vars = load_env_file()
419
-
420
- if not env_vars:
421
- console.print("[yellow]No environment variables found to delete.[/]")
422
- return
423
-
424
- # Filter out comments
425
- variables = [k for k in env_vars.keys() if not k.startswith('#') and k]
426
-
427
- # Display variables with selection
428
- init_console()
429
- console.print("Select a variable to delete:")
430
-
431
- # Let user select a variable to delete with hint
432
- var_name = questionary.select(
433
- "Select a variable to delete:",
434
- choices=variables + ["Cancel"],
435
- ).ask()
436
-
437
- if var_name == "Cancel":
438
- return
439
-
440
- # Confirm deletion
441
- confirm = questionary.confirm(f"Are you sure you want to delete {var_name}?").ask()
442
- if not confirm:
443
- console.print("[yellow]Deletion cancelled.[/]")
444
- return
445
-
446
- # Delete the variable
447
- del env_vars[var_name]
448
- save_env_file(env_vars)
449
- console.print(f"[green]Deleted {var_name} successfully.[/]")
450
-
451
-
452
- def manage_profiles():
453
- """Manage environment profiles."""
454
- init_console()
455
- console.print(Panel("[bold green]Environment Profile Management[/]"), justify="center")
456
-
457
- # Get current profile and available profiles
458
- current_profile = get_current_profile()
459
- available_profiles = get_available_profiles()
460
-
461
- if current_profile:
462
- console.print(f"Current Profile: [bold cyan]{current_profile}[/]")
463
-
464
- if not available_profiles:
465
- console.print("[yellow]No profiles found.[/]")
466
- else:
467
- console.print("Available Profiles:")
468
- for profile in available_profiles:
469
- if profile == current_profile:
470
- console.print(f" [bold cyan]{profile} (active)[/]")
471
- else:
472
- console.print(f" {profile}")
473
-
474
- console.line()
475
-
476
- # Profile management options
477
- choice = questionary.select(
478
- "What would you like to do?",
479
- choices=[
480
- questionary.Separator(line=" "),
481
- "Switch to a different profile",
482
- "Create a new profile",
483
- "Rename a profile",
484
- "Delete a profile",
485
- "Back to settings menu",
486
- ],
487
- ).ask()
488
-
489
- if choice == "Switch to a different profile":
490
- switch_profile()
491
- elif choice == "Create a new profile":
492
- create_profile()
493
- elif choice == "Rename a profile":
494
- rename_profile()
495
- elif choice == "Delete a profile":
496
- delete_profile()
497
-
498
-
499
- def switch_profile():
500
- """Switch to a different environment profile."""
501
- available_profiles = get_available_profiles()
502
- current_profile = get_current_profile()
503
-
504
- if not available_profiles:
505
- console.print("[yellow]No profiles available to switch to.[/]")
506
- return
507
-
508
- # Remove current profile from the list to avoid switching to the same profile
509
- selectable_profiles = [p for p in available_profiles if p != current_profile]
510
-
511
- if not selectable_profiles:
512
- console.print("[yellow]No other profiles available to switch to.[/]")
513
- return
514
-
515
- target_profile = questionary.select(
516
- "Select a profile to switch to:",
517
- choices=selectable_profiles + ["Cancel"],
518
- ).ask()
519
-
520
- if target_profile == "Cancel":
521
- return
522
-
523
- # Confirm switch
524
- confirm = questionary.confirm(f"Are you sure you want to switch to the {target_profile} profile?").ask()
525
- if not confirm:
526
- return
527
-
528
- # Backup current .env file
529
- backup_env_file()
530
-
531
- # Copy selected profile to .env
532
- source_file = f"{ENV_PROFILE_PREFIX}{target_profile}"
533
- if os.path.exists(source_file):
534
- shutil.copy2(source_file, ENV_FILE)
535
- console.print(f"[green]Switched to {target_profile} profile successfully.[/]")
536
- else:
537
- console.print(f"[red]Error: Could not find profile file {source_file}.[/]")
538
-
539
-
540
- def create_profile():
541
- """Create a new environment profile."""
542
- profile_name = questionary.text("Enter new profile name:").ask()
543
-
544
- if not profile_name:
545
- console.print("[yellow]Profile name cannot be empty.[/]")
546
- return
547
-
548
- # Check if profile already exists
549
- target_file = f"{ENV_PROFILE_PREFIX}{profile_name}"
550
- if os.path.exists(target_file):
551
- console.print(f"[yellow]Profile {profile_name} already exists.[/]")
552
- return
553
-
554
- # Determine source file - use current .env or template
555
- source_choices = ["Current environment (.env)", ".env_template"]
556
- if os.path.exists(ENV_TEMPLATE_FILE):
557
- source_choices.append(ENV_TEMPLATE_FILE)
558
-
559
- source_choice = questionary.select(
560
- "Create profile based on:",
561
- choices=source_choices + ["Cancel"],
562
- ).ask()
563
-
564
- if source_choice == "Cancel":
565
- return
566
-
567
- source_file = ENV_FILE if source_choice == "Current environment (.env)" else ENV_TEMPLATE_FILE
568
-
569
- if not os.path.exists(source_file):
570
- console.print(f"[red]Error: Source file {source_file} not found.[/]")
571
- return
572
-
573
- # Create new profile file
574
- try:
575
- # Copy source file
576
- shutil.copy2(source_file, target_file)
577
-
578
- # Add profile header if it doesn't exist
579
- with open(target_file, 'r') as file:
580
- content = file.read()
581
-
582
- if not content.startswith("# Profile:"):
583
- with open(target_file, 'w') as file:
584
- profile_header = DEFAULT_PROFILE_COMMENT.format(profile_name=profile_name)
585
- file.write(f"{profile_header}\n{content}")
586
-
587
- console.print(f"[green]Created {profile_name} profile successfully.[/]")
588
- except Exception as e:
589
- console.print(f"[red]Error creating profile: {str(e)}[/]")
590
-
591
-
592
- def rename_profile():
593
- """Rename an existing profile."""
594
- available_profiles = get_available_profiles()
595
- current_profile = get_current_profile()
596
-
597
- if not available_profiles:
598
- console.print("[yellow]No profiles available to rename.[/]")
599
- return
600
-
601
- # Let user select a profile to rename
602
- profile_to_rename = questionary.select(
603
- "Select a profile to rename:",
604
- choices=available_profiles + ["Cancel"],
605
- ).ask()
606
-
607
- if profile_to_rename == "Cancel":
608
- return
609
-
610
- # Get new name
611
- new_name = questionary.text("Enter new profile name:").ask()
612
-
613
- if not new_name:
614
- console.print("[yellow]New profile name cannot be empty.[/]")
615
- return
616
-
617
- if new_name in available_profiles:
618
- console.print(f"[yellow]Profile {new_name} already exists.[/]")
619
- return
620
-
621
- # Rename profile file
622
- source_file = f"{ENV_PROFILE_PREFIX}{profile_to_rename}"
623
- target_file = f"{ENV_PROFILE_PREFIX}{new_name}"
624
-
625
- try:
626
- # Read content of the source file
627
- with open(source_file, 'r') as file:
628
- content = file.readlines()
629
-
630
- # Update profile header if it exists
631
- if content and content[0].startswith("# Profile:"):
632
- content[0] = DEFAULT_PROFILE_COMMENT.format(profile_name=new_name) + "\n"
633
-
634
- # Write to new file
635
- with open(target_file, 'w') as file:
636
- file.writelines(content)
637
-
638
- # Remove old file
639
- os.remove(source_file)
640
-
641
- # If this was the current profile, update .env as well
642
- if profile_to_rename == current_profile:
643
- with open(ENV_FILE, 'r') as file:
644
- content = file.readlines()
645
-
646
- if content and content[0].startswith("# Profile:"):
647
- content[0] = DEFAULT_PROFILE_COMMENT.format(profile_name=new_name) + "\n"
648
-
649
- with open(ENV_FILE, 'w') as file:
650
- file.writelines(content)
651
-
652
- console.print(f"[green]Renamed {profile_to_rename} to {new_name} successfully.[/]")
653
- except Exception as e:
654
- console.print(f"[red]Error renaming profile: {str(e)}[/]")
655
-
656
-
657
- def delete_profile():
658
- """Delete an existing profile."""
659
- available_profiles = get_available_profiles()
660
- current_profile = get_current_profile()
661
-
662
- if not available_profiles:
663
- console.print("[yellow]No profiles available to delete.[/]")
664
- return
665
-
666
- # Let user select a profile to delete
667
- profile_to_delete = questionary.select(
668
- "Select a profile to delete:",
669
- choices=available_profiles + ["Cancel"],
670
- ).ask()
671
-
672
- if profile_to_delete == "Cancel":
673
- return
674
-
675
- # Confirm deletion
676
- confirm = questionary.confirm(
677
- f"Are you sure you want to delete the {profile_to_delete} profile? This cannot be undone."
678
- ).ask()
679
-
680
- if not confirm:
681
- return
682
-
683
- # Delete profile file
684
- profile_file = f"{ENV_PROFILE_PREFIX}{profile_to_delete}"
685
-
686
- try:
687
- os.remove(profile_file)
688
-
689
- # Warn if deleting the current profile
690
- if profile_to_delete == current_profile:
691
- console.print(
692
- f"[yellow]Warning: You deleted the currently active profile. "
693
- f"The .env file still contains those settings but is no longer marked as a profile.[/]"
694
- )
695
-
696
- # Remove profile header from .env
697
- with open(ENV_FILE, 'r') as file:
698
- content = file.readlines()
699
-
700
- if content and content[0].startswith("# Profile:"):
701
- content = content[1:]
702
- with open(ENV_FILE, 'w') as file:
703
- file.writelines(content)
704
-
705
- console.print(f"[green]Deleted {profile_to_delete} profile successfully.[/]")
706
- except Exception as e:
707
- console.print(f"[red]Error deleting profile: {str(e)}[/]")
708
-
709
-
710
- def is_sensitive(key: str) -> bool:
711
- """Check if a variable is considered sensitive.
712
-
713
- Args:
714
- key: The variable name
715
-
716
- Returns:
717
- True if sensitive, False otherwise
718
- """
719
- sensitive_patterns = ['key', 'token', 'secret', 'password', 'api', 'pat']
720
- key_lower = key.lower()
721
- return any(pattern in key_lower for pattern in sensitive_patterns)
722
-
723
-
724
- def mask_sensitive_value(value: str) -> str:
725
- """Mask a sensitive value.
726
-
727
- Args:
728
- value: The sensitive value
729
-
730
- Returns:
731
- Masked value
732
- """
733
- if not value:
734
- return value
735
-
736
- if len(value) <= 4:
737
- return "••••"
738
-
739
- # Show first 2 and last 2 characters
740
- return value[:2] + "•" * (len(value) - 4) + value[-2:]
741
-
742
-
743
- def get_current_profile() -> Optional[str]:
744
- """Get the name of the current active profile.
745
-
746
- Returns:
747
- Profile name or None if no profile is active
748
- """
749
- if not os.path.exists(ENV_FILE):
750
- return None
751
-
752
- try:
753
- with open(ENV_FILE, 'r') as file:
754
- first_line = file.readline().strip()
755
-
756
- if first_line.startswith("# Profile:"):
757
- return first_line.replace("# Profile:", "").strip()
758
- except Exception:
759
- pass
760
-
761
- return None
762
-
763
-
764
- def get_available_profiles() -> List[str]:
765
- """Get a list of available profiles.
766
-
767
- Returns:
768
- List of profile names
769
- """
770
- profiles = []
771
-
772
- for file in os.listdir():
773
- if file.startswith(ENV_PROFILE_PREFIX):
774
- profile_name = file[len(ENV_PROFILE_PREFIX):]
775
- profiles.append(profile_name)
776
-
777
- return profiles
778
-
779
-
780
- def backup_env_file():
781
- """Create a backup of the current .env file."""
782
- if not os.path.exists(ENV_FILE):
783
- return
784
-
785
- backup_file = f"{ENV_FILE}.bak"
786
- shutil.copy2(ENV_FILE, backup_file)
787
-
788
-
789
- def load_env_file() -> Dict[str, str]:
790
- """Load the .env file into a dictionary.
791
-
792
- Returns:
793
- Dictionary of environment variables
794
- """
795
- env_vars = {}
796
-
797
- if not os.path.exists(ENV_FILE):
798
- console.print(f"[yellow]Warning: {ENV_FILE} file not found.[/]")
799
- return env_vars
800
-
801
- try:
802
- with open(ENV_FILE, 'r') as file:
803
- lines = file.readlines()
804
-
805
- # Process each line
806
- for line in lines:
807
- line = line.strip()
808
-
809
- # Skip empty lines
810
- if not line:
811
- env_vars[""] = ""
812
- continue
813
-
814
- # Handle comments
815
- if line.startswith('#'):
816
- env_vars[line] = ""
817
- continue
818
-
819
- # Handle regular variables
820
- if '=' in line:
821
- key, value = line.split('=', 1)
822
- env_vars[key] = value
823
- else:
824
- # Handle lines without equals sign
825
- env_vars[line] = ""
826
-
827
- except Exception as e:
828
- console.print(f"[red]Error loading .env file: {str(e)}[/]")
829
-
830
- return env_vars
831
-
832
-
833
- def save_env_file(env_vars: Dict[str, str]):
834
- """Save environment variables back to the .env file.
835
-
836
- Args:
837
- env_vars: Dictionary of environment variables
838
- """
839
- # Create backup
840
- backup_env_file()
841
-
842
- try:
843
- with open(ENV_FILE, 'w') as file:
844
- for key, value in env_vars.items():
845
- if key.startswith('#'):
846
- # Write comments as is
847
- file.write(f"{key}\n")
848
- elif not key:
849
- # Write empty lines
850
- file.write("\n")
851
- else:
852
- # Write regular variables
853
- file.write(f"{key}={value}\n")
854
-
855
- console.print("[green]Settings saved successfully.[/]")
856
- except Exception as e:
857
- console.print(f"[red]Error saving .env file: {str(e)}[/]")