llamactl 0.2.7a1__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.
@@ -0,0 +1,171 @@
1
+ """Textual-based forms for CLI interactions"""
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ from textual.app import App, ComposeResult
8
+ from textual.containers import (
9
+ Container,
10
+ HorizontalGroup,
11
+ )
12
+ from textual.validation import Length
13
+ from textual.widgets import Button, Input, Label, Static
14
+
15
+ from ..config import Profile
16
+ from ..config import config_manager
17
+
18
+
19
+ @dataclass
20
+ class ProfileForm:
21
+ """Form data for profile editing/creation"""
22
+
23
+ name: str = ""
24
+ api_url: str = ""
25
+ active_project_id: str = ""
26
+ existing_name: Optional[str] = None
27
+
28
+ @classmethod
29
+ def from_profile(cls, profile: Profile) -> "ProfileForm":
30
+ """Create form from existing profile"""
31
+ return cls(
32
+ name=profile.name,
33
+ api_url=profile.api_url,
34
+ active_project_id=profile.active_project_id or "",
35
+ )
36
+
37
+
38
+ class ProfileEditApp(App[Optional[ProfileForm]]):
39
+ """Textual app for editing profiles"""
40
+
41
+ CSS_PATH = Path(__file__).parent / "styles.tcss"
42
+
43
+ def __init__(self, initial_data: ProfileForm):
44
+ super().__init__()
45
+ self.form_data = initial_data
46
+
47
+ def on_mount(self) -> None:
48
+ self.theme = "tokyo-night"
49
+
50
+ def on_key(self, event) -> None:
51
+ """Handle key events, including Ctrl+C"""
52
+ if event.key == "ctrl+c":
53
+ self.exit(None)
54
+
55
+ def compose(self) -> ComposeResult:
56
+ with Container(classes="form-container"):
57
+ title = "Edit Profile" if self.form_data.existing_name else "Create Profile"
58
+ yield Static(title, classes="primary-message")
59
+ yield Static("", id="error-message", classes="error-message hidden")
60
+ with Static(classes="two-column-form-grid"):
61
+ yield Label(
62
+ "Profile Name: *", classes="required form-label", shrink=True
63
+ )
64
+ yield Input(
65
+ value=self.form_data.name,
66
+ placeholder="Enter profile name",
67
+ validators=[Length(minimum=1)],
68
+ id="name",
69
+ compact=True,
70
+ )
71
+ yield Label("API URL: *", classes="required form-label", shrink=True)
72
+ yield Input(
73
+ value=self.form_data.api_url,
74
+ placeholder="https://prod-cloud-llama-deploy",
75
+ validators=[Length(minimum=1)],
76
+ id="api_url",
77
+ compact=True,
78
+ )
79
+ yield Label("Project ID:", classes="form-label", shrink=True)
80
+ yield Input(
81
+ value=self.form_data.active_project_id,
82
+ placeholder="Optional project ID",
83
+ id="project_id",
84
+ compact=True,
85
+ )
86
+ with HorizontalGroup(classes="button-row"):
87
+ yield Button("Save", variant="primary", id="save", compact=True)
88
+ yield Button("Cancel", variant="default", id="cancel", compact=True)
89
+
90
+ def on_button_pressed(self, event: Button.Pressed) -> None:
91
+ if event.button.id == "save":
92
+ if self._validate_form():
93
+ result = self._get_form_data()
94
+ try:
95
+ if result.existing_name:
96
+ config_manager.delete_profile(result.existing_name)
97
+ profile = config_manager.create_profile(
98
+ result.name,
99
+ result.api_url,
100
+ result.active_project_id,
101
+ )
102
+ self.exit(profile)
103
+ except Exception as e:
104
+ self._handle_error(e)
105
+
106
+ elif event.button.id == "cancel":
107
+ self.exit(None)
108
+
109
+ def _handle_error(self, error: Exception) -> None:
110
+ error_message = self.query_one("#error-message", Static)
111
+ error_message.update(f"Error creating profile: {error}")
112
+ error_message.add_class("visible")
113
+
114
+ def _validate_form(self) -> bool:
115
+ """Validate required fields"""
116
+ name_input = self.query_one("#name", Input)
117
+ api_url_input = self.query_one("#api_url", Input)
118
+ error_message = self.query_one("#error-message", Static)
119
+
120
+ errors = []
121
+
122
+ # Clear previous error state
123
+ name_input.remove_class("error")
124
+ api_url_input.remove_class("error")
125
+
126
+ if not name_input.value.strip():
127
+ name_input.add_class("error")
128
+ errors.append("Profile name is required")
129
+
130
+ if not api_url_input.value.strip():
131
+ api_url_input.add_class("error")
132
+ errors.append("API URL is required")
133
+
134
+ if errors:
135
+ error_message.update("; ".join(errors))
136
+ error_message.add_class("visible")
137
+ return False
138
+ else:
139
+ error_message.update("")
140
+ error_message.remove_class("visible")
141
+ return True
142
+
143
+ def _get_form_data(self) -> ProfileForm:
144
+ """Extract form data from inputs"""
145
+ name_input = self.query_one("#name", Input)
146
+ api_url_input = self.query_one("#api_url", Input)
147
+ project_id_input = self.query_one("#project_id", Input)
148
+
149
+ return ProfileForm(
150
+ name=name_input.value.strip(),
151
+ api_url=api_url_input.value.strip(),
152
+ active_project_id=project_id_input.value.strip(),
153
+ existing_name=self.form_data.existing_name,
154
+ )
155
+
156
+
157
+ def edit_profile_form(profile: Profile) -> ProfileForm | None:
158
+ """Launch profile edit form and return result"""
159
+ initial_data = ProfileForm.from_profile(profile)
160
+ initial_data.existing_name = profile.name or None
161
+ app = ProfileEditApp(initial_data)
162
+ return app.run()
163
+
164
+
165
+ def create_profile_form() -> Optional[ProfileForm]:
166
+ """Launch profile creation form and return result"""
167
+ return edit_profile_form(
168
+ Profile(
169
+ name="", api_url="https://prod-cloud-llama-deploy", active_project_id=None
170
+ )
171
+ )
@@ -0,0 +1,186 @@
1
+ """Textual-based deployment forms for CLI interactions"""
2
+
3
+ from llama_deploy.cli.env import load_env_secrets_from_string
4
+
5
+ from textual.app import ComposeResult
6
+ from textual.containers import HorizontalGroup, Widget
7
+ from textual.widgets import Button, Input, Label, Static, TextArea
8
+ from textual.reactive import reactive
9
+
10
+
11
+ class SecretsWidget(Widget):
12
+ """Widget for managing deployment secrets with reactive updates"""
13
+
14
+ DEFAULT_CSS = """
15
+ SecretsWidget {
16
+ padding-top: 1;
17
+ border-top: wide $secondary-muted;
18
+ layout: vertical;
19
+ height: auto;
20
+ overflow-y: auto;
21
+ }
22
+
23
+
24
+ .secrets-grid {
25
+ layout: grid;
26
+ grid-size: 4;
27
+ grid-columns: auto 1fr auto auto;
28
+ grid-rows: auto;
29
+ margin-bottom: 1;
30
+ grid-gutter: 0 1;
31
+ }
32
+
33
+ .secrets-header {
34
+ text-style: bold;
35
+ color: $text-accent;
36
+ margin-bottom: 1;
37
+ }
38
+
39
+ .add-secrets-button-row {
40
+ layout: horizontal;
41
+ align: right middle;
42
+ margin: 1 0;
43
+ }
44
+ #new_secrets {
45
+ max-height: 7;
46
+ }
47
+ SecretsWidget .secret-label {
48
+ color: $text-accent;
49
+ }
50
+
51
+ SecretsWidget .text-container {
52
+ padding: 0;
53
+ height: 7;
54
+ }
55
+ #new_secrets {
56
+ margin: 0;
57
+ padding: 0;
58
+ height: 7;
59
+ }
60
+
61
+
62
+ """
63
+ secrets = reactive({}, recompose=True) # Auto-recompose when secrets change
64
+ prior_secrets = reactive(set(), recompose=True)
65
+ visible_secrets = reactive(
66
+ set(), recompose=True
67
+ ) # Auto-recompose when visible secrets change
68
+
69
+ def __init__(
70
+ self,
71
+ initial_secrets: dict[str, str] | None = None,
72
+ prior_secrets: set[str] | None = None,
73
+ ):
74
+ super().__init__()
75
+ self.secrets = initial_secrets or {}
76
+ self.prior_secrets = prior_secrets or set()
77
+ self.visible_secrets = set()
78
+
79
+ def compose(self) -> ComposeResult:
80
+ """Compose the secrets section - called automatically when secrets change"""
81
+ yield Static("Environment Variables", classes="secrets-header")
82
+ secret_names = self.prior_secrets.union(self.secrets.keys())
83
+ hidden = len(secret_names) == 0
84
+ with Static(
85
+ classes="secrets-grid" + (" hidden" if hidden else ""),
86
+ id="secrets-grid",
87
+ ):
88
+ for secret_name in secret_names:
89
+ yield Label(f"{secret_name}:", classes="secret-label", shrink=True)
90
+ is_unknown = secret_name in self.prior_secrets
91
+ visible = secret_name in self.visible_secrets
92
+ yield Input(
93
+ value=self.secrets.get(secret_name, "***"),
94
+ placeholder="Leave blank to delete",
95
+ password=not visible,
96
+ id=f"secret_{secret_name}",
97
+ compact=True,
98
+ )
99
+ yield Button(
100
+ "hide" if visible and not is_unknown else "show",
101
+ id=f"show_{secret_name}",
102
+ compact=True,
103
+ disabled=is_unknown,
104
+ )
105
+ yield Button(
106
+ "delete",
107
+ id=f"delete_{secret_name}",
108
+ compact=True,
109
+ )
110
+
111
+ with HorizontalGroup(classes="text-container"):
112
+ yield TextArea(
113
+ "",
114
+ id="new_secrets",
115
+ show_line_numbers=True,
116
+ highlight_cursor_line=True,
117
+ )
118
+ with HorizontalGroup(classes="add-secrets-button-row"):
119
+ yield Button(
120
+ "Update Secrets",
121
+ classes="add-secret",
122
+ id="add_secrets",
123
+ compact=True,
124
+ )
125
+
126
+ def on_button_pressed(self, event: Button.Pressed) -> None:
127
+ """Handle button presses within the secrets widget"""
128
+ if event.button.id == "add_secrets":
129
+ self._add_secrets_from_textarea()
130
+ elif event.button.id and event.button.id.startswith("delete_"):
131
+ secret_name = event.button.id.removeprefix("delete_")
132
+ self._delete_secret(secret_name)
133
+ elif event.button.id and event.button.id.startswith("show_"):
134
+ secret_name = event.button.id.removeprefix("show_")
135
+ self._toggle_secret_visibility(secret_name)
136
+
137
+ def _add_secrets_from_textarea(self) -> None:
138
+ """Parse and add secrets from the textarea"""
139
+ textarea = self.query_one("#new_secrets", TextArea)
140
+ content = textarea.text.strip()
141
+
142
+ if not content:
143
+ return
144
+
145
+ # Parse .env format
146
+ new_secrets = load_env_secrets_from_string(content)
147
+
148
+ # Update secrets - this will trigger automatic recompose
149
+ updated_secrets = self.secrets.copy()
150
+ updated_secrets.update(new_secrets)
151
+ updated_prior_secrets = self.prior_secrets.copy()
152
+ updated_prior_secrets = updated_prior_secrets.difference(new_secrets.keys())
153
+ self.secrets = updated_secrets
154
+
155
+ # Clear textarea
156
+ textarea.text = ""
157
+
158
+ def _toggle_secret_visibility(self, secret_name: str) -> None:
159
+ """Toggle the visibility of a secret"""
160
+ visible_secrets = self.visible_secrets.copy()
161
+ if secret_name in visible_secrets:
162
+ visible_secrets.remove(secret_name)
163
+ else:
164
+ visible_secrets.add(secret_name)
165
+ self.visible_secrets = visible_secrets
166
+
167
+ def _delete_secret(self, secret_name: str) -> None:
168
+ """Delete a secret from the form"""
169
+ if secret_name in self.secrets:
170
+ updated_secrets = self.secrets.copy()
171
+ del updated_secrets[secret_name]
172
+ self.secrets = updated_secrets # Triggers automatic recompose
173
+ if secret_name in self.prior_secrets:
174
+ updated_prior_secrets = self.prior_secrets.copy()
175
+ updated_prior_secrets.remove(secret_name)
176
+ self.prior_secrets = updated_prior_secrets
177
+
178
+ def get_updated_secrets(self) -> dict[str, str]:
179
+ """Get current secrets with values from input fields"""
180
+ self._add_secrets_from_textarea()
181
+ return self.secrets
182
+
183
+ def get_updated_prior_secrets(self) -> set[str]:
184
+ """Get current prior secrets"""
185
+ self._add_secrets_from_textarea()
186
+ return self.prior_secrets
@@ -0,0 +1,162 @@
1
+ /* =============================================== */
2
+ /* GLOBAL LAYOUT & CONTAINERS */
3
+ /* =============================================== */
4
+
5
+ Screen {
6
+ align: center middle;
7
+ }
8
+
9
+ Container {
10
+ padding: 2;
11
+ margin: 2;
12
+ border: round $primary;
13
+ }
14
+
15
+ .form-container {
16
+ max-width: 100;
17
+ height: auto;
18
+ }
19
+
20
+ /* =============================================== */
21
+ /* FORM STRUCTURE & GRIDS */
22
+ /* =============================================== */
23
+
24
+ .two-column-form-grid {
25
+ layout: grid;
26
+ grid-size: 2;
27
+ grid-columns: auto 1fr;
28
+ grid-gutter: 0 1;
29
+ grid-rows: auto;
30
+ height: auto;
31
+ }
32
+
33
+ /* =============================================== */
34
+ /* FORM ELEMENTS */
35
+ /* =============================================== */
36
+
37
+ Label.required {
38
+ text-style: bold;
39
+ }
40
+
41
+ Label.form-label {
42
+ color: $text;
43
+ }
44
+
45
+
46
+ Input.error {
47
+ border: heavy $error;
48
+ }
49
+
50
+ Input.disabled {
51
+ color: $text-muted;
52
+ background: $surface-lighten-1;
53
+ }
54
+
55
+ /* =============================================== */
56
+ /* UTILITIES, MESSAGES & NOTIFICATIONS */
57
+ /* =============================================== */
58
+
59
+ .mb-1 {
60
+ margin-bottom: 1;
61
+ }
62
+
63
+ .mb-2 {
64
+ margin-bottom: 2;
65
+ }
66
+
67
+ .mt-1 {
68
+ margin-top: 1;
69
+ }
70
+
71
+ .mt-2 {
72
+ margin-top: 2;
73
+ }
74
+
75
+ .text-error {
76
+ color: $error;
77
+ }
78
+
79
+ .text-success {
80
+ color: $success;
81
+ }
82
+
83
+ .text-warning {
84
+ color: $warning;
85
+ }
86
+ .primary-message {
87
+ color: $text-accent;
88
+ background: $accent-muted;
89
+ border-left: heavy $accent;
90
+ margin: 0 0 1 0;
91
+ padding: 0 0 0 1
92
+ }
93
+
94
+ .error-message {
95
+ color: $text-error;
96
+ background: $error-muted;
97
+ border-left: heavy $error;
98
+ margin: 0 0 1 0;
99
+ padding: 0 0 0 1
100
+ }
101
+
102
+ .primary-message {
103
+ color: $text-primary;
104
+ background: $primary-muted;
105
+ border-left: heavy $primary;
106
+ margin: 0 0 1 0;
107
+ padding: 0 0 0 1
108
+ }
109
+
110
+ .secondary-message {
111
+ color: $text-secondary;
112
+ background: $secondary-muted;
113
+ border-left: heavy $secondary;
114
+ margin: 0 0 1 0;
115
+ padding: 0 0 0 1
116
+ }
117
+
118
+ .success-message {
119
+ color: $text-success;
120
+ background: $success-muted;
121
+ border-left: heavy $success;
122
+ padding: 1;
123
+ margin: 1 0;
124
+ }
125
+
126
+ .hidden {
127
+ display: none
128
+ }
129
+
130
+ .hidden.visible {
131
+ display: block;
132
+ }
133
+
134
+ .layout-horizontal {
135
+ layout: horizontal;
136
+ }
137
+ .layout-vertical {
138
+ layout: vertical;
139
+ }
140
+
141
+ /* =============================================== */
142
+ /* BUTTONS & ACTIONS */
143
+ /* =============================================== */
144
+
145
+ .button-row {
146
+ align: right middle;
147
+ }
148
+ .button-row Button {
149
+ margin-left: 1;
150
+ }
151
+
152
+
153
+ Button.danger {
154
+ background: $error;
155
+ color: $text-error;
156
+ }
157
+
158
+ Button.secondary {
159
+ margin-left: 1;
160
+ }
161
+
162
+
@@ -0,0 +1,122 @@
1
+ Metadata-Version: 2.3
2
+ Name: llamactl
3
+ Version: 0.2.7a1
4
+ Summary: A command-line interface for managing LlamaDeploy projects and deployments
5
+ Author: Adrian Lyjak
6
+ Author-email: Adrian Lyjak <adrianlyjak@gmail.com>
7
+ License: MIT
8
+ Requires-Dist: llama-deploy-core>=0.2.7a1,<0.3.0
9
+ Requires-Dist: llama-deploy-appserver>=0.2.7a1,<0.3.0
10
+ Requires-Dist: httpx>=0.24.0
11
+ Requires-Dist: rich>=13.0.0
12
+ Requires-Dist: questionary>=2.0.0
13
+ Requires-Dist: click>=8.2.1
14
+ Requires-Dist: python-dotenv>=1.0.0
15
+ Requires-Dist: tenacity>=9.1.2
16
+ Requires-Dist: textual>=4.0.0
17
+ Requires-Dist: aiohttp>=3.12.14
18
+ Requires-Python: >=3.12, <4
19
+ Description-Content-Type: text/markdown
20
+
21
+ # llamactl
22
+
23
+ > [!WARNING]
24
+ > This repository contains pre-release software. It is unstable, incomplete, and subject to breaking changes. Not recommended for use.
25
+
26
+
27
+ A command-line interface for managing LlamaDeploy projects and deployments.
28
+
29
+ ## Installation
30
+
31
+ Install from PyPI:
32
+
33
+ ```bash
34
+ pip install llamactl
35
+ ```
36
+
37
+ Or using uv:
38
+
39
+ ```bash
40
+ uv add llamactl
41
+ ```
42
+
43
+ ## Quick Start
44
+
45
+ 1. **Configure your profile**: Set up connection to your LlamaDeploy control plane
46
+ ```bash
47
+ llamactl profile configure
48
+ ```
49
+
50
+ 2. **Check health**: Verify connection to the control plane
51
+ ```bash
52
+ llamactl health
53
+ ```
54
+
55
+ 3. **Create a project**: Initialize a new deployment project
56
+ ```bash
57
+ llamactl project create my-project
58
+ ```
59
+
60
+ 4. **Deploy**: Deploy your project to the control plane
61
+ ```bash
62
+ llamactl deployment create my-deployment --project-name my-project
63
+ ```
64
+
65
+ ## Commands
66
+
67
+ ### Profile Management
68
+ - `llamactl profile configure` - Configure connection to control plane
69
+ - `llamactl profile show` - Show current profile configuration
70
+ - `llamactl profile list` - List all configured profiles
71
+
72
+ ### Project Management
73
+ - `llamactl project create <name>` - Create a new project
74
+ - `llamactl project list` - List all projects
75
+ - `llamactl project show <name>` - Show project details
76
+ - `llamactl project delete <name>` - Delete a project
77
+
78
+ ### Deployment Management
79
+ - `llamactl deployment create <name>` - Create a new deployment
80
+ - `llamactl deployment list` - List all deployments
81
+ - `llamactl deployment show <name>` - Show deployment details
82
+ - `llamactl deployment delete <name>` - Delete a deployment
83
+ - `llamactl deployment logs <name>` - View deployment logs
84
+
85
+ ### Health & Status
86
+ - `llamactl health` - Check control plane health
87
+ - `llamactl serve` - Start local development server
88
+
89
+ ## Configuration
90
+
91
+ llamactl stores configuration in your home directory at `~/.llamactl/`.
92
+
93
+ ### Profile Configuration
94
+ Profiles allow you to manage multiple control plane connections:
95
+
96
+ ```bash
97
+ # Configure default profile
98
+ llamactl profile configure
99
+
100
+ # Configure named profile
101
+ llamactl profile configure --profile production
102
+
103
+ # Use specific profile for commands
104
+ llamactl --profile production deployment list
105
+ ```
106
+
107
+ ## Development
108
+
109
+ This CLI is part of the LlamaDeploy ecosystem. For development setup:
110
+
111
+ 1. Clone the repository
112
+ 2. Install dependencies: `uv sync`
113
+ 3. Run tests: `uv run pytest`
114
+
115
+ ## Requirements
116
+
117
+ - Python 3.12+
118
+ - Access to a LlamaDeploy control plane
119
+
120
+ ## License
121
+
122
+ This project is licensed under the MIT License.
@@ -0,0 +1,19 @@
1
+ llama_deploy/cli/__init__.py,sha256=d0cda44bc0c9b76a5bff53f216c558541485910be36e94bd306dd0eeee8048c5,740
2
+ llama_deploy/cli/client.py,sha256=1518e395291356538e2c7c63b1ad424c5484161b921fc0f54d7f6ad5cdcd0ccc,6385
3
+ llama_deploy/cli/commands.py,sha256=d8ce617e18d0a7220d9c7f467f71db54bc6cfc49c3447fd5ae81c84bc95afe8f,17586
4
+ llama_deploy/cli/config.py,sha256=b339d95fceb7a15a183663032396aaeb2afffe1ddf06494416a6a0183a6658ca,6275
5
+ llama_deploy/cli/debug.py,sha256=e85a72d473bbe1645eb31772f7349bde703d45704166f767385895c440afc762,496
6
+ llama_deploy/cli/env.py,sha256=bb1dcde428c779796ad2b39b58d84f08df75a15031afca577aca0db5ce9a9ea0,1015
7
+ llama_deploy/cli/interactive_prompts/utils.py,sha256=59bd7cab8fe359d5a52e1805e8c9555b5cdf16f3699d7fa3be4d189503c4617f,2482
8
+ llama_deploy/cli/options.py,sha256=78b6e36e39fa88f0587146995e2cb66418b67d16f945f0b7570dab37cf5fc673,576
9
+ llama_deploy/cli/textual/deployment_form.py,sha256=7e7d0e40ae451ca7bf202be746378ae54af30b94b85a3c524ad02933c66cc182,15107
10
+ llama_deploy/cli/textual/git_validation.py,sha256=a00ab16b104b9f2f6c989243a65fb19f4b177562798a1174027e70b596fcb5c9,13378
11
+ llama_deploy/cli/textual/github_callback_server.py,sha256=a74b1f5741bdaa682086771fd73a145e1e22359601f16f036f72a87e64b0a152,7444
12
+ llama_deploy/cli/textual/llama_loader.py,sha256=dfef7118eb42d0fec033731b3f3b16ed4dbf4c551f4059c36e290e73c9aa5d13,1244
13
+ llama_deploy/cli/textual/profile_form.py,sha256=2c6ca4690c22b499cc327b117c97e7914d4243b73faa92c0f5ac9cfdcf59b3d7,6015
14
+ llama_deploy/cli/textual/secrets_form.py,sha256=1fd47a5a5ee9dfa0fd2a86f5888894820897c55fbb0cd30e60d6bc08570288b5,6303
15
+ llama_deploy/cli/textual/styles.tcss,sha256=72338c5634bae0547384669382c4e06deec1380ef7bbc31099b1dca8ce49b2d0,2711
16
+ llamactl-0.2.7a1.dist-info/WHEEL,sha256=cc8ae5806c5874a696cde0fcf78fdf73db4982e7c824f3ceab35e2b65182fa1a,79
17
+ llamactl-0.2.7a1.dist-info/entry_points.txt,sha256=b67e1eb64305058751a651a80f2d2268b5f7046732268421e796f64d4697f83c,52
18
+ llamactl-0.2.7a1.dist-info/METADATA,sha256=b74e8a9886a9823e5b65476b5b0712a17d73d3ef6117e058c86bd290a70fac8a,3137
19
+ llamactl-0.2.7a1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.7.21
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ llamactl = llama_deploy.cli:main
3
+