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.
- llama_deploy/cli/__init__.py +32 -0
- llama_deploy/cli/client.py +173 -0
- llama_deploy/cli/commands.py +549 -0
- llama_deploy/cli/config.py +173 -0
- llama_deploy/cli/debug.py +16 -0
- llama_deploy/cli/env.py +30 -0
- llama_deploy/cli/interactive_prompts/utils.py +86 -0
- llama_deploy/cli/options.py +21 -0
- llama_deploy/cli/textual/deployment_form.py +409 -0
- llama_deploy/cli/textual/git_validation.py +357 -0
- llama_deploy/cli/textual/github_callback_server.py +207 -0
- llama_deploy/cli/textual/llama_loader.py +52 -0
- llama_deploy/cli/textual/profile_form.py +171 -0
- llama_deploy/cli/textual/secrets_form.py +186 -0
- llama_deploy/cli/textual/styles.tcss +162 -0
- llamactl-0.2.7a1.dist-info/METADATA +122 -0
- llamactl-0.2.7a1.dist-info/RECORD +19 -0
- llamactl-0.2.7a1.dist-info/WHEEL +4 -0
- llamactl-0.2.7a1.dist-info/entry_points.txt +3 -0
|
@@ -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,,
|