llamactl 0.3.13__tar.gz → 0.3.15__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.13 → llamactl-0.3.15}/PKG-INFO +5 -5
  2. {llamactl-0.3.13 → llamactl-0.3.15}/pyproject.toml +5 -5
  3. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/commands/deployment.py +127 -4
  4. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/commands/init.py +24 -47
  5. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/textual/deployment_form.py +26 -5
  6. {llamactl-0.3.13 → llamactl-0.3.15}/README.md +0 -0
  7. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/__init__.py +0 -0
  8. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/app.py +0 -0
  9. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/auth/client.py +0 -0
  10. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/client.py +0 -0
  11. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/commands/aliased_group.py +0 -0
  12. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/commands/auth.py +0 -0
  13. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/commands/env.py +0 -0
  14. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/commands/serve.py +0 -0
  15. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/config/_config.py +0 -0
  16. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/config/_migrations.py +0 -0
  17. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/config/auth_service.py +0 -0
  18. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/config/env_service.py +0 -0
  19. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/config/migrations/0001_init.sql +0 -0
  20. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/config/migrations/0002_add_auth_fields.sql +0 -0
  21. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/config/migrations/__init__.py +0 -0
  22. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/config/schema.py +0 -0
  23. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/debug.py +0 -0
  24. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/env.py +0 -0
  25. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/interactive_prompts/session_utils.py +0 -0
  26. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/interactive_prompts/utils.py +0 -0
  27. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/options.py +0 -0
  28. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/py.typed +0 -0
  29. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/styles.py +0 -0
  30. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/textual/deployment_help.py +0 -0
  31. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/textual/deployment_monitor.py +0 -0
  32. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/textual/git_validation.py +0 -0
  33. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/textual/github_callback_server.py +0 -0
  34. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/textual/llama_loader.py +0 -0
  35. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/textual/secrets_form.py +0 -0
  36. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/textual/styles.tcss +0 -0
  37. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/utils/env_inject.py +0 -0
  38. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/utils/redact.py +0 -0
  39. {llamactl-0.3.13 → llamactl-0.3.15}/src/llama_deploy/cli/utils/version.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: llamactl
3
- Version: 0.3.13
3
+ Version: 0.3.15
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.13,<0.4.0
9
- Requires-Dist: llama-deploy-appserver>=0.3.13,<0.4.0
10
- Requires-Dist: httpx>=0.24.0,<1.0.0
8
+ Requires-Dist: llama-deploy-core[client]>=0.3.15,<0.4.0
9
+ Requires-Dist: llama-deploy-appserver>=0.3.15,<0.4.0
10
+ Requires-Dist: vibe-llama-core>=0.1.0
11
11
  Requires-Dist: rich>=13.0.0
12
12
  Requires-Dist: questionary>=2.0.0
13
13
  Requires-Dist: click>=8.2.1
@@ -15,7 +15,7 @@ Requires-Dist: python-dotenv>=1.0.0
15
15
  Requires-Dist: tenacity>=9.1.2
16
16
  Requires-Dist: textual>=6.0.0
17
17
  Requires-Dist: aiohttp>=3.12.14
18
- Requires-Dist: copier>=9.9.0
18
+ Requires-Dist: copier>=9.10.2
19
19
  Requires-Dist: pyjwt[crypto]>=2.10.1
20
20
  Requires-Python: >=3.11, <4
21
21
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "llamactl"
3
- version = "0.3.13"
3
+ version = "0.3.15"
4
4
  description = "A command-line interface for managing LlamaDeploy projects and deployments"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -9,9 +9,9 @@ authors = [
9
9
  ]
10
10
  requires-python = ">=3.11, <4"
11
11
  dependencies = [
12
- "llama-deploy-core[client]>=0.3.13,<0.4.0",
13
- "llama-deploy-appserver>=0.3.13,<0.4.0",
14
- "httpx>=0.24.0,<1.0.0",
12
+ "llama-deploy-core[client]>=0.3.15,<0.4.0",
13
+ "llama-deploy-appserver>=0.3.15,<0.4.0",
14
+ "vibe-llama-core>=0.1.0",
15
15
  "rich>=13.0.0",
16
16
  "questionary>=2.0.0",
17
17
  "click>=8.2.1",
@@ -19,7 +19,7 @@ dependencies = [
19
19
  "tenacity>=9.1.2",
20
20
  "textual>=6.0.0",
21
21
  "aiohttp>=3.12.14",
22
- "copier>=9.9.0",
22
+ "copier>=9.10.2",
23
23
  "pyjwt[crypto]>=2.10.1",
24
24
  ]
25
25
 
@@ -12,13 +12,17 @@ import click
12
12
  import questionary
13
13
  from llama_deploy.cli.commands.auth import validate_authenticated_profile
14
14
  from llama_deploy.cli.styles import HEADER_COLOR, MUTED_COL, PRIMARY_COL, WARNING
15
- from llama_deploy.core.schema.deployments import DeploymentUpdate
15
+ from llama_deploy.core.schema.deployments import (
16
+ DeploymentHistoryResponse,
17
+ DeploymentResponse,
18
+ DeploymentUpdate,
19
+ )
16
20
  from rich import print as rprint
17
21
  from rich.table import Table
18
22
  from rich.text import Text
19
23
 
20
24
  from ..app import app, console
21
- from ..client import get_project_client
25
+ from ..client import get_project_client, project_client_context
22
26
  from ..interactive_prompts.session_utils import (
23
27
  is_interactive_session,
24
28
  )
@@ -221,8 +225,15 @@ def edit_deployment(deployment_id: str | None, interactive: bool) -> None:
221
225
  @deployments.command("update")
222
226
  @global_options
223
227
  @click.argument("deployment_id", required=False)
228
+ @click.option(
229
+ "--git-ref",
230
+ help="Reference branch or commit SHA for the deployment. If not provided, the current reference branch and latest commit on it will be used.",
231
+ default=None,
232
+ )
224
233
  @interactive_option
225
- def refresh_deployment(deployment_id: str | None, interactive: bool) -> None:
234
+ def refresh_deployment(
235
+ deployment_id: str | None, git_ref: str | None, interactive: bool
236
+ ) -> None:
226
237
  """Update the deployment, pulling the latest code from it's branch"""
227
238
  validate_authenticated_profile(interactive)
228
239
  try:
@@ -240,7 +251,9 @@ def refresh_deployment(deployment_id: str | None, interactive: bool) -> None:
240
251
 
241
252
  # Create an empty update to force git SHA refresh with spinner
242
253
  with console.status(f"Refreshing {deployment_name}..."):
243
- deployment_update = DeploymentUpdate()
254
+ deployment_update = DeploymentUpdate(
255
+ git_ref=git_ref,
256
+ )
244
257
  updated_deployment = asyncio.run(
245
258
  get_project_client().update_deployment(
246
259
  deployment_id,
@@ -263,6 +276,116 @@ def refresh_deployment(deployment_id: str | None, interactive: bool) -> None:
263
276
  raise click.Abort()
264
277
 
265
278
 
279
+ @deployments.command("history")
280
+ @global_options
281
+ @click.argument("deployment_id", required=False)
282
+ @interactive_option
283
+ def show_history(deployment_id: str | None, interactive: bool) -> None:
284
+ """Show release history for a deployment."""
285
+ validate_authenticated_profile(interactive)
286
+ try:
287
+ deployment_id = select_deployment(deployment_id, interactive=interactive)
288
+ if not deployment_id:
289
+ rprint(f"[{WARNING}]No deployment selected[/]")
290
+ return
291
+
292
+ async def _fetch_history() -> DeploymentHistoryResponse:
293
+ async with project_client_context() as client:
294
+ return await client.get_deployment_history(deployment_id)
295
+
296
+ history = asyncio.run(_fetch_history())
297
+ items = history.history
298
+ if not items:
299
+ rprint(f"No history recorded for {deployment_id}")
300
+ return
301
+
302
+ table = Table(show_edge=False, box=None, header_style=f"bold {HEADER_COLOR}")
303
+ table.add_column("Released At", style=MUTED_COL)
304
+ table.add_column("Git SHA", style=PRIMARY_COL)
305
+ # newest first
306
+ items_sorted = sorted(
307
+ items,
308
+ key=lambda it: it.released_at,
309
+ reverse=True,
310
+ )
311
+ for item in items_sorted:
312
+ ts = item.released_at.isoformat()
313
+ sha = item.git_sha
314
+ table.add_row(ts, sha)
315
+ console.print(table)
316
+ except Exception as e:
317
+ rprint(f"[red]Error: {e}[/red]")
318
+ raise click.Abort()
319
+
320
+
321
+ @deployments.command("rollback", hidden=True)
322
+ @global_options
323
+ @click.argument("deployment_id", required=False)
324
+ @click.option("--git-sha", required=False, help="Git SHA to roll back to")
325
+ @interactive_option
326
+ def rollback(deployment_id: str | None, git_sha: str | None, interactive: bool) -> None:
327
+ """Rollback a deployment to a previous git sha."""
328
+ validate_authenticated_profile(interactive)
329
+ try:
330
+ deployment_id = select_deployment(deployment_id, interactive=interactive)
331
+ if not deployment_id:
332
+ rprint(f"[{WARNING}]No deployment selected[/]")
333
+ return
334
+
335
+ if not git_sha:
336
+ # If not provided, prompt from history
337
+ async def _fetch_current_and_history() -> tuple[
338
+ DeploymentResponse, DeploymentHistoryResponse
339
+ ]:
340
+ async with project_client_context() as client:
341
+ current = await client.get_deployment(deployment_id)
342
+ hist = await client.get_deployment_history(deployment_id)
343
+ return current, hist
344
+
345
+ current_deployment, history = asyncio.run(_fetch_current_and_history())
346
+ current_sha = current_deployment.git_sha or ""
347
+
348
+ items = history.history or []
349
+ # Sort newest first
350
+ items_sorted = sorted(items, key=lambda it: it.released_at, reverse=True)
351
+ choices = []
352
+ for it in items_sorted:
353
+ short = it.git_sha[:7]
354
+ suffix = (
355
+ " [current]" if current_sha and it.git_sha == current_sha else ""
356
+ )
357
+ choices.append(
358
+ questionary.Choice(
359
+ title=f"{short}{suffix} ({it.released_at})", value=it.git_sha
360
+ )
361
+ )
362
+ if not choices:
363
+ rprint(f"[{WARNING}]No history available to rollback[/]")
364
+ return
365
+ git_sha = questionary.select("Select git sha:", choices=choices).ask()
366
+ if not git_sha:
367
+ rprint(f"[{WARNING}]Cancelled[/]")
368
+ return
369
+
370
+ if interactive and not confirm_action(
371
+ f"Rollback '{deployment_id}' to {git_sha[:7]}?"
372
+ ):
373
+ rprint(f"[{WARNING}]Cancelled[/]")
374
+ return
375
+
376
+ async def _do_rollback() -> DeploymentResponse:
377
+ async with project_client_context() as client:
378
+ return await client.rollback_deployment(deployment_id, git_sha)
379
+
380
+ updated = asyncio.run(_do_rollback())
381
+ rprint(
382
+ f"[green]Rollback initiated[/green]: {deployment_id} → {updated.git_sha[:7] if updated.git_sha else 'unknown'}"
383
+ )
384
+ except Exception as e:
385
+ rprint(f"[red]Error: {e}[/red]")
386
+ raise click.Abort()
387
+
388
+
266
389
  def select_deployment(
267
390
  deployment_id: str | None = None, interactive: bool = is_interactive_session()
268
391
  ) -> str | None:
@@ -9,7 +9,6 @@ from pathlib import Path
9
9
 
10
10
  import click
11
11
  import copier
12
- import httpx
13
12
  import questionary
14
13
  from click.exceptions import Exit
15
14
  from llama_deploy.cli.app import app
@@ -18,9 +17,9 @@ from llama_deploy.cli.options import (
18
17
  interactive_option,
19
18
  )
20
19
  from llama_deploy.cli.styles import HEADER_COLOR_HEX
21
- from llama_deploy.core.client.ssl_util import get_httpx_verify_param
22
20
  from rich import print as rprint
23
21
  from rich.text import Text
22
+ from vibe_llama_core.docs import get_agent_rules
24
23
 
25
24
 
26
25
  @app.command()
@@ -295,14 +294,16 @@ def _create(
295
294
  # Extract a short error message if present
296
295
  err_msg = ""
297
296
  if isinstance(e, subprocess.CalledProcessError):
298
- stderr = getattr(e, "stderr", b"")
299
- if isinstance(stderr, (bytes, bytearray)):
297
+ stderr_bytes = e.stderr or b""
298
+ if isinstance(stderr_bytes, (bytes, bytearray)):
300
299
  try:
301
- stderr = stderr.decode("utf-8", "ignore")
300
+ stderr_text = stderr_bytes.decode("utf-8", "ignore")
302
301
  except Exception:
303
- stderr = ""
304
- if isinstance(stderr, str) and stderr.strip():
305
- err_msg = stderr.strip().split("\n")[-1]
302
+ stderr_text = ""
303
+ else:
304
+ stderr_text = str(stderr_bytes)
305
+ if stderr_text.strip():
306
+ err_msg = stderr_text.strip().split("\n")[-1]
306
307
  elif isinstance(e, FileNotFoundError):
307
308
  err_msg = "git executable not found"
308
309
 
@@ -406,14 +407,6 @@ async def _download_and_write_agents_md(include_llama_cloud: bool) -> bool:
406
407
 
407
408
  Returns True if any documentation was fetched, False otherwise.
408
409
  """
409
- BASE_URL = "https://raw.githubusercontent.com/run-llama/vibe-llama/main"
410
-
411
- services: dict[str, str] = {
412
- "LlamaIndex": f"{BASE_URL}/documentation/llamaindex.md",
413
- "LlamaCloud Services": f"{BASE_URL}/documentation/llamacloud.md",
414
- "llama-index-workflows": f"{BASE_URL}/documentation/llama-index-workflows.md",
415
- "LlamaDeploy": f"{BASE_URL}/documentation/llamadeploy.md",
416
- }
417
410
 
418
411
  selected_services: list[str] = [
419
412
  "LlamaDeploy",
@@ -423,35 +416,19 @@ async def _download_and_write_agents_md(include_llama_cloud: bool) -> bool:
423
416
  if include_llama_cloud:
424
417
  selected_services.append("LlamaCloud Services")
425
418
 
426
- urls: list[str] = [(s, u) for s in selected_services if (u := services.get(s))]
427
-
428
- contents: list[str] = []
429
- timeout = httpx.Timeout(5.0)
430
- async with httpx.AsyncClient(
431
- timeout=timeout, verify=get_httpx_verify_param()
432
- ) as client:
433
-
434
- async def get_docs(service: str, url: str) -> str | None:
435
- try:
436
- resp = await client.get(url)
437
- resp.raise_for_status()
438
- text = resp.text.strip()
439
- if text:
440
- return text
441
- except Exception:
442
- # best-effort: skip failures
443
- rprint(
444
- f"[yellow]Failed to fetch documentation for {service}, skipping[/]"
445
- )
446
- return None
447
-
448
- results = await asyncio.gather(
449
- *[get_docs(service, url) for service, url in urls]
450
- )
451
- contents = [r for r in results if r is not None]
452
-
453
- if contents:
454
- agents_md = "\n\n---\n\n".join(contents) + "\n"
455
- Path("AGENTS.md").write_text(agents_md, encoding="utf-8")
419
+ downloads = 0
420
+
421
+ for service in selected_services:
422
+ try:
423
+ await get_agent_rules(
424
+ agent="OpenAI Codex CLI",
425
+ service=service,
426
+ overwrite_files=False,
427
+ verbose=False,
428
+ ) # type: ignore
429
+ except Exception:
430
+ rprint(f"[yellow]Failed to fetch documentation for {service}, skipping[/]")
431
+ else:
432
+ downloads += 1
456
433
 
457
- return bool(contents)
434
+ return downloads > 0
@@ -82,6 +82,8 @@ class DeploymentForm:
82
82
  installed_appserver_version: str | None = None
83
83
  existing_llama_deploy_version: str | None = None
84
84
  selected_appserver_version: str | None = None
85
+ # required secret names from config
86
+ required_secret_names: list[str] = field(default_factory=list)
85
87
 
86
88
  @classmethod
87
89
  def from_deployment(cls, deployment: DeploymentResponse) -> "DeploymentForm":
@@ -319,6 +321,12 @@ class DeploymentFormWidget(Widget):
319
321
 
320
322
  def on_button_pressed(self, event: Button.Pressed) -> None:
321
323
  if event.button.id == "save":
324
+ # Ensure latest input values are captured by blurring current focus first
325
+ try:
326
+ if self.screen.focused is not None:
327
+ self.screen.focused.blur()
328
+ except Exception:
329
+ pass
322
330
  self._save()
323
331
  elif event.button.id == "change_pat":
324
332
  updated_form = dataclasses.replace(self.resolve_form_data())
@@ -343,11 +351,11 @@ class DeploymentFormWidget(Widget):
343
351
  self.post_message(StartValidationMessage(self.form_data))
344
352
 
345
353
  def _validate_form(self) -> bool:
346
- """Validate required fields"""
354
+ """Validate required fields from the current UI state"""
347
355
  name_input = self.query_one("#name", Input)
348
356
  repo_url_input = self.query_one("#repo_url", Input)
349
357
 
350
- errors = []
358
+ errors: list[str] = []
351
359
 
352
360
  # Clear previous error state
353
361
  name_input.remove_class("error")
@@ -361,12 +369,19 @@ class DeploymentFormWidget(Widget):
361
369
  repo_url_input.add_class("error")
362
370
  errors.append("Repository URL is required")
363
371
 
372
+ missing_required: list[str] = []
373
+ for secret_name in sorted(self.form_data.required_secret_names):
374
+ value = (self.form_data.secrets.get(secret_name) or "").strip()
375
+ if value == "":
376
+ missing_required.append(secret_name)
377
+ if missing_required:
378
+ errors.append("Missing required secrets: " + ", ".join(missing_required))
379
+
364
380
  if errors:
365
381
  self._show_error("; ".join(errors))
366
382
  return False
367
- else:
368
- self._show_error("")
369
- return True
383
+ self._show_error("")
384
+ return True
370
385
 
371
386
  def _show_error(self, message: str) -> None:
372
387
  """Show an error message"""
@@ -408,6 +423,7 @@ class DeploymentFormWidget(Widget):
408
423
  installed_appserver_version=self.form_data.installed_appserver_version,
409
424
  existing_llama_deploy_version=self.form_data.existing_llama_deploy_version,
410
425
  selected_appserver_version=self.form_data.selected_appserver_version,
426
+ required_secret_names=self.form_data.required_secret_names,
411
427
  )
412
428
 
413
429
 
@@ -608,11 +624,15 @@ def _initialize_deployment_data() -> DeploymentForm:
608
624
  warnings: list[str] = []
609
625
  has_git = is_git_repo()
610
626
  has_no_workflows = False
627
+ required_secret_names: list[str] = []
611
628
  try:
612
629
  config = read_deployment_config(Path("."), Path("."))
613
630
  if config.name != DEFAULT_DEPLOYMENT_NAME:
614
631
  name = config.name
615
632
  has_no_workflows = config.has_no_workflows()
633
+ # Seed required secret names from config if present
634
+ required_secret_names = config.required_env_vars
635
+
616
636
  except Exception:
617
637
  warnings.append("Could not parse local deployment config. It may be invalid.")
618
638
  if not has_git and has_no_workflows:
@@ -685,6 +705,7 @@ def _initialize_deployment_data() -> DeploymentForm:
685
705
  env_info_messages=env_info_message,
686
706
  installed_appserver_version=installed,
687
707
  selected_appserver_version=installed,
708
+ required_secret_names=required_secret_names,
688
709
  )
689
710
  return form
690
711
 
File without changes