meshagent-cli 0.35.4__tar.gz → 0.35.6__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 (87) hide show
  1. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/PKG-INFO +15 -14
  2. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/api_keys.py +35 -6
  3. meshagent_cli-0.35.6/meshagent/cli/api_keys_test.py +79 -0
  4. meshagent_cli-0.35.6/meshagent/cli/async_typer.py +380 -0
  5. meshagent_cli-0.35.6/meshagent/cli/async_typer_test.py +128 -0
  6. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/chatbot.py +83 -16
  7. meshagent_cli-0.35.6/meshagent/cli/cli.py +293 -0
  8. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/cli_secrets.py +32 -6
  9. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/codex.py +19 -4
  10. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/containers.py +24 -58
  11. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/containers_test.py +65 -0
  12. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/multi.py +85 -44
  13. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/oci_archive.py +66 -16
  14. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/oci_archive_test.py +32 -5
  15. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/process_test.py +199 -1
  16. meshagent_cli-0.35.6/meshagent/cli/room.py +78 -0
  17. meshagent_cli-0.35.6/meshagent/cli/root_commands.py +122 -0
  18. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/sync.py +7 -2
  19. meshagent_cli-0.35.6/meshagent/cli/version.py +1 -0
  20. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent_cli.egg-info/PKG-INFO +15 -14
  21. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent_cli.egg-info/SOURCES.txt +3 -0
  22. meshagent_cli-0.35.6/meshagent_cli.egg-info/requires.txt +30 -0
  23. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/pyproject.toml +14 -13
  24. meshagent_cli-0.35.4/meshagent/cli/async_typer.py +0 -142
  25. meshagent_cli-0.35.4/meshagent/cli/cli.py +0 -281
  26. meshagent_cli-0.35.4/meshagent/cli/room.py +0 -48
  27. meshagent_cli-0.35.4/meshagent/cli/version.py +0 -1
  28. meshagent_cli-0.35.4/meshagent_cli.egg-info/requires.txt +0 -29
  29. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/README.md +0 -0
  30. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/__init__.py +0 -0
  31. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/agent.py +0 -0
  32. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/auth.py +0 -0
  33. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/auth_async.py +0 -0
  34. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/call.py +0 -0
  35. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/cli_mcp.py +0 -0
  36. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/cli_test.py +0 -0
  37. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/common_options.py +0 -0
  38. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/database.py +0 -0
  39. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/developer.py +0 -0
  40. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/developer_test.py +0 -0
  41. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/helper.py +0 -0
  42. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/helper_test.py +0 -0
  43. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/helpers.py +0 -0
  44. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/host.py +0 -0
  45. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/image.py +0 -0
  46. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/image_test.py +0 -0
  47. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/mailbot.py +0 -0
  48. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/mailboxes.py +0 -0
  49. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/meeting_transcriber.py +0 -0
  50. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/memory.py +0 -0
  51. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/memory_test.py +0 -0
  52. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/messaging.py +0 -0
  53. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/oauth2.py +0 -0
  54. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/participant_token.py +0 -0
  55. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/port.py +0 -0
  56. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/port_test.py +0 -0
  57. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/process.py +0 -0
  58. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/projects.py +0 -0
  59. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/queue.py +0 -0
  60. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/queue_test.py +0 -0
  61. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/room_services.py +0 -0
  62. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/rooms.py +0 -0
  63. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/routes.py +0 -0
  64. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/scheduled_tasks.py +0 -0
  65. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/scheduled_tasks_test.py +0 -0
  66. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/services.py +0 -0
  67. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/services_test.py +0 -0
  68. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/sessions.py +0 -0
  69. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/sessions_test.py +0 -0
  70. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/storage.py +0 -0
  71. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/storage_test.py +0 -0
  72. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/sync_test.py +0 -0
  73. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/task_runner.py +0 -0
  74. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/test.py +0 -0
  75. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/tui/__init__.py +0 -0
  76. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/tui/setup.py +0 -0
  77. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/tui/setup_splash_frames.py +0 -0
  78. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/tui/setup_test.py +0 -0
  79. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/voicebot.py +0 -0
  80. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/webhook.py +0 -0
  81. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/webserver.py +0 -0
  82. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/webserver_test.py +0 -0
  83. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/worker.py +0 -0
  84. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent_cli.egg-info/dependency_links.txt +0 -0
  85. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent_cli.egg-info/entry_points.txt +0 -0
  86. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent_cli.egg-info/top_level.txt +0 -0
  87. {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshagent-cli
3
- Version: 0.35.4
3
+ Version: 0.35.6
4
4
  Summary: CLI for Meshagent
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Documentation, https://docs.meshagent.com
@@ -14,26 +14,27 @@ Requires-Dist: opentelemetry-distro~=0.59b0
14
14
  Requires-Dist: opentelemetry-exporter-otlp-proto-http~=1.33
15
15
  Requires-Dist: art~=6.5
16
16
  Requires-Dist: pydantic-yaml~=1.5
17
- Requires-Dist: pathspec~=0.12.1
17
+ Requires-Dist: pathspec<2,>=1.0.3
18
+ Requires-Dist: zstandard~=0.25.0
18
19
  Requires-Dist: rich~=14.3.0
19
20
  Requires-Dist: textual<2.0,>=0.50
20
21
  Requires-Dist: prompt-toolkit~=3.0.52
21
22
  Provides-Extra: all
22
- Requires-Dist: meshagent-agents[all]~=0.35.4; extra == "all"
23
- Requires-Dist: meshagent-api[all]~=0.35.4; extra == "all"
24
- Requires-Dist: meshagent-computers~=0.35.4; extra == "all"
25
- Requires-Dist: meshagent-openai~=0.35.4; extra == "all"
26
- Requires-Dist: meshagent-anthropic~=0.35.4; extra == "all"
27
- Requires-Dist: meshagent-codex~=0.35.4; extra == "all"
28
- Requires-Dist: meshagent-mcp~=0.35.4; extra == "all"
29
- Requires-Dist: meshagent-tools~=0.35.4; extra == "all"
23
+ Requires-Dist: meshagent-agents[all]~=0.35.6; extra == "all"
24
+ Requires-Dist: meshagent-api[all]~=0.35.6; extra == "all"
25
+ Requires-Dist: meshagent-computers~=0.35.6; extra == "all"
26
+ Requires-Dist: meshagent-openai~=0.35.6; extra == "all"
27
+ Requires-Dist: meshagent-anthropic~=0.35.6; extra == "all"
28
+ Requires-Dist: meshagent-codex~=0.35.6; extra == "all"
29
+ Requires-Dist: meshagent-mcp~=0.35.6; extra == "all"
30
+ Requires-Dist: meshagent-tools~=0.35.6; extra == "all"
30
31
  Requires-Dist: supabase-auth~=2.28.0; extra == "all"
31
32
  Requires-Dist: prompt-toolkit~=3.0.52; extra == "all"
32
33
  Provides-Extra: mcp-service
33
- Requires-Dist: meshagent-agents[all]~=0.35.4; extra == "mcp-service"
34
- Requires-Dist: meshagent-api~=0.35.4; extra == "mcp-service"
35
- Requires-Dist: meshagent-mcp~=0.35.4; extra == "mcp-service"
36
- Requires-Dist: meshagent-tools~=0.35.4; extra == "mcp-service"
34
+ Requires-Dist: meshagent-agents[all]~=0.35.6; extra == "mcp-service"
35
+ Requires-Dist: meshagent-api~=0.35.6; extra == "mcp-service"
36
+ Requires-Dist: meshagent-mcp~=0.35.6; extra == "mcp-service"
37
+ Requires-Dist: meshagent-tools~=0.35.6; extra == "mcp-service"
37
38
  Requires-Dist: supabase-auth~=2.28.0; extra == "mcp-service"
38
39
 
39
40
  # [Meshagent](https://www.meshagent.com)
@@ -1,21 +1,36 @@
1
1
  import json
2
+ import shlex
3
+ from typing import Annotated
4
+
5
+ import typer
2
6
  from rich import print
3
7
 
4
- from meshagent.cli.common_options import ProjectIdOption
5
8
  from meshagent.cli import async_typer
9
+ from meshagent.cli.common_options import OutputFormatOption, ProjectIdOption
6
10
  from meshagent.cli.helper import (
11
+ get_active_api_key,
7
12
  get_client,
8
13
  print_json_table,
9
14
  resolve_project_id,
10
15
  set_active_api_key,
11
16
  )
12
- from meshagent.cli.common_options import OutputFormatOption
13
- from typing import Annotated
14
- import typer
15
17
 
16
18
  app = async_typer.AsyncTyper(help="Manage or activate api-keys for your project")
17
19
 
18
20
 
21
+ async def _require_active_api_key(*, project_id: str | None) -> str:
22
+ resolved_project_id = await resolve_project_id(project_id=project_id)
23
+ key = await get_active_api_key(project_id=resolved_project_id)
24
+ if key is None:
25
+ print(
26
+ f"[red]No activated API key found for project {resolved_project_id}. "
27
+ "Use meshagent api-key activate or meshagent api-key create "
28
+ "--activate to store one locally.[/red]"
29
+ )
30
+ raise typer.Exit(code=1)
31
+ return key
32
+
33
+
19
34
  @app.async_command("list", help="List API keys for a project.")
20
35
  async def list(
21
36
  *,
@@ -82,6 +97,21 @@ async def create(
82
97
  )
83
98
 
84
99
 
100
+ @app.async_command("show", help="Show the activated API key for a project.")
101
+ async def show(*, project_id: ProjectIdOption):
102
+ key = await _require_active_api_key(project_id=project_id)
103
+ typer.echo(key)
104
+
105
+
106
+ @app.async_command(
107
+ "env",
108
+ help="Print the activated API key as a shell export snippet.",
109
+ )
110
+ async def env(*, project_id: ProjectIdOption):
111
+ key = await _require_active_api_key(project_id=project_id)
112
+ typer.echo(f"export MESHAGENT_API_KEY={shlex.quote(key)}")
113
+
114
+
85
115
  @app.async_command(
86
116
  "activate",
87
117
  help="Set the default API key for a project in local CLI settings.",
@@ -92,8 +122,7 @@ async def activate(
92
122
  key: str,
93
123
  ):
94
124
  project_id = await resolve_project_id(project_id=project_id)
95
- if activate:
96
- await set_active_api_key(project_id=project_id, key=key)
125
+ await set_active_api_key(project_id=project_id, key=key)
97
126
 
98
127
 
99
128
  @app.async_command("delete", help="Delete an API key from a project.")
@@ -0,0 +1,79 @@
1
+ import pytest
2
+ import typer
3
+
4
+ from meshagent.cli import api_keys
5
+
6
+
7
+ @pytest.mark.asyncio
8
+ async def test_show_prints_activated_api_key(
9
+ monkeypatch: pytest.MonkeyPatch,
10
+ ) -> None:
11
+ printed: list[str] = []
12
+
13
+ async def fake_resolve_project_id(*, project_id: str | None) -> str:
14
+ assert project_id == "project-1"
15
+ return "resolved-project"
16
+
17
+ async def fake_get_active_api_key(*, project_id: str) -> str | None:
18
+ assert project_id == "resolved-project"
19
+ return "ma-key-1"
20
+
21
+ monkeypatch.setattr(api_keys, "resolve_project_id", fake_resolve_project_id)
22
+ monkeypatch.setattr(api_keys, "get_active_api_key", fake_get_active_api_key)
23
+ monkeypatch.setattr(api_keys.typer, "echo", printed.append)
24
+
25
+ await api_keys.show(project_id="project-1")
26
+
27
+ assert printed == ["ma-key-1"]
28
+
29
+
30
+ @pytest.mark.asyncio
31
+ async def test_env_prints_shell_export_for_activated_api_key(
32
+ monkeypatch: pytest.MonkeyPatch,
33
+ ) -> None:
34
+ printed: list[str] = []
35
+
36
+ async def fake_resolve_project_id(*, project_id: str | None) -> str:
37
+ assert project_id == "project-1"
38
+ return "resolved-project"
39
+
40
+ async def fake_get_active_api_key(*, project_id: str) -> str | None:
41
+ assert project_id == "resolved-project"
42
+ return "ma-key-1"
43
+
44
+ monkeypatch.setattr(api_keys, "resolve_project_id", fake_resolve_project_id)
45
+ monkeypatch.setattr(api_keys, "get_active_api_key", fake_get_active_api_key)
46
+ monkeypatch.setattr(api_keys.typer, "echo", printed.append)
47
+
48
+ await api_keys.env(project_id="project-1")
49
+
50
+ assert printed == ["export MESHAGENT_API_KEY=ma-key-1"]
51
+
52
+
53
+ @pytest.mark.asyncio
54
+ async def test_show_exits_when_no_activated_api_key(
55
+ monkeypatch: pytest.MonkeyPatch,
56
+ ) -> None:
57
+ printed: list[str] = []
58
+
59
+ async def fake_resolve_project_id(*, project_id: str | None) -> str:
60
+ assert project_id == "project-1"
61
+ return "resolved-project"
62
+
63
+ async def fake_get_active_api_key(*, project_id: str) -> str | None:
64
+ assert project_id == "resolved-project"
65
+ return None
66
+
67
+ monkeypatch.setattr(api_keys, "resolve_project_id", fake_resolve_project_id)
68
+ monkeypatch.setattr(api_keys, "get_active_api_key", fake_get_active_api_key)
69
+ monkeypatch.setattr(api_keys, "print", printed.append)
70
+
71
+ with pytest.raises(typer.Exit) as exc_info:
72
+ await api_keys.show(project_id="project-1")
73
+
74
+ assert exc_info.value.exit_code == 1
75
+ assert printed == [
76
+ "[red]No activated API key found for project resolved-project. "
77
+ "Use meshagent api-key activate or meshagent api-key create "
78
+ "--activate to store one locally.[/red]"
79
+ ]
@@ -0,0 +1,380 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import importlib
5
+ import inspect
6
+ import sys
7
+ import threading
8
+ from dataclasses import dataclass
9
+ from functools import partial, wraps
10
+ from typing import Any, Callable, Sequence, TypeVar
11
+
12
+ import click
13
+ import typer
14
+ from typer import Typer
15
+ from typer.main import DeveloperExceptionConfig
16
+ from typer.main import _typer_developer_exception_attr_name
17
+ from typer.main import except_hook
18
+ from typer.main import get_command as get_typer_command
19
+ from typer.main import get_group as get_typer_group
20
+ from typer.main import get_install_completion_arguments
21
+
22
+ T = TypeVar("T")
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class LazyCommandRegistration:
27
+ name: str
28
+ module: str
29
+ attribute: str = "app"
30
+ help: str | None = None
31
+ short_help: str | None = None
32
+ hidden: bool = False
33
+ deprecated: bool = False
34
+ command_path: tuple[str, ...] = ()
35
+
36
+
37
+ class LazyLoadedCommand(click.Command):
38
+ def __init__(
39
+ self,
40
+ *,
41
+ registration: LazyCommandRegistration,
42
+ ) -> None:
43
+ super().__init__(
44
+ name=registration.name,
45
+ help=registration.help,
46
+ short_help=registration.short_help,
47
+ hidden=registration.hidden,
48
+ deprecated=registration.deprecated,
49
+ )
50
+ self._registration = registration
51
+ self._loaded_command: click.Command | None = None
52
+
53
+ def _load_command(self) -> click.Command:
54
+ if self._loaded_command is not None:
55
+ return self._loaded_command
56
+
57
+ module = importlib.import_module(self._registration.module)
58
+ try:
59
+ target = module.__dict__[self._registration.attribute]
60
+ except KeyError as exc:
61
+ raise RuntimeError(
62
+ f"{self._registration.module} has no attribute {self._registration.attribute}"
63
+ ) from exc
64
+
65
+ command = _coerce_to_click_command(target)
66
+ for segment in self._registration.command_path:
67
+ if not isinstance(command, click.Group):
68
+ raise RuntimeError(
69
+ f"{self._registration.module}.{self._registration.attribute} does not expose subcommand path "
70
+ f"{' '.join(self._registration.command_path)}"
71
+ )
72
+ resolved = command.get_command(click.Context(command), segment)
73
+ if resolved is None:
74
+ raise RuntimeError(
75
+ f"{self._registration.module}.{self._registration.attribute} has no subcommand {segment}"
76
+ )
77
+ command = resolved
78
+
79
+ self._loaded_command = command
80
+ return command
81
+
82
+ def make_context(
83
+ self,
84
+ info_name: str | None,
85
+ args: list[str],
86
+ parent: click.Context | None = None,
87
+ **extra: Any,
88
+ ) -> click.Context:
89
+ return self._load_command().make_context(
90
+ info_name,
91
+ args,
92
+ parent=parent,
93
+ **extra,
94
+ )
95
+
96
+ def shell_complete(
97
+ self, ctx: click.Context, incomplete: str
98
+ ) -> list[click.shell_completion.CompletionItem]:
99
+ return self._load_command().shell_complete(ctx, incomplete)
100
+
101
+ def get_help(self, ctx: click.Context) -> str:
102
+ return self._load_command().get_help(ctx)
103
+
104
+
105
+ def _coerce_to_click_command(target: Any) -> click.Command:
106
+ if isinstance(target, click.Command):
107
+ return target
108
+ if isinstance(target, Typer):
109
+ return get_command(target)
110
+ raise TypeError(f"Unsupported lazy command target: {target!r}")
111
+
112
+
113
+ def _materialize_command(command: click.Command) -> click.Command:
114
+ if isinstance(command, LazyLoadedCommand):
115
+ return _materialize_command(command._load_command())
116
+
117
+ if isinstance(command, click.Group):
118
+ command.commands = {
119
+ name: _materialize_command(subcommand)
120
+ for name, subcommand in command.commands.items()
121
+ }
122
+ return command
123
+
124
+
125
+ def get_command(
126
+ typer_instance: Typer | click.Command,
127
+ *,
128
+ materialize_lazy: bool = False,
129
+ ) -> click.Command:
130
+ if isinstance(typer_instance, click.Command):
131
+ click_command = typer_instance
132
+ elif isinstance(typer_instance, LazyTyper):
133
+ if len(typer_instance.registered_lazy_commands) > 0:
134
+ click_command = get_typer_group(typer_instance)
135
+ for registration in typer_instance.registered_lazy_commands:
136
+ click_command.commands[registration.name] = LazyLoadedCommand(
137
+ registration=registration,
138
+ )
139
+ if typer_instance._add_completion:
140
+ click_install_param, click_show_param = (
141
+ get_install_completion_arguments()
142
+ )
143
+ click_command.params.append(click_install_param)
144
+ click_command.params.append(click_show_param)
145
+ else:
146
+ click_command = get_typer_command(typer_instance)
147
+ else:
148
+ click_command = get_typer_command(typer_instance)
149
+
150
+ if materialize_lazy:
151
+ return _materialize_command(click_command)
152
+ return click_command
153
+
154
+
155
+ def collect_lazy_command_modules(root: "LazyTyper") -> list[str]:
156
+ modules: set[str] = set()
157
+ pending: list[LazyTyper] = [root]
158
+ seen_apps: set[int] = set()
159
+ seen_targets: set[tuple[str, str]] = set()
160
+
161
+ while pending:
162
+ app = pending.pop()
163
+ app_id = id(app)
164
+ if app_id in seen_apps:
165
+ continue
166
+ seen_apps.add(app_id)
167
+
168
+ for registration in app.registered_lazy_commands:
169
+ modules.add(registration.module)
170
+
171
+ target_key = (registration.module, registration.attribute)
172
+ if target_key in seen_targets:
173
+ continue
174
+ seen_targets.add(target_key)
175
+
176
+ module = importlib.import_module(registration.module)
177
+ try:
178
+ target = module.__dict__[registration.attribute]
179
+ except KeyError as exc:
180
+ raise RuntimeError(
181
+ f"{registration.module} has no attribute {registration.attribute}"
182
+ ) from exc
183
+
184
+ if isinstance(target, LazyTyper):
185
+ pending.append(target)
186
+
187
+ return sorted(modules)
188
+
189
+
190
+ def collect_lazy_command_modules_from_entrypoint(
191
+ module_name: str,
192
+ *,
193
+ attribute: str = "app",
194
+ ) -> list[str]:
195
+ module = importlib.import_module(module_name)
196
+ try:
197
+ target = module.__dict__[attribute]
198
+ except KeyError as exc:
199
+ raise RuntimeError(f"{module_name} has no attribute {attribute}") from exc
200
+
201
+ if not isinstance(target, LazyTyper):
202
+ raise TypeError(
203
+ f"{module_name}.{attribute} must be a LazyTyper, got {type(target)!r}"
204
+ )
205
+
206
+ return collect_lazy_command_modules(target)
207
+
208
+
209
+ def _run_coroutine_sync(
210
+ coro: "asyncio.Future[T] | asyncio.coroutines.Coroutine[Any, Any, T]",
211
+ ) -> T:
212
+ """
213
+ Run an awaitable from sync code.
214
+
215
+ - If we're not currently in an event loop, use asyncio.run().
216
+ - If we ARE in a running loop (e.g. inside an agent / notebook / ASGI app),
217
+ run asyncio.run() in a separate thread and block for the result.
218
+
219
+ This avoids: RuntimeError: asyncio.run() cannot be called from a running event loop
220
+ """
221
+ try:
222
+ asyncio.get_running_loop()
223
+ in_running_loop = True
224
+ except RuntimeError:
225
+ in_running_loop = False
226
+
227
+ if not in_running_loop:
228
+ return asyncio.run(coro) # type: ignore[arg-type]
229
+
230
+ result: dict[str, Any] = {}
231
+ done = threading.Event()
232
+
233
+ def _worker() -> None:
234
+ try:
235
+ result["value"] = asyncio.run(coro) # type: ignore[arg-type]
236
+ except BaseException as e:
237
+ result["error"] = e
238
+ finally:
239
+ done.set()
240
+
241
+ t = threading.Thread(target=_worker, daemon=True)
242
+ t.start()
243
+ done.wait()
244
+
245
+ if "error" in result:
246
+ raise result["error"]
247
+ return result["value"] # type: ignore[return-value]
248
+
249
+
250
+ def _missing_parameter_name(error: click.MissingParameter) -> str:
251
+ param = error.param
252
+ if isinstance(param, click.Option):
253
+ for option_name in param.opts:
254
+ if option_name.startswith("--"):
255
+ return option_name
256
+ if param.opts:
257
+ return param.opts[0]
258
+ if param.secondary_opts:
259
+ return param.secondary_opts[0]
260
+ if isinstance(param, click.Argument) and param.name is not None:
261
+ return param.name
262
+ if param is not None and param.name is not None:
263
+ return param.name
264
+
265
+ param_hint = error.param_hint
266
+ if isinstance(param_hint, str):
267
+ return param_hint
268
+ if param_hint:
269
+ return param_hint[0]
270
+ return "unknown"
271
+
272
+
273
+ class AsyncTyper(Typer):
274
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
275
+ if "no_args_is_help" not in kwargs:
276
+ kwargs["no_args_is_help"] = True
277
+ super().__init__(*args, **kwargs)
278
+
279
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
280
+ explicit_standalone_mode = "standalone_mode" in kwargs
281
+ if not explicit_standalone_mode:
282
+ kwargs["standalone_mode"] = False
283
+
284
+ if sys.excepthook != except_hook:
285
+ sys.excepthook = except_hook
286
+
287
+ try:
288
+ return get_command(self)(*args, **kwargs)
289
+ except click.MissingParameter as e:
290
+ if explicit_standalone_mode:
291
+ raise
292
+ missing_name = _missing_parameter_name(e)
293
+ click.secho(
294
+ f" Required parameter is missing: {missing_name}",
295
+ fg="red",
296
+ err=True,
297
+ )
298
+ if e.ctx is not None:
299
+ typer.echo(e.ctx.get_help(), err=True)
300
+ raise SystemExit(e.exit_code)
301
+ except click.ClickException as e:
302
+ if explicit_standalone_mode:
303
+ raise
304
+ e.show()
305
+ raise SystemExit(e.exit_code)
306
+ except click.Abort:
307
+ if explicit_standalone_mode:
308
+ raise
309
+ raise SystemExit(1)
310
+ except click.exceptions.Exit as e:
311
+ if explicit_standalone_mode:
312
+ raise
313
+ raise SystemExit(e.exit_code)
314
+ except Exception as e:
315
+ setattr(
316
+ e,
317
+ _typer_developer_exception_attr_name,
318
+ DeveloperExceptionConfig(
319
+ pretty_exceptions_enable=self.pretty_exceptions_enable,
320
+ pretty_exceptions_show_locals=self.pretty_exceptions_show_locals,
321
+ pretty_exceptions_short=self.pretty_exceptions_short,
322
+ ),
323
+ )
324
+ raise
325
+
326
+ @staticmethod
327
+ def maybe_run_async(decorator: Callable[..., Any], func: Callable[..., Any]) -> Any:
328
+ if inspect.iscoroutinefunction(func):
329
+
330
+ @wraps(func)
331
+ def runner(*args: Any, **kwargs: Any) -> Any:
332
+ return _run_coroutine_sync(func(*args, **kwargs))
333
+
334
+ decorator(runner)
335
+ else:
336
+ decorator(func)
337
+ return func
338
+
339
+ def callback(self, *args: Any, **kwargs: Any) -> Any:
340
+ decorator = super().callback(*args, **kwargs)
341
+ return partial(self.maybe_run_async, decorator)
342
+
343
+ def command(self, *args: Any, **kwargs: Any) -> Any:
344
+ decorator = super().command(*args, **kwargs)
345
+ return partial(self.maybe_run_async, decorator)
346
+
347
+ # keep your existing name if you prefer
348
+ def async_command(self, *args: Any, **kwargs: Any) -> Any:
349
+ return self.command(*args, **kwargs)
350
+
351
+
352
+ class LazyTyper(AsyncTyper):
353
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
354
+ super().__init__(*args, **kwargs)
355
+ self.registered_lazy_commands: list[LazyCommandRegistration] = []
356
+
357
+ def add_lazy_command(
358
+ self,
359
+ *,
360
+ name: str,
361
+ module: str,
362
+ attribute: str = "app",
363
+ help: str | None = None,
364
+ short_help: str | None = None,
365
+ hidden: bool = False,
366
+ deprecated: bool = False,
367
+ command_path: Sequence[str] = (),
368
+ ) -> None:
369
+ self.registered_lazy_commands.append(
370
+ LazyCommandRegistration(
371
+ name=name,
372
+ module=module,
373
+ attribute=attribute,
374
+ help=help,
375
+ short_help=short_help,
376
+ hidden=hidden,
377
+ deprecated=deprecated,
378
+ command_path=tuple(command_path),
379
+ )
380
+ )