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.
@@ -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.")