agentstack-cli 0.4.0__py3-none-macosx_12_0_arm64.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.
- agentstack_cli/__init__.py +77 -0
- agentstack_cli/api.py +125 -0
- agentstack_cli/async_typer.py +104 -0
- agentstack_cli/auth_manager.py +115 -0
- agentstack_cli/commands/__init__.py +3 -0
- agentstack_cli/commands/agent.py +1077 -0
- agentstack_cli/commands/build.py +184 -0
- agentstack_cli/commands/mcp.py +141 -0
- agentstack_cli/commands/model.py +624 -0
- agentstack_cli/commands/platform/__init__.py +181 -0
- agentstack_cli/commands/platform/base_driver.py +222 -0
- agentstack_cli/commands/platform/istio.py +186 -0
- agentstack_cli/commands/platform/lima_driver.py +210 -0
- agentstack_cli/commands/platform/wsl_driver.py +226 -0
- agentstack_cli/commands/self.py +206 -0
- agentstack_cli/commands/server.py +237 -0
- agentstack_cli/configuration.py +76 -0
- agentstack_cli/console.py +25 -0
- agentstack_cli/data/.gitignore +2 -0
- agentstack_cli/data/helm-chart.tgz +0 -0
- agentstack_cli/data/lima-guestagent.Linux-aarch64.gz +0 -0
- agentstack_cli/data/limactl +0 -0
- agentstack_cli/utils.py +281 -0
- agentstack_cli-0.4.0.dist-info/METADATA +104 -0
- agentstack_cli-0.4.0.dist-info/RECORD +27 -0
- agentstack_cli-0.4.0.dist-info/WHEEL +4 -0
- agentstack_cli-0.4.0.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import importlib.resources
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
import typing
|
|
10
|
+
import uuid
|
|
11
|
+
from subprocess import CompletedProcess
|
|
12
|
+
|
|
13
|
+
import anyio
|
|
14
|
+
import psutil
|
|
15
|
+
import pydantic
|
|
16
|
+
import yaml
|
|
17
|
+
|
|
18
|
+
from agentstack_cli.commands.platform.base_driver import BaseDriver
|
|
19
|
+
from agentstack_cli.configuration import Configuration
|
|
20
|
+
from agentstack_cli.console import console
|
|
21
|
+
from agentstack_cli.utils import run_command
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class LimaDriver(BaseDriver):
|
|
25
|
+
limactl_exe: str
|
|
26
|
+
|
|
27
|
+
def __init__(self, vm_name: str = "agentstack"):
|
|
28
|
+
super().__init__(vm_name)
|
|
29
|
+
bundled_limactl_exe = importlib.resources.files("agentstack_cli") / "data" / "limactl"
|
|
30
|
+
if bundled_limactl_exe.is_file():
|
|
31
|
+
self.limactl_exe = str(bundled_limactl_exe)
|
|
32
|
+
else:
|
|
33
|
+
self.limactl_exe = str(shutil.which("limactl"))
|
|
34
|
+
console.warning(f"Using external Lima from {self.limactl_exe}")
|
|
35
|
+
|
|
36
|
+
@typing.override
|
|
37
|
+
async def run_in_vm(
|
|
38
|
+
self,
|
|
39
|
+
command: list[str],
|
|
40
|
+
message: str,
|
|
41
|
+
env: dict[str, str] | None = None,
|
|
42
|
+
input: bytes | None = None,
|
|
43
|
+
) -> CompletedProcess[bytes]:
|
|
44
|
+
return await run_command(
|
|
45
|
+
[self.limactl_exe, "shell", f"--tty={sys.stdin.isatty()}", self.vm_name, "--", "sudo", *command],
|
|
46
|
+
message,
|
|
47
|
+
env={"LIMA_HOME": str(Configuration().lima_home)} | (env or {}),
|
|
48
|
+
cwd="/",
|
|
49
|
+
input=input,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@typing.override
|
|
53
|
+
async def status(self) -> typing.Literal["running"] | str | None:
|
|
54
|
+
try:
|
|
55
|
+
result = await run_command(
|
|
56
|
+
[self.limactl_exe, "--tty=false", "list", "--format=json"],
|
|
57
|
+
"Looking for existing Agent Stack platform in Lima",
|
|
58
|
+
env={"LIMA_HOME": str(Configuration().lima_home)},
|
|
59
|
+
cwd="/",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
for line in result.stdout.decode().split("\n"):
|
|
63
|
+
if not line:
|
|
64
|
+
continue
|
|
65
|
+
status = pydantic.TypeAdapter(typing.TypedDict("Status", {"name": str, "status": str})).validate_json(
|
|
66
|
+
line
|
|
67
|
+
)
|
|
68
|
+
if status["name"] == self.vm_name:
|
|
69
|
+
return status["status"].lower()
|
|
70
|
+
return None
|
|
71
|
+
except Exception:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
@typing.override
|
|
75
|
+
async def create_vm(self):
|
|
76
|
+
Configuration().home.mkdir(exist_ok=True)
|
|
77
|
+
current_status = await self.status()
|
|
78
|
+
|
|
79
|
+
if not current_status:
|
|
80
|
+
await run_command(
|
|
81
|
+
[self.limactl_exe, "--tty=false", "delete", "--force", self.vm_name],
|
|
82
|
+
"Cleaning up remains of previous instance",
|
|
83
|
+
env={"LIMA_HOME": str(Configuration().lima_home)},
|
|
84
|
+
check=False,
|
|
85
|
+
cwd="/",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
await run_command(
|
|
89
|
+
[self.limactl_exe, "--tty=false", "delete", "--force", "beeai-platform"],
|
|
90
|
+
"Cleaning up remains of legacy instance",
|
|
91
|
+
env={"LIMA_HOME": str(Configuration().lima_home)},
|
|
92
|
+
check=False,
|
|
93
|
+
cwd="/",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
total_memory_gib = typing.cast(int, psutil.virtual_memory().total / (1024**3))
|
|
97
|
+
|
|
98
|
+
if total_memory_gib < 4:
|
|
99
|
+
console.error("Not enough memory. Agent Stack platform requires at least 4 GB of RAM.")
|
|
100
|
+
sys.exit(1)
|
|
101
|
+
|
|
102
|
+
if total_memory_gib < 8:
|
|
103
|
+
console.warning("Less than 8 GB of RAM detected. Performance may be degraded.")
|
|
104
|
+
|
|
105
|
+
vm_memory_gib = round(min(8, max(3, total_memory_gib / 2)))
|
|
106
|
+
|
|
107
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete_on_close=False) as template_file:
|
|
108
|
+
template_file.write(
|
|
109
|
+
yaml.dump(
|
|
110
|
+
{
|
|
111
|
+
"images": [
|
|
112
|
+
{
|
|
113
|
+
"location": "https://cloud-images.ubuntu.com/releases/noble/release/ubuntu-24.04-server-cloudimg-amd64.img",
|
|
114
|
+
"arch": "x86_64",
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"location": "https://cloud-images.ubuntu.com/releases/noble/release/ubuntu-24.04-server-cloudimg-arm64.img",
|
|
118
|
+
"arch": "aarch64",
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
"portForwards": [
|
|
122
|
+
{
|
|
123
|
+
"guestIP": "127.0.0.1",
|
|
124
|
+
"guestPortRange": [1024, 65535],
|
|
125
|
+
"hostPortRange": [1024, 65535],
|
|
126
|
+
"hostIP": "127.0.0.1",
|
|
127
|
+
},
|
|
128
|
+
{"guestIP": "0.0.0.0", "proto": "any", "ignore": True},
|
|
129
|
+
],
|
|
130
|
+
"mounts": [
|
|
131
|
+
{"location": "/tmp/agentstack", "mountPoint": "/tmp/agentstack", "writable": True}
|
|
132
|
+
],
|
|
133
|
+
"containerd": {"system": False, "user": False},
|
|
134
|
+
"hostResolver": {"hosts": {"host.docker.internal": "host.lima.internal"}},
|
|
135
|
+
"memory": f"{vm_memory_gib}GiB",
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
template_file.flush()
|
|
140
|
+
template_file.close()
|
|
141
|
+
await run_command(
|
|
142
|
+
[
|
|
143
|
+
self.limactl_exe,
|
|
144
|
+
"--tty=false",
|
|
145
|
+
"start",
|
|
146
|
+
str(template_file.name),
|
|
147
|
+
f"--name={self.vm_name}",
|
|
148
|
+
],
|
|
149
|
+
"Creating a Lima VM",
|
|
150
|
+
env={"LIMA_HOME": str(Configuration().lima_home)},
|
|
151
|
+
cwd="/",
|
|
152
|
+
)
|
|
153
|
+
elif current_status != "running":
|
|
154
|
+
await run_command(
|
|
155
|
+
[self.limactl_exe, "--tty=false", "start", self.vm_name],
|
|
156
|
+
"Starting up",
|
|
157
|
+
env={"LIMA_HOME": str(Configuration().lima_home)},
|
|
158
|
+
cwd="/",
|
|
159
|
+
)
|
|
160
|
+
else:
|
|
161
|
+
console.info("Updating an existing instance.")
|
|
162
|
+
|
|
163
|
+
@typing.override
|
|
164
|
+
async def stop(self):
|
|
165
|
+
await run_command(
|
|
166
|
+
[self.limactl_exe, "--tty=false", "stop", "--force", self.vm_name],
|
|
167
|
+
"Stopping Agent Stack VM",
|
|
168
|
+
env={"LIMA_HOME": str(Configuration().lima_home)},
|
|
169
|
+
cwd="/",
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
@typing.override
|
|
173
|
+
async def delete(self):
|
|
174
|
+
await run_command(
|
|
175
|
+
[self.limactl_exe, "--tty=false", "delete", "--force", self.vm_name],
|
|
176
|
+
"Deleting Agent Stack platform",
|
|
177
|
+
env={"LIMA_HOME": str(Configuration().lima_home)},
|
|
178
|
+
check=False,
|
|
179
|
+
cwd="/",
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
@typing.override
|
|
183
|
+
async def import_image(self, tag: str):
|
|
184
|
+
image_dir = anyio.Path("/tmp/agentstack")
|
|
185
|
+
await image_dir.mkdir(exist_ok=True, parents=True)
|
|
186
|
+
image_file = str(uuid.uuid4())
|
|
187
|
+
image_path = image_dir / image_file
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
await run_command(
|
|
191
|
+
["docker", "image", "save", "-o", str(image_path), tag], f"Exporting image {tag} from Docker"
|
|
192
|
+
)
|
|
193
|
+
await self.run_in_vm(
|
|
194
|
+
["/bin/sh", "-c", f"k3s ctr images import /tmp/agentstack/{image_file}"],
|
|
195
|
+
f"Importing image {tag} into Agent Stack platform",
|
|
196
|
+
)
|
|
197
|
+
finally:
|
|
198
|
+
await image_path.unlink(missing_ok=True)
|
|
199
|
+
|
|
200
|
+
@typing.override
|
|
201
|
+
async def exec(self, command: list[str]):
|
|
202
|
+
await anyio.run_process(
|
|
203
|
+
[self.limactl_exe, "shell", f"--tty={sys.stdin.isatty()}", self.vm_name, "--", *command],
|
|
204
|
+
input=None if sys.stdin.isatty() else sys.stdin.read().encode(),
|
|
205
|
+
check=False,
|
|
206
|
+
stdout=None,
|
|
207
|
+
stderr=None,
|
|
208
|
+
env={**os.environ, "LIMA_HOME": str(Configuration().lima_home)},
|
|
209
|
+
cwd="/",
|
|
210
|
+
)
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import configparser
|
|
5
|
+
import os
|
|
6
|
+
import pathlib
|
|
7
|
+
import platform
|
|
8
|
+
import sys
|
|
9
|
+
import textwrap
|
|
10
|
+
import typing
|
|
11
|
+
|
|
12
|
+
import anyio
|
|
13
|
+
import pydantic
|
|
14
|
+
import yaml
|
|
15
|
+
from InquirerPy import inquirer
|
|
16
|
+
from InquirerPy.base.control import Choice
|
|
17
|
+
|
|
18
|
+
from agentstack_cli.commands.platform.base_driver import BaseDriver
|
|
19
|
+
from agentstack_cli.configuration import Configuration
|
|
20
|
+
from agentstack_cli.console import console
|
|
21
|
+
from agentstack_cli.utils import run_command
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class WSLDriver(BaseDriver):
|
|
25
|
+
@typing.override
|
|
26
|
+
async def run_in_vm(
|
|
27
|
+
self,
|
|
28
|
+
command: list[str],
|
|
29
|
+
message: str,
|
|
30
|
+
env: dict[str, str] | None = None,
|
|
31
|
+
input: bytes | None = None,
|
|
32
|
+
check: bool = True,
|
|
33
|
+
):
|
|
34
|
+
return await run_command(
|
|
35
|
+
["wsl.exe", "--user", "root", "--distribution", self.vm_name, "--", *command],
|
|
36
|
+
message,
|
|
37
|
+
env={**(env or {}), "WSL_UTF8": "1", "WSLENV": os.getenv("WSLENV", "") + ":WSL_UTF8"},
|
|
38
|
+
input=input,
|
|
39
|
+
check=check,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
@typing.override
|
|
43
|
+
async def status(self) -> typing.Literal["running"] | str | None:
|
|
44
|
+
try:
|
|
45
|
+
for status, cmd in [("running", ["--running"]), ("stopped", [])]:
|
|
46
|
+
result = await run_command(
|
|
47
|
+
["wsl.exe", "--list", "--quiet", *cmd],
|
|
48
|
+
f"Looking for {status} Agent Stack platform in WSL",
|
|
49
|
+
env={"WSL_UTF8": "1", "WSLENV": os.getenv("WSLENV", "") + ":WSL_UTF8"},
|
|
50
|
+
)
|
|
51
|
+
if self.vm_name in result.stdout.decode().splitlines():
|
|
52
|
+
return status
|
|
53
|
+
return None
|
|
54
|
+
except Exception:
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
@typing.override
|
|
58
|
+
async def create_vm(self):
|
|
59
|
+
if (await run_command(["wsl.exe", "--status"], "Checking for WSL2", check=False)).returncode != 0:
|
|
60
|
+
await run_command(["wsl.exe", "--install", "--no-launch", "--web-download"], "Installing WSL2")
|
|
61
|
+
await run_command(["wsl.exe", "--upgrade"], "Upgrading WSL2", check=False)
|
|
62
|
+
|
|
63
|
+
config_file = (
|
|
64
|
+
pathlib.Path.home()
|
|
65
|
+
if platform.system() == "Windows"
|
|
66
|
+
else pathlib.Path(
|
|
67
|
+
(
|
|
68
|
+
await run_command(
|
|
69
|
+
["/bin/sh", "-c", '''wslpath "$(cmd.exe /c 'echo %USERPROFILE%')"'''], "Detecting home path"
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
.stdout.decode()
|
|
73
|
+
.strip()
|
|
74
|
+
)
|
|
75
|
+
) / ".wslconfig"
|
|
76
|
+
config_file.touch()
|
|
77
|
+
with config_file.open("r+") as f:
|
|
78
|
+
config = configparser.ConfigParser()
|
|
79
|
+
f.seek(0)
|
|
80
|
+
config.read_file(f)
|
|
81
|
+
|
|
82
|
+
if not config.has_section("wsl2"):
|
|
83
|
+
config.add_section("wsl2")
|
|
84
|
+
|
|
85
|
+
if (
|
|
86
|
+
config.get("wsl2", "networkingMode", fallback=None) != "mirrored"
|
|
87
|
+
and await inquirer.select( # type: ignore
|
|
88
|
+
textwrap.dedent("""\
|
|
89
|
+
The Agent Stack platform needs to switch WSL to `mirrored` networking mode in order to support connecting to Windows applications -- like Ollama or self-registered agents. If you skip this step, these features won't be available.
|
|
90
|
+
|
|
91
|
+
However, the default `nat` mode is required by some software, like Docker Desktop or Rancher Desktop, to function properly. If you use such software, you may want to keep the default `nat` mode.
|
|
92
|
+
|
|
93
|
+
(It can be changed anytime later in C:/Users/<your name>/.wslconfig, followed by `wsl --shutdown` and `agentstack platform start` to apply changes.)
|
|
94
|
+
"""),
|
|
95
|
+
choices=[
|
|
96
|
+
Choice(
|
|
97
|
+
value=True,
|
|
98
|
+
name="Change WSL2 networking mode to `mirrored`",
|
|
99
|
+
),
|
|
100
|
+
Choice(
|
|
101
|
+
value=False,
|
|
102
|
+
name="Leave WSL2 networking mode as `nat`",
|
|
103
|
+
),
|
|
104
|
+
],
|
|
105
|
+
).execute_async()
|
|
106
|
+
):
|
|
107
|
+
config.set("wsl2", "networkingMode", "mirrored")
|
|
108
|
+
f.seek(0)
|
|
109
|
+
f.truncate(0)
|
|
110
|
+
config.write(f)
|
|
111
|
+
|
|
112
|
+
if platform.system() == "Linux":
|
|
113
|
+
console.warning(
|
|
114
|
+
"WSL networking mode updated. Please close WSL, run [green]wsl --shutdown[/green] from PowerShell, re-open WSL and run [green]agentstack platform start[/green] again."
|
|
115
|
+
)
|
|
116
|
+
sys.exit(1)
|
|
117
|
+
await run_command(["wsl.exe", "--shutdown"], "Updating WSL2 networking")
|
|
118
|
+
|
|
119
|
+
Configuration().home.mkdir(exist_ok=True)
|
|
120
|
+
if not await self.status():
|
|
121
|
+
await run_command(
|
|
122
|
+
["wsl.exe", "--unregister", self.vm_name], "Cleaning up remains of previous instance", check=False
|
|
123
|
+
)
|
|
124
|
+
await run_command(
|
|
125
|
+
["wsl.exe", "--unregister", "beeai-platform"], "Cleaning up remains of legacy instance", check=False
|
|
126
|
+
)
|
|
127
|
+
await run_command(
|
|
128
|
+
["wsl.exe", "--install", "--name", self.vm_name, "--no-launch", "--web-download"],
|
|
129
|
+
"Creating a WSL distribution",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
await self.run_in_vm(
|
|
133
|
+
[
|
|
134
|
+
"sh",
|
|
135
|
+
"-c",
|
|
136
|
+
"echo '[network]\ngenerateResolvConf = false\n[boot]\nsystemd=true\n' >/etc/wsl.conf && rm /etc/resolv.conf && echo 'nameserver 1.1.1.1\n' >/etc/resolv.conf && chattr +i /etc/resolv.conf",
|
|
137
|
+
],
|
|
138
|
+
"Setting up DNS configuration",
|
|
139
|
+
check=False,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
await run_command(["wsl.exe", "--terminate", self.vm_name], "Restarting Agent Stack VM")
|
|
143
|
+
await self.run_in_vm(["dbus-launch", "true"], "Ensuring persistence of Agent Stack VM")
|
|
144
|
+
|
|
145
|
+
@typing.override
|
|
146
|
+
async def deploy(
|
|
147
|
+
self,
|
|
148
|
+
set_values_list: list[str],
|
|
149
|
+
values_file: pathlib.Path | None = None,
|
|
150
|
+
import_images: list[str] | None = None,
|
|
151
|
+
) -> None:
|
|
152
|
+
await self.run_in_vm(
|
|
153
|
+
["k3s", "kubectl", "apply", "-f", "-"],
|
|
154
|
+
"Setting up internal networking",
|
|
155
|
+
input=yaml.dump(
|
|
156
|
+
{
|
|
157
|
+
"apiVersion": "v1",
|
|
158
|
+
"kind": "ConfigMap",
|
|
159
|
+
"metadata": {"name": "coredns-custom", "namespace": "kube-system"},
|
|
160
|
+
"data": {
|
|
161
|
+
"default.server": "host.docker.internal {\n hosts {\n 127.0.0.1 host.docker.internal\n fallthrough\n }\n}"
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
).encode(),
|
|
165
|
+
)
|
|
166
|
+
await super().deploy(set_values_list=set_values_list, values_file=values_file, import_images=import_images)
|
|
167
|
+
await self.run_in_vm(
|
|
168
|
+
["sh", "-c", "cat >/etc/systemd/system/kubectl-port-forward@.service"],
|
|
169
|
+
"Installing systemd unit for port-forwarding",
|
|
170
|
+
input=textwrap.dedent("""\
|
|
171
|
+
[Unit]
|
|
172
|
+
Description=Kubectl Port Forward for service %%i
|
|
173
|
+
After=network.target
|
|
174
|
+
|
|
175
|
+
[Service]
|
|
176
|
+
Type=simple
|
|
177
|
+
ExecStart=/bin/bash -c 'IFS=":" read svc port <<< "%i"; exec /usr/local/bin/kubectl port-forward --address=127.0.0.1 svc/$svc $port:$port'
|
|
178
|
+
Restart=on-failure
|
|
179
|
+
User=root
|
|
180
|
+
|
|
181
|
+
[Install]
|
|
182
|
+
WantedBy=multi-user.target
|
|
183
|
+
""").encode(),
|
|
184
|
+
)
|
|
185
|
+
await self.run_in_vm(["systemctl", "daemon-reexec"], "Reloading systemd")
|
|
186
|
+
services_json = (
|
|
187
|
+
await self.run_in_vm(
|
|
188
|
+
["k3s", "kubectl", "get", "svc", "--field-selector=spec.type=LoadBalancer", "--output=json"],
|
|
189
|
+
"Detecting ports to forward",
|
|
190
|
+
)
|
|
191
|
+
).stdout
|
|
192
|
+
ServicePort = typing.TypedDict("ServicePort", {"port": int, "name": str})
|
|
193
|
+
ServiceSpec = typing.TypedDict("ServiceSpec", {"ports": list[ServicePort]})
|
|
194
|
+
ServiceMetadata = typing.TypedDict("ServiceMetadata", {"name": str, "namespace": str})
|
|
195
|
+
Service = typing.TypedDict("Service", {"metadata": ServiceMetadata, "spec": ServiceSpec})
|
|
196
|
+
Services = typing.TypedDict("Services", {"items": list[Service]})
|
|
197
|
+
for service in pydantic.TypeAdapter(Services).validate_json(services_json)["items"]:
|
|
198
|
+
name = service["metadata"]["name"]
|
|
199
|
+
for port_item in service["spec"]["ports"]:
|
|
200
|
+
port = port_item["port"]
|
|
201
|
+
await self.run_in_vm(
|
|
202
|
+
["systemctl", "enable", "--now", f"kubectl-port-forward@{name}:{port}.service"],
|
|
203
|
+
f"Starting port-forward for {name}:{port}",
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
@typing.override
|
|
207
|
+
async def stop(self):
|
|
208
|
+
await run_command(["wsl.exe", "--terminate", self.vm_name], "Stopping Agent Stack VM")
|
|
209
|
+
|
|
210
|
+
@typing.override
|
|
211
|
+
async def delete(self):
|
|
212
|
+
await run_command(["wsl.exe", "--unregister", self.vm_name], "Deleting Agent Stack platform", check=False)
|
|
213
|
+
|
|
214
|
+
@typing.override
|
|
215
|
+
async def import_image(self, tag: str) -> None:
|
|
216
|
+
raise NotImplementedError("Importing images is not supported on this platform.")
|
|
217
|
+
|
|
218
|
+
@typing.override
|
|
219
|
+
async def exec(self, command: list[str]):
|
|
220
|
+
await anyio.run_process(
|
|
221
|
+
["wsl.exe", "--user", "root", "--distribution", self.vm_name, "--", *command],
|
|
222
|
+
input=None if sys.stdin.isatty() else sys.stdin.read().encode(),
|
|
223
|
+
check=False,
|
|
224
|
+
stdout=None,
|
|
225
|
+
stderr=None,
|
|
226
|
+
)
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import functools
|
|
3
|
+
import importlib.metadata
|
|
4
|
+
import os
|
|
5
|
+
import platform
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import typing
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
import packaging.version
|
|
13
|
+
import pydantic
|
|
14
|
+
import typer
|
|
15
|
+
from InquirerPy import inquirer
|
|
16
|
+
|
|
17
|
+
import agentstack_cli.commands.platform
|
|
18
|
+
from agentstack_cli.async_typer import AsyncTyper
|
|
19
|
+
from agentstack_cli.commands.model import setup as model_setup
|
|
20
|
+
from agentstack_cli.configuration import Configuration
|
|
21
|
+
from agentstack_cli.console import console
|
|
22
|
+
from agentstack_cli.utils import run_command, verbosity
|
|
23
|
+
|
|
24
|
+
app = AsyncTyper()
|
|
25
|
+
configuration = Configuration()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@functools.cache
|
|
29
|
+
def _path() -> str:
|
|
30
|
+
# These are PATHs where `uv` installs itself when installed through own install script
|
|
31
|
+
# Package managers may install elsewhere, but that location should already be in PATH
|
|
32
|
+
return os.pathsep.join(
|
|
33
|
+
[
|
|
34
|
+
*([xdg_bin_home] if (xdg_bin_home := os.getenv("XDG_BIN_HOME")) else []),
|
|
35
|
+
*([os.path.realpath(f"{xdg_data_home}/../bin")] if (xdg_data_home := os.getenv("XDG_DATA_HOME")) else []),
|
|
36
|
+
os.path.expanduser("~/.local/bin"),
|
|
37
|
+
os.getenv("PATH", ""),
|
|
38
|
+
]
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.command("version")
|
|
43
|
+
async def version(verbose: typing.Annotated[bool, typer.Option("-v", help="Show verbose output")] = False):
|
|
44
|
+
"""Print version of the Agent Stack CLI."""
|
|
45
|
+
with verbosity(verbose=verbose):
|
|
46
|
+
cli_version = importlib.metadata.version("agentstack-cli")
|
|
47
|
+
platform_version = await agentstack_cli.commands.platform.get_driver().version()
|
|
48
|
+
|
|
49
|
+
latest_cli_version: str | None = None
|
|
50
|
+
with console.status("Checking for newer version...", spinner="dots"):
|
|
51
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
52
|
+
response = await client.get("https://pypi.org/pypi/agentstack-cli/json")
|
|
53
|
+
PyPIPackageInfo = typing.TypedDict("PyPIPackageInfo", {"version": str})
|
|
54
|
+
PyPIPackage = typing.TypedDict("PyPIPackage", {"info": PyPIPackageInfo})
|
|
55
|
+
if response.status_code == 200:
|
|
56
|
+
latest_cli_version = pydantic.TypeAdapter(PyPIPackage).validate_json(response.text)["info"][
|
|
57
|
+
"version"
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
console.print()
|
|
61
|
+
console.print(f" agentstack-cli version: [bold]{cli_version}[/bold]")
|
|
62
|
+
console.print(
|
|
63
|
+
f"agentstack-platform version: [bold]{platform_version.replace('-', '') if platform_version is not None else 'not running'}[/bold]"
|
|
64
|
+
)
|
|
65
|
+
console.print()
|
|
66
|
+
|
|
67
|
+
if latest_cli_version and packaging.version.parse(latest_cli_version) > packaging.version.parse(cli_version):
|
|
68
|
+
console.hint(
|
|
69
|
+
f"A newer version ([bold]{latest_cli_version}[/bold]) is available. Update using: [green]agentstack self upgrade[/green]."
|
|
70
|
+
)
|
|
71
|
+
elif platform_version is None:
|
|
72
|
+
console.hint("Start the Agent Stack platform using: [green]agentstack platform start[/green]")
|
|
73
|
+
elif platform_version.replace("-", "") != cli_version:
|
|
74
|
+
console.hint("Update the Agent Stack platform using: [green]agentstack platform start[/green]")
|
|
75
|
+
else:
|
|
76
|
+
console.success("Everything is up to date!")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@app.command("install")
|
|
80
|
+
async def install(
|
|
81
|
+
verbose: typing.Annotated[bool, typer.Option("-v", help="Show verbose output")] = False,
|
|
82
|
+
):
|
|
83
|
+
"""Install Agent Stack platform pre-requisites."""
|
|
84
|
+
with verbosity(verbose=verbose):
|
|
85
|
+
ready_to_start = False
|
|
86
|
+
if platform.system() == "Linux":
|
|
87
|
+
if shutil.which(
|
|
88
|
+
f"qemu-system-{'aarch64' if platform.machine().lower() == 'arm64' else platform.machine().lower()}"
|
|
89
|
+
):
|
|
90
|
+
ready_to_start = True
|
|
91
|
+
else:
|
|
92
|
+
if os.geteuid() != 0:
|
|
93
|
+
console.hint(
|
|
94
|
+
"You may be prompted for your password to install QEMU, as this needs root privileges."
|
|
95
|
+
)
|
|
96
|
+
os.execlp("sudo", sys.executable, *sys.argv)
|
|
97
|
+
for cmd in [
|
|
98
|
+
["apt", "install", "-y", "-qq", "qemu-system"],
|
|
99
|
+
["dnf", "install", "-y", "-q", "@virtualization"],
|
|
100
|
+
["pacman", "-S", "--noconfirm", "--noprogressbar", "qemu"],
|
|
101
|
+
["zypper", "install", "-y", "-qq", "qemu"],
|
|
102
|
+
["yum", "install", "-y", "-q", "qemu-kvm"],
|
|
103
|
+
["emerge", "--quiet", "app-emulation/qemu"],
|
|
104
|
+
]:
|
|
105
|
+
if shutil.which(cmd[0]):
|
|
106
|
+
try:
|
|
107
|
+
await run_command(cmd, f"Installing QEMU with {cmd[0]}")
|
|
108
|
+
ready_to_start = True
|
|
109
|
+
break
|
|
110
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
111
|
+
console.warning(
|
|
112
|
+
"Failed to install QEMU automatically. Please install QEMU manually before using Agent Stack. Refer to https://www.qemu.org/download/ for instructions."
|
|
113
|
+
)
|
|
114
|
+
break
|
|
115
|
+
elif platform.system() == "Darwin":
|
|
116
|
+
ready_to_start = True
|
|
117
|
+
|
|
118
|
+
already_started = False
|
|
119
|
+
console.print()
|
|
120
|
+
if (
|
|
121
|
+
ready_to_start
|
|
122
|
+
and await inquirer.confirm( # pyright: ignore[reportPrivateImportUsage]
|
|
123
|
+
message="Do you want to start the Agent Stack platform now? Will run: agentstack platform start",
|
|
124
|
+
default=True,
|
|
125
|
+
).execute_async()
|
|
126
|
+
):
|
|
127
|
+
try:
|
|
128
|
+
await agentstack_cli.commands.platform.start(set_values_list=[], import_images=[], verbose=verbose)
|
|
129
|
+
already_started = True
|
|
130
|
+
console.print()
|
|
131
|
+
except Exception:
|
|
132
|
+
console.warning("Platform start failed. You can retry with [green]agentstack platform start[/green].")
|
|
133
|
+
|
|
134
|
+
already_configured = False
|
|
135
|
+
if (
|
|
136
|
+
already_started
|
|
137
|
+
and await inquirer.confirm( # pyright: ignore[reportPrivateImportUsage]
|
|
138
|
+
message="Do you want to configure your LLM provider now? Will run: agentstack model setup", default=True
|
|
139
|
+
).execute_async()
|
|
140
|
+
):
|
|
141
|
+
try:
|
|
142
|
+
await model_setup(verbose=verbose)
|
|
143
|
+
already_configured = True
|
|
144
|
+
except Exception:
|
|
145
|
+
console.warning("Model setup failed. You can retry with [green]agentstack model setup[/green].")
|
|
146
|
+
|
|
147
|
+
if (
|
|
148
|
+
already_configured
|
|
149
|
+
and await inquirer.confirm( # pyright: ignore[reportPrivateImportUsage]
|
|
150
|
+
message="Do you want to open the web UI now? Will run: agentstack ui", default=True
|
|
151
|
+
).execute_async()
|
|
152
|
+
):
|
|
153
|
+
import webbrowser
|
|
154
|
+
|
|
155
|
+
webbrowser.open("http://localhost:8334")
|
|
156
|
+
|
|
157
|
+
console.print()
|
|
158
|
+
console.success("Installation complete!")
|
|
159
|
+
if not shutil.which("agentstack", path=_path()):
|
|
160
|
+
console.hint("Open a new terminal window to use the [green]agentstack[/green] command.")
|
|
161
|
+
if not already_started:
|
|
162
|
+
console.hint("Start the Agent Stack platform using: [green]agentstack platform start[/green]")
|
|
163
|
+
if not already_configured:
|
|
164
|
+
console.hint("Configure your LLM provider using: [green]agentstack model setup[/green]")
|
|
165
|
+
console.hint(
|
|
166
|
+
"Use [green]agentstack ui[/green] to open the web GUI, or [green]agentstack run chat[/green] to talk to an agent on the command line."
|
|
167
|
+
)
|
|
168
|
+
console.hint(
|
|
169
|
+
"Run [green]agentstack --help[/green] to learn about available commands, or check the documentation at https://agentstack.beeai.dev/"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@app.command("upgrade")
|
|
174
|
+
async def upgrade(verbose: typing.Annotated[bool, typer.Option("-v", help="Show verbose output")] = False):
|
|
175
|
+
"""Upgrade Agent Stack CLI and Platform to the latest version."""
|
|
176
|
+
if not shutil.which("uv", path=_path()):
|
|
177
|
+
console.error("Can't self-upgrade because 'uv' was not found.")
|
|
178
|
+
raise typer.Exit(1)
|
|
179
|
+
|
|
180
|
+
with verbosity(verbose=verbose):
|
|
181
|
+
await run_command(
|
|
182
|
+
["uv", "tool", "install", "--force", "agentstack-cli"],
|
|
183
|
+
"Upgrading agentstack-cli",
|
|
184
|
+
env={"PATH": _path()},
|
|
185
|
+
)
|
|
186
|
+
await agentstack_cli.commands.platform.start(set_values_list=[], import_images=[], verbose=verbose)
|
|
187
|
+
await version(verbose=verbose)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@app.command("uninstall")
|
|
191
|
+
async def uninstall(
|
|
192
|
+
verbose: typing.Annotated[bool, typer.Option("-v", help="Show verbose output")] = False,
|
|
193
|
+
):
|
|
194
|
+
"""Uninstall Agent Stack CLI and Platform."""
|
|
195
|
+
if not shutil.which("uv", path=_path()):
|
|
196
|
+
console.error("Can't self-uninstall because 'uv' was not found.")
|
|
197
|
+
raise typer.Exit(1)
|
|
198
|
+
|
|
199
|
+
with verbosity(verbose=verbose):
|
|
200
|
+
await agentstack_cli.commands.platform.delete(verbose=verbose)
|
|
201
|
+
await run_command(
|
|
202
|
+
["uv", "tool", "uninstall", "agentstack-cli"],
|
|
203
|
+
"Uninstalling agentstack-cli",
|
|
204
|
+
env={"PATH": _path()},
|
|
205
|
+
)
|
|
206
|
+
console.success("Agent Stack uninstalled successfully.")
|