llamactl 0.3.9__tar.gz → 0.3.11__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 (39) hide show
  1. {llamactl-0.3.9 → llamactl-0.3.11}/PKG-INFO +3 -3
  2. {llamactl-0.3.9 → llamactl-0.3.11}/pyproject.toml +3 -3
  3. {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/commands/init.py +38 -23
  4. {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/commands/serve.py +98 -0
  5. {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/config/auth_service.py +2 -5
  6. {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/textual/deployment_form.py +100 -6
  7. {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/textual/deployment_help.py +6 -0
  8. {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/textual/styles.tcss +112 -0
  9. llamactl-0.3.11/src/llama_deploy/cli/utils/redact.py +29 -0
  10. llamactl-0.3.11/src/llama_deploy/cli/utils/version.py +11 -0
  11. {llamactl-0.3.9 → llamactl-0.3.11}/README.md +0 -0
  12. {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/__init__.py +0 -0
  13. {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/app.py +0 -0
  14. {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/auth/client.py +0 -0
  15. {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/client.py +0 -0
  16. {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/commands/aliased_group.py +0 -0
  17. {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/commands/auth.py +0 -0
  18. {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/commands/deployment.py +0 -0
  19. {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/commands/env.py +0 -0
  20. {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/config/_config.py +0 -0
  21. {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/config/_migrations.py +0 -0
  22. {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/config/env_service.py +0 -0
  23. {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/config/migrations/0001_init.sql +0 -0
  24. {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/config/migrations/0002_add_auth_fields.sql +0 -0
  25. {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/config/migrations/__init__.py +0 -0
  26. {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/config/schema.py +0 -0
  27. {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/debug.py +0 -0
  28. {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/env.py +0 -0
  29. {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/interactive_prompts/session_utils.py +0 -0
  30. {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/interactive_prompts/utils.py +0 -0
  31. {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/options.py +0 -0
  32. {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/py.typed +0 -0
  33. {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/styles.py +0 -0
  34. {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/textual/deployment_monitor.py +0 -0
  35. {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/textual/git_validation.py +0 -0
  36. {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/textual/github_callback_server.py +0 -0
  37. {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/textual/llama_loader.py +0 -0
  38. {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/textual/secrets_form.py +0 -0
  39. {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/utils/env_inject.py +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: llamactl
3
- Version: 0.3.9
3
+ Version: 0.3.11
4
4
  Summary: A command-line interface for managing LlamaDeploy projects and deployments
5
5
  Author: Adrian Lyjak
6
6
  Author-email: Adrian Lyjak <adrianlyjak@gmail.com>
7
7
  License: MIT
8
- Requires-Dist: llama-deploy-core[client]>=0.3.9,<0.4.0
9
- Requires-Dist: llama-deploy-appserver>=0.3.9,<0.4.0
8
+ Requires-Dist: llama-deploy-core[client]>=0.3.11,<0.4.0
9
+ Requires-Dist: llama-deploy-appserver>=0.3.11,<0.4.0
10
10
  Requires-Dist: httpx>=0.24.0,<1.0.0
11
11
  Requires-Dist: rich>=13.0.0
12
12
  Requires-Dist: questionary>=2.0.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "llamactl"
3
- version = "0.3.9"
3
+ version = "0.3.11"
4
4
  description = "A command-line interface for managing LlamaDeploy projects and deployments"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -9,8 +9,8 @@ authors = [
9
9
  ]
10
10
  requires-python = ">=3.11, <4"
11
11
  dependencies = [
12
- "llama-deploy-core[client]>=0.3.9,<0.4.0",
13
- "llama-deploy-appserver>=0.3.9,<0.4.0",
12
+ "llama-deploy-core[client]>=0.3.11,<0.4.0",
13
+ "llama-deploy-appserver>=0.3.11,<0.4.0",
14
14
  "httpx>=0.24.0,<1.0.0",
15
15
  "rich>=13.0.0",
16
16
  "questionary>=2.0.0",
@@ -55,8 +55,6 @@ def init(
55
55
 
56
56
  def _create(template: str | None, dir: Path | None, force: bool) -> None:
57
57
  # defer loading to improve cli startup time
58
- from vibe_llama.scaffold import create_scaffold
59
- from vibe_llama.scaffold.scaffold import ProjectName
60
58
  from vibe_llama.sdk import VibeLlamaStarter
61
59
 
62
60
  @dataclass
@@ -64,13 +62,9 @@ def _create(template: str | None, dir: Path | None, force: bool) -> None:
64
62
  id: str
65
63
  name: str
66
64
  description: str
67
- source: VibeLlamaTemplate | GithubTemplateRepo
65
+ source: GithubTemplateRepo
68
66
  llama_cloud: bool
69
67
 
70
- @dataclass
71
- class VibeLlamaTemplate:
72
- name: ProjectName
73
-
74
68
  @dataclass
75
69
  class GithubTemplateRepo:
76
70
  url: str
@@ -85,6 +79,15 @@ def _create(template: str | None, dir: Path | None, force: bool) -> None:
85
79
  ),
86
80
  llama_cloud=False,
87
81
  ),
82
+ TemplateOption(
83
+ id="showcase",
84
+ name="Showcase",
85
+ description="A collection of workflow and UI patterns to build LlamaDeploy apps",
86
+ source=GithubTemplateRepo(
87
+ url="https://github.com/run-llama/template-workflow-showcase"
88
+ ),
89
+ llama_cloud=False,
90
+ ),
88
91
  TemplateOption(
89
92
  id="document-qa",
90
93
  name="Document Question & Answer",
@@ -110,42 +113,54 @@ def _create(template: str | None, dir: Path | None, force: bool) -> None:
110
113
  id="basic",
111
114
  name="Basic Workflow",
112
115
  description="A base example that showcases usage patterns for workflows",
113
- source=VibeLlamaTemplate(name="basic"),
116
+ source=GithubTemplateRepo(
117
+ url="https://github.com/run-llama/template-workflow-basic"
118
+ ),
114
119
  llama_cloud=False,
115
120
  ),
116
121
  TemplateOption(
117
122
  id="document_parsing",
118
123
  name="Document Parser",
119
124
  description="A workflow that, using LlamaParse, parses unstructured documents and returns their raw text content",
120
- source=VibeLlamaTemplate(name="document_parsing"),
125
+ source=GithubTemplateRepo(
126
+ url="https://github.com/run-llama/template-workflow-document-parsing"
127
+ ),
121
128
  llama_cloud=True,
122
129
  ),
123
130
  TemplateOption(
124
131
  id="human_in_the_loop",
125
132
  name="Human in the Loop",
126
133
  description="A workflow showcasing how to use human in the loop with LlamaIndex workflows",
127
- source=VibeLlamaTemplate(name="human_in_the_loop"),
134
+ source=GithubTemplateRepo(
135
+ url="https://github.com/run-llama/template-workflow-human-in-the-loop"
136
+ ),
128
137
  llama_cloud=False,
129
138
  ),
130
139
  TemplateOption(
131
140
  id="invoice_extraction",
132
141
  name="Invoice Extraction",
133
142
  description="A workflow that, given an invoice, extracts several key details using LlamaExtract",
134
- source=VibeLlamaTemplate(name="invoice_extraction"),
143
+ source=GithubTemplateRepo(
144
+ url="https://github.com/run-llama/template-workflow-invoice-extraction"
145
+ ),
135
146
  llama_cloud=True,
136
147
  ),
137
148
  TemplateOption(
138
149
  id="rag",
139
150
  name="RAG",
140
151
  description="A workflow that embeds, indexes and queries your documents on the fly, providing you with a simple RAG pipeline",
141
- source=VibeLlamaTemplate(name="rag"),
152
+ source=GithubTemplateRepo(
153
+ url="https://github.com/run-llama/template-workflow-rag"
154
+ ),
142
155
  llama_cloud=False,
143
156
  ),
144
157
  TemplateOption(
145
158
  id="web_scraping",
146
159
  name="Web Scraping",
147
160
  description="A workflow that, given several urls, scrapes and summarizes their content using Google's Gemini API",
148
- source=VibeLlamaTemplate(name="web_scraping"),
161
+ source=GithubTemplateRepo(
162
+ url="https://github.com/run-llama/template-workflow-web-scraping"
163
+ ),
149
164
  llama_cloud=False,
150
165
  ),
151
166
  ]
@@ -202,16 +217,14 @@ def _create(template: str | None, dir: Path | None, force: bool) -> None:
202
217
  else:
203
218
  shutil.rmtree(dir, ignore_errors=True)
204
219
 
205
- if isinstance(resolved_template.source, GithubTemplateRepo):
206
- copier.run_copy(
207
- resolved_template.source.url,
208
- dir,
209
- quiet=True,
210
- )
211
- else:
212
- asyncio.run(create_scaffold(resolved_template.source.name, str(dir)))
220
+ copier.run_copy(
221
+ resolved_template.source.url,
222
+ dir,
223
+ quiet=True,
224
+ )
213
225
  # Initialize git repository if git is available
214
226
  has_git = False
227
+ git_initialized = False
215
228
  try:
216
229
  subprocess.run(["git", "--version"], check=True, capture_output=True)
217
230
  has_git = True
@@ -249,6 +262,7 @@ def _create(template: str | None, dir: Path | None, force: bool) -> None:
249
262
  check=True,
250
263
  capture_output=True,
251
264
  )
265
+ git_initialized = True
252
266
  except (subprocess.CalledProcessError, FileNotFoundError) as e:
253
267
  # Extract a short error message if present
254
268
  err_msg = ""
@@ -292,7 +306,8 @@ def _create(template: str | None, dir: Path | None, force: bool) -> None:
292
306
  rprint(" [orange3]uvx[/] llamactl serve")
293
307
  rprint("")
294
308
  rprint("[bold]To deploy:[/]")
295
- if has_git:
309
+ # Only show manual git init steps if repository failed to initialize earlier
310
+ if not git_initialized:
296
311
  rprint(" [orange3]git[/] init")
297
312
  rprint(" [orange3]git[/] add .")
298
313
  rprint(" [orange3]git[/] commit -m 'Initial commit'")
@@ -303,7 +318,7 @@ def _create(template: str | None, dir: Path | None, force: bool) -> None:
303
318
  rprint(" [orange3]git[/] push -u origin main")
304
319
  rprint("")
305
320
  # rprint(" [orange3]uvx[/] llamactl login")
306
- rprint(" [orange3]uvx[/] llamactl deploy")
321
+ rprint(" [orange3]uvx[/] llamactl deploy create")
307
322
  rprint("")
308
323
 
309
324
 
@@ -1,3 +1,4 @@
1
+ import asyncio
1
2
  import logging
2
3
  import os
3
4
  from pathlib import Path
@@ -5,15 +6,19 @@ from typing import Literal
5
6
 
6
7
  import click
7
8
  import questionary
9
+ from click.exceptions import Abort, Exit
8
10
  from llama_deploy.cli.commands.auth import validate_authenticated_profile
9
11
  from llama_deploy.cli.config.env_service import service
10
12
  from llama_deploy.cli.config.schema import Auth
11
13
  from llama_deploy.cli.options import interactive_option
12
14
  from llama_deploy.cli.styles import WARNING
15
+ from llama_deploy.cli.utils.redact import redact_api_key
16
+ from llama_deploy.core.client.manage_client import ControlPlaneClient
13
17
  from llama_deploy.core.config import DEFAULT_DEPLOYMENT_FILE_PATH
14
18
  from llama_deploy.core.deployment_config import (
15
19
  read_deployment_config_from_git_root_or_cwd,
16
20
  )
21
+ from llama_deploy.core.schema.projects import ProjectSummary
17
22
  from rich import print as rprint
18
23
 
19
24
  from ..app import app
@@ -87,6 +92,16 @@ def serve(
87
92
  rprint(f"[red]Deployment file '{deployment_file}' not found[/red]")
88
93
  raise click.Abort()
89
94
 
95
+ # Early check: appserver requires a pyproject.toml in the config directory
96
+ config_dir = deployment_file if deployment_file.is_dir() else deployment_file.parent
97
+ if not (config_dir / "pyproject.toml").exists():
98
+ rprint(
99
+ "[red]No pyproject.toml found at[/red] "
100
+ f"[bold]{config_dir}[/bold].\n"
101
+ "Add a pyproject.toml to your project and re-run 'llamactl serve'."
102
+ )
103
+ raise click.Abort()
104
+
90
105
  try:
91
106
  # Pre-check: if the template requires llama cloud access, ensure credentials
92
107
  _maybe_inject_llama_cloud_credentials(
@@ -108,6 +123,7 @@ def serve(
108
123
  build=preview,
109
124
  )
110
125
  deployment_config = get_deployment_config()
126
+ _print_connection_summary()
111
127
  start_server_in_target_venv(
112
128
  cwd=Path.cwd(),
113
129
  deployment_file=deployment_file,
@@ -127,6 +143,9 @@ def serve(
127
143
  else None,
128
144
  )
129
145
 
146
+ except (Exit, Abort):
147
+ raise
148
+
130
149
  except KeyboardInterrupt:
131
150
  logger.debug("Shutting down...")
132
151
 
@@ -207,6 +226,32 @@ def _maybe_inject_llama_cloud_credentials(
207
226
 
208
227
  existing = os.environ.get("LLAMA_CLOUD_API_KEY") or vars.get("LLAMA_CLOUD_API_KEY")
209
228
  if existing:
229
+ # If interactive, allow choosing between env var and configured profile
230
+ if interactive:
231
+ choice = questionary.select(
232
+ "LLAMA_CLOUD_API_KEY detected in environment. Which credentials do you want to use?",
233
+ choices=[
234
+ questionary.Choice(
235
+ title=f"Use environment variable - {redact_api_key(existing)}",
236
+ value="env",
237
+ ),
238
+ questionary.Choice(title="Use configured profile", value="profile"),
239
+ ],
240
+ ).ask()
241
+ if choice is None:
242
+ raise Exit(0)
243
+ if choice == "profile":
244
+ # Ensure we have an authenticated profile and inject from it
245
+ authed = validate_authenticated_profile(True)
246
+ _set_env_vars_from_profile(authed)
247
+ return
248
+ # Default to env var path when cancelled or explicitly chosen
249
+ _set_env_vars_from_env({**os.environ, **vars})
250
+ # If no project id provided, try to detect and select one using the env API key
251
+ if not os.environ.get("LLAMA_DEPLOY_PROJECT_ID"):
252
+ _maybe_select_project_for_env_key()
253
+ return
254
+ # Non-interactive: trust current environment variables
210
255
  _set_env_vars_from_env({**os.environ, **vars})
211
256
  return
212
257
 
@@ -243,3 +288,56 @@ def _maybe_inject_llama_cloud_credentials(
243
288
  rprint(
244
289
  f"[{WARNING}]Warning: LLAMA_CLOUD_API_KEY is not set and no logged-in profile was found. The app may not work.[/]"
245
290
  )
291
+
292
+
293
+ def _maybe_select_project_for_env_key() -> None:
294
+ """When using an env API key, ensure LLAMA_DEPLOY_PROJECT_ID is set.
295
+
296
+ If more than one project exists, prompt the user to select one.
297
+ """
298
+ api_key = os.environ.get("LLAMA_CLOUD_API_KEY")
299
+ base_url = os.environ.get("LLAMA_CLOUD_BASE_URL", "https://api.cloud.llamaindex.ai")
300
+ if not api_key:
301
+ return
302
+ try:
303
+
304
+ async def _run() -> list[ProjectSummary]:
305
+ async with ControlPlaneClient.ctx(base_url, api_key, None) as client:
306
+ return await client.list_projects()
307
+
308
+ projects = asyncio.run(_run())
309
+ if not projects:
310
+ return
311
+ if len(projects) == 1:
312
+ os.environ["LLAMA_DEPLOY_PROJECT_ID"] = projects[0].project_id
313
+ return
314
+ # Multiple: prompt selection
315
+ choice = questionary.select(
316
+ "Select a project",
317
+ choices=[
318
+ questionary.Choice(
319
+ title=f"{p.project_name} ({p.deployment_count} deployments)",
320
+ value=p.project_id,
321
+ )
322
+ for p in projects
323
+ ],
324
+ ).ask()
325
+ if choice:
326
+ os.environ["LLAMA_DEPLOY_PROJECT_ID"] = choice
327
+ except Exception:
328
+ # Best-effort; if we fail to list, do nothing
329
+ pass
330
+
331
+
332
+ def _print_connection_summary() -> None:
333
+ base_url = os.environ.get("LLAMA_CLOUD_BASE_URL")
334
+ project_id = os.environ.get("LLAMA_DEPLOY_PROJECT_ID")
335
+ api_key = os.environ.get("LLAMA_CLOUD_API_KEY")
336
+ if not base_url and not project_id and not api_key:
337
+ return
338
+ redacted = redact_api_key(api_key)
339
+ env_text = base_url or "-"
340
+ proj_text = project_id or "-"
341
+ rprint(
342
+ f"Connecting to environment: [bold]{env_text}[/], project: [bold]{proj_text}[/], api key: [bold]{redacted}[/]"
343
+ )
@@ -3,6 +3,7 @@ import asyncio
3
3
  from llama_deploy.cli.auth.client import PlatformAuthClient, RefreshMiddleware
4
4
  from llama_deploy.cli.config._config import Auth, ConfigManager, Environment
5
5
  from llama_deploy.cli.config.schema import DeviceOIDC
6
+ from llama_deploy.cli.utils.redact import redact_api_key
6
7
  from llama_deploy.core.client.manage_client import ControlPlaneClient, httpx
7
8
  from llama_deploy.core.schema import VersionResponse
8
9
  from llama_deploy.core.schema.projects import ProjectSummary
@@ -123,8 +124,4 @@ class AuthService:
123
124
 
124
125
  def _auto_profile_name_from_token(api_key: str) -> str:
125
126
  token = api_key or "token"
126
- cleaned = token.replace(" ", "")
127
- first = cleaned[:6]
128
- last = cleaned[-4:] if len(cleaned) > 10 else cleaned[-2:]
129
- base = f"{first}****{last}"
130
- return base
127
+ return redact_api_key(token)
@@ -5,6 +5,7 @@ import logging
5
5
  import re
6
6
  from dataclasses import dataclass, field
7
7
  from pathlib import Path
8
+ from textwrap import dedent
8
9
  from urllib.parse import urlsplit
9
10
 
10
11
  from llama_deploy.cli.client import get_project_client as get_client
@@ -23,6 +24,7 @@ from llama_deploy.cli.textual.git_validation import (
23
24
  ValidationResultMessage,
24
25
  )
25
26
  from llama_deploy.cli.textual.secrets_form import SecretsWidget
27
+ from llama_deploy.cli.utils.version import get_installed_appserver_version
26
28
  from llama_deploy.core.deployment_config import (
27
29
  DEFAULT_DEPLOYMENT_NAME,
28
30
  read_deployment_config,
@@ -40,6 +42,7 @@ from llama_deploy.core.schema.deployments import (
40
42
  DeploymentResponse,
41
43
  DeploymentUpdate,
42
44
  )
45
+ from packaging.version import Version
43
46
  from textual import events
44
47
  from textual.app import App, ComposeResult
45
48
  from textual.containers import Container, HorizontalGroup, Widget
@@ -47,7 +50,7 @@ from textual.content import Content
47
50
  from textual.message import Message
48
51
  from textual.reactive import reactive
49
52
  from textual.validation import Length
50
- from textual.widgets import Button, Input, Label, Static
53
+ from textual.widgets import Button, Input, Label, Select, Static
51
54
 
52
55
 
53
56
  @dataclass
@@ -75,11 +78,20 @@ class DeploymentForm:
75
78
  warnings: list[str] = field(default_factory=list)
76
79
  # env info
77
80
  env_info_messages: str | None = None
81
+ # appserver version fields
82
+ installed_appserver_version: str | None = None
83
+ existing_llama_deploy_version: str | None = None
84
+ selected_appserver_version: str | None = None
78
85
 
79
86
  @classmethod
80
87
  def from_deployment(cls, deployment: DeploymentResponse) -> "DeploymentForm":
81
88
  secret_names = deployment.secret_names or []
82
89
 
90
+ installed = get_installed_appserver_version()
91
+ existing = deployment.llama_deploy_version
92
+ # If versions match (or existing is None), treat as non-editable like create
93
+ selected = existing or installed
94
+
83
95
  return DeploymentForm(
84
96
  name=deployment.name,
85
97
  id=deployment.id,
@@ -91,8 +103,15 @@ class DeploymentForm:
91
103
  secrets={},
92
104
  initial_secrets=set(secret_names),
93
105
  is_editing=True,
106
+ installed_appserver_version=installed,
107
+ existing_llama_deploy_version=existing,
108
+ selected_appserver_version=selected,
94
109
  )
95
110
 
111
+ @staticmethod
112
+ def appserver_version() -> str | None:
113
+ return get_installed_appserver_version()
114
+
96
115
  def to_update(self) -> DeploymentUpdate:
97
116
  """Convert form data to API format"""
98
117
 
@@ -100,6 +119,8 @@ class DeploymentForm:
100
119
  for secret in self.removed_secrets:
101
120
  secrets[secret] = None
102
121
 
122
+ appserver_version = self.selected_appserver_version
123
+
103
124
  data = DeploymentUpdate(
104
125
  repo_url=self.repo_url,
105
126
  git_ref=self.git_ref or "main",
@@ -110,12 +131,14 @@ class DeploymentForm:
110
131
  else self.personal_access_token
111
132
  ),
112
133
  secrets=secrets,
134
+ llama_deploy_version=appserver_version,
113
135
  )
114
136
 
115
137
  return data
116
138
 
117
139
  def to_create(self) -> DeploymentCreate:
118
140
  """Convert form data to API format"""
141
+ appserver_version = self.selected_appserver_version
119
142
 
120
143
  return DeploymentCreate(
121
144
  name=self.name,
@@ -124,6 +147,7 @@ class DeploymentForm:
124
147
  git_ref=self.git_ref or "main",
125
148
  personal_access_token=self.personal_access_token,
126
149
  secrets=self.secrets,
150
+ llama_deploy_version=appserver_version,
127
151
  )
128
152
 
129
153
 
@@ -149,12 +173,23 @@ class DeploymentFormWidget(Widget):
149
173
  def compose(self) -> ComposeResult:
150
174
  title = "Edit Deployment" if self.form_data.is_editing else "Create Deployment"
151
175
 
152
- yield Static(
153
- Content.from_markup(
154
- f"{title} [italic][@click=app.show_help()]More info[/][/italic]"
155
- ),
176
+ with HorizontalGroup(
156
177
  classes="primary-message",
157
- )
178
+ ):
179
+ yield Static(
180
+ Content.from_markup(
181
+ f"{title} [italic][@click=app.show_help()]More info[/][/]"
182
+ ),
183
+ classes="w-1fr",
184
+ )
185
+ yield Static(
186
+ Content.from_markup(
187
+ dedent("""
188
+ [italic]Tab or click to navigate.[/]
189
+ """).strip()
190
+ ),
191
+ classes="text-right w-1fr",
192
+ )
158
193
  yield Static(
159
194
  self.error_message,
160
195
  id="error-message",
@@ -200,6 +235,11 @@ class DeploymentFormWidget(Widget):
200
235
  compact=True,
201
236
  )
202
237
 
238
+ yield Static(classes="full-width")
239
+ yield Static(
240
+ Content.from_markup("[italic]Advanced[/]"),
241
+ classes="text-center full-width",
242
+ )
203
243
  yield Label("Config File:", classes="form-label", shrink=True)
204
244
  yield Input(
205
245
  value=self.form_data.deployment_file_path,
@@ -207,6 +247,7 @@ class DeploymentFormWidget(Widget):
207
247
  id="deployment_file_path",
208
248
  compact=True,
209
249
  )
250
+
210
251
  yield Label("Personal Access Token:", classes="form-label", shrink=True)
211
252
  if self.form_data.has_existing_pat:
212
253
  yield Button(
@@ -226,6 +267,45 @@ class DeploymentFormWidget(Widget):
226
267
  compact=True,
227
268
  )
228
269
 
270
+ # Appserver version display/selector
271
+ yield Label("Appserver Version:", classes="form-label", shrink=True)
272
+ versions_differ = (
273
+ self.form_data.is_editing
274
+ and self.form_data.installed_appserver_version
275
+ and self.form_data.existing_llama_deploy_version
276
+ and self.form_data.installed_appserver_version
277
+ != self.form_data.existing_llama_deploy_version
278
+ )
279
+ if versions_differ:
280
+ # Show dropdown selector for version choice
281
+ installed_version = self.form_data.installed_appserver_version
282
+ existing_version = self.form_data.existing_llama_deploy_version
283
+ current_selection = (
284
+ self.form_data.selected_appserver_version
285
+ or existing_version
286
+ or installed_version
287
+ )
288
+ is_upgrade = Version(installed_version) > Version(existing_version)
289
+ label = "Upgrade" if is_upgrade else "Downgrade"
290
+ yield Select(
291
+ [
292
+ (f"{label} to {installed_version}", installed_version),
293
+ (f"Keep {existing_version}", existing_version),
294
+ ],
295
+ value=current_selection,
296
+ id="appserver_version_select",
297
+ allow_blank=False,
298
+ compact=True,
299
+ )
300
+ else:
301
+ # Non-editable display of version
302
+ readonly_version = (
303
+ self.form_data.installed_appserver_version
304
+ or self.form_data.existing_llama_deploy_version
305
+ or "unknown"
306
+ )
307
+ yield Static(readonly_version, id="appserver_version_readonly")
308
+
229
309
  # Secrets section
230
310
  yield SecretsWidget(
231
311
  initial_secrets=self.form_data.secrets,
@@ -249,6 +329,13 @@ class DeploymentFormWidget(Widget):
249
329
  # Post message to parent app to handle cancel
250
330
  self.post_message(CancelFormMessage())
251
331
 
332
+ def on_select_changed(self, event: Select.Changed) -> None:
333
+ """Handle version selection changes"""
334
+ if event.select.id == "appserver_version_select" and event.value:
335
+ updated_form = dataclasses.replace(self.resolve_form_data())
336
+ updated_form.selected_appserver_version = str(event.value)
337
+ self.form_data = updated_form
338
+
252
339
  def _save(self) -> None:
253
340
  self.form_data = self.resolve_form_data()
254
341
  if self._validate_form():
@@ -318,6 +405,9 @@ class DeploymentFormWidget(Widget):
318
405
  removed_secrets=self.original_form_data.initial_secrets.difference(
319
406
  updated_prior_secrets
320
407
  ),
408
+ installed_appserver_version=self.form_data.installed_appserver_version,
409
+ existing_llama_deploy_version=self.form_data.existing_llama_deploy_version,
410
+ selected_appserver_version=self.form_data.selected_appserver_version,
321
411
  )
322
412
 
323
413
 
@@ -583,6 +673,8 @@ def _initialize_deployment_data() -> DeploymentForm:
583
673
  if len(secrets) > 0:
584
674
  env_info_message = "Secrets were automatically seeded from your .env file. Remove or change any that should not be set. They must be manually configured after creation."
585
675
 
676
+ installed = get_installed_appserver_version()
677
+
586
678
  form = DeploymentForm(
587
679
  name=name or "",
588
680
  repo_url=repo_url or "",
@@ -591,6 +683,8 @@ def _initialize_deployment_data() -> DeploymentForm:
591
683
  deployment_file_path=config_file_path or "",
592
684
  warnings=warnings,
593
685
  env_info_messages=env_info_message,
686
+ installed_appserver_version=installed,
687
+ selected_appserver_version=installed,
594
688
  )
595
689
  return form
596
690
 
@@ -39,6 +39,12 @@ class DeploymentHelpWidget(Widget):
39
39
  [b]Config File[/b]
40
40
  Path to a directory or file containing a `[slategrey reverse]pyproject.toml[/]` or `[slategrey reverse]llama_deploy.yaml[/]` containing the llama deploy configuration. Only necessary if you have the configuration not at the root of the repo, or you have an unconventional configuration file.
41
41
 
42
+ [b]Personal Access Token[/b]
43
+ A personal access token to access the git repository. Can be used instead of the github integration.
44
+
45
+ [b]Appserver Version[/b]
46
+ The version of the appserver to deploy. Affects features and functionality. By default this is set to the current llamactl version, and then retained until manually upgraded.
47
+
42
48
  [b]Secrets[/b]
43
49
  Secrets to add as environment variables to the deployment. e.g. to access a database or an API. Supports adding in `[slategrey reverse].env[/]` file format.
44
50
 
@@ -60,6 +60,10 @@ Input.disabled {
60
60
  /* UTILITIES, MESSAGES & NOTIFICATIONS */
61
61
  /* =============================================== */
62
62
 
63
+ .mb-0 {
64
+ margin-bottom: 0 !important;
65
+ }
66
+
63
67
  .mb-1 {
64
68
  margin-bottom: 1;
65
69
  }
@@ -68,6 +72,10 @@ Input.disabled {
68
72
  margin-bottom: 2;
69
73
  }
70
74
 
75
+ .mt-0 {
76
+ margin-top: 0;
77
+ }
78
+
71
79
  .mt-1 {
72
80
  margin-top: 1;
73
81
  }
@@ -76,6 +84,10 @@ Input.disabled {
76
84
  margin-top: 2;
77
85
  }
78
86
 
87
+ .m-0 {
88
+ margin: 0;
89
+ }
90
+
79
91
  .m-1 {
80
92
  margin: 1;
81
93
  }
@@ -102,6 +114,7 @@ Input.disabled {
102
114
  padding: 0 0 0 1
103
115
  }
104
116
 
117
+
105
118
  .error-message {
106
119
  color: $text-error;
107
120
  background: $error-muted;
@@ -201,3 +214,102 @@ Button.secondary {
201
214
  align: left middle;
202
215
  }
203
216
 
217
+
218
+ .top-left {
219
+ align: left top;
220
+ }
221
+ .top-center {
222
+ align: left middle;
223
+ }
224
+ .top-right {
225
+ align: right top;
226
+ }
227
+ .middle-left {
228
+ align: left middle;
229
+ }
230
+ .middle-center {
231
+ align: center middle;
232
+ }
233
+ .middle-right {
234
+ align: right middle;
235
+ }
236
+ .bottom-left {
237
+ align: left bottom;
238
+ }
239
+ .bottom-center {
240
+ align: right bottom;
241
+ }
242
+ .bottom-right {
243
+ align: right bottom;
244
+ }
245
+
246
+
247
+ .text-right {
248
+ text-align: right;
249
+ }
250
+ .text-left {
251
+ text-align: left;
252
+ }
253
+ .text-center {
254
+ text-align: center;
255
+ }
256
+ .text-justify {
257
+ text-align: justify;
258
+ }
259
+
260
+ .background-red {
261
+ background: red;
262
+ }
263
+
264
+
265
+ .w-1 {
266
+ width: 1;
267
+ }
268
+
269
+ .w-2 {
270
+ width: 2;
271
+ }
272
+
273
+ .w-3 {
274
+ width: 3;
275
+ }
276
+
277
+ .w-1fr {
278
+ width: 1fr;
279
+ }
280
+
281
+ .w-100 {
282
+ width: 100%;
283
+ }
284
+
285
+ .w-2fr {
286
+ width: 2fr;
287
+ }
288
+
289
+ .h-1 {
290
+ height: 1;
291
+ }
292
+
293
+ .h-2 {
294
+ height: 2;
295
+ }
296
+
297
+ .h-3 {
298
+ height: 3;
299
+ }
300
+
301
+ .h-1fr {
302
+ height: 1fr;
303
+ }
304
+
305
+ .h-100 {
306
+ height: 100%;
307
+ }
308
+
309
+ .h-2fr {
310
+ height: 2fr;
311
+ }
312
+
313
+ .h-100 {
314
+ height: 100%;
315
+ }
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ def redact_api_key(
5
+ token: str | None,
6
+ visible_prefix: int = 6,
7
+ visible_suffix_long: int = 4,
8
+ visible_suffix_short: int = 2,
9
+ long_threshold: int = 10,
10
+ mask: str = "****",
11
+ ) -> str:
12
+ """Redact an API key for display.
13
+
14
+ Shows a prefix and suffix with a mask in the middle. If token is short,
15
+ reduces the suffix length to keep at least two trailing characters visible.
16
+
17
+ This mirrors the masking behavior used for profile names.
18
+ """
19
+ if not token:
20
+ return "-"
21
+ cleaned = token.replace(" ", "")
22
+ if len(cleaned) <= 0:
23
+ return "-"
24
+ first = cleaned[:visible_prefix]
25
+ last_len = (
26
+ visible_suffix_long if len(cleaned) > long_threshold else visible_suffix_short
27
+ )
28
+ last = cleaned[-last_len:] if last_len > 0 else ""
29
+ return f"{first}{mask}{last}"
@@ -0,0 +1,11 @@
1
+ """Version utilities shared across CLI components."""
2
+
3
+ from importlib import metadata as importlib_metadata
4
+
5
+
6
+ def get_installed_appserver_version() -> str | None:
7
+ """Return the installed version of `llama-deploy-appserver`, if available."""
8
+ try:
9
+ return importlib_metadata.version("llama-deploy-appserver")
10
+ except Exception:
11
+ return None
File without changes