Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 181 additions & 0 deletions scripts/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
#!/bin/bash

# Copyright 2026 Celesto AI
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# install.sh - One-command installer for SmolVM.
#
# Usage:
# curl -sSL https://celesto.ai/install.sh | bash
# curl -sSL https://celesto.ai/install.sh | bash -s -- --with-docker
#
# What it does:
# 1. Installs uv (Python package manager) if not present
# 2. Installs smolvm into an isolated tool environment via uv
# 3. Runs `smolvm setup` to configure the host
#
# After installation, the `smolvm` command is available globally.

set -euo pipefail

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

BOLD="\033[1m"
GREEN="\033[0;32m"
YELLOW="\033[0;33m"
RED="\033[0;31m"
RESET="\033[0m"

info() { printf "${BOLD}${GREEN}==>${RESET} ${BOLD}%s${RESET}\n" "$*"; }
warn() { printf "${BOLD}${YELLOW}warning:${RESET} %s\n" "$*"; }
error() { printf "${BOLD}${RED}error:${RESET} %s\n" "$*" >&2; }
die() { error "$@"; exit 1; }

# Collect extra flags to forward to `smolvm setup`
SETUP_ARGS=()
for arg in "$@"; do
SETUP_ARGS+=("$arg")
done

# ---------------------------------------------------------------------------
# Step 1 — Ensure uv is available
# ---------------------------------------------------------------------------

find_uv() {
# 1. Already on PATH
if command -v uv >/dev/null 2>&1; then
return 0
fi
# 2. Common install locations (not yet on PATH in this session)
for candidate in "$HOME/.local/bin/uv" "$HOME/.cargo/bin/uv"; do
if [ -x "$candidate" ]; then
export PATH="$(dirname "$candidate"):$PATH"
return 0
fi
done
return 1
}

ensure_uv() {
if find_uv; then
info "uv is already installed ($(uv --version))"
return
fi

info "Installing uv …"
curl -LsSf https://astral.sh/uv/install.sh | sh

# The installer puts uv in ~/.local/bin (or ~/.cargo/bin on older versions)
if ! find_uv; then
die "uv installation failed. Please install it manually: https://docs.astral.sh/uv/getting-started/installation/"
fi

info "uv installed ($(uv --version))"
}

# ---------------------------------------------------------------------------
# Step 2 — Install smolvm
# ---------------------------------------------------------------------------

install_smolvm() {
if uv tool list 2>/dev/null | grep -q '^smolvm '; then
# Installed as a uv tool — upgrade in place
info "smolvm is already installed (uv tool), upgrading …"
uv tool upgrade smolvm
else
# Fresh install (or installed via pip/editable — uv tool install won't conflict)
info "Installing smolvm …"
uv tool install smolvm
fi

# uv tool bin dir may not be on PATH yet in this session
local tool_bin
tool_bin="$(uv tool dir 2>/dev/null)/../bin"
if [ -d "$tool_bin" ]; then
export PATH="$tool_bin:$PATH"
fi
export PATH="$HOME/.local/bin:$PATH"

if ! command -v smolvm >/dev/null 2>&1; then
die "smolvm installation failed — 'smolvm' command not found on PATH."
fi

info "$(smolvm --version)"
}

# ---------------------------------------------------------------------------
# Step 3 — Run smolvm setup
# ---------------------------------------------------------------------------

run_setup() {
info "Running smolvm setup …"
smolvm setup ${SETUP_ARGS[@]+"${SETUP_ARGS[@]}"}
}

# ---------------------------------------------------------------------------
# Step 4 — Shell PATH reminder
# ---------------------------------------------------------------------------

shell_hint() {
# Check if ~/.local/bin is already on the user's default PATH
local shell_name
shell_name="$(basename "${SHELL:-/bin/sh}")"
local rc_file=""
case "$shell_name" in
zsh) rc_file="$HOME/.zshrc" ;;
bash) rc_file="$HOME/.bashrc" ;;
fish) rc_file="$HOME/.config/fish/config.fish" ;;
esac

if [ -n "$rc_file" ] && [ -f "$rc_file" ]; then
if ! grep -q '.local/bin' "$rc_file" 2>/dev/null; then
warn "Add ~/.local/bin to your PATH so 'smolvm' is available in new shells:"
printf "\n %s\n\n" "echo 'export PATH=\"\$HOME/.local/bin:\$PATH\"' >> $rc_file"
fi
fi
}

# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

main() {
printf "\n"
printf "${GREEN}"
cat <<'BANNER'
___ _ _ _ ___
/ __|___ | |___ __| |_ ___ /_\ |_ _|
| (__/ -_)| / -_|_-< _/ _ \ / _ \ | |
\___\___||_\___/__/\__\___//_/ \_\___|
BANNER
printf "${RESET}"
printf " ${BOLD}SmolVM Installer${RESET}\n"
printf " One command to give AI agents their own computer.\n\n"

ensure_uv
install_smolvm
run_setup
shell_hint

printf "\n"
info "Verifying installation …"
smolvm doctor
printf "\n"
info "Done! SmolVM is ready to use."
printf "\n"
}

main
6 changes: 6 additions & 0 deletions src/smolvm/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,12 @@ def build_parser() -> argparse.ArgumentParser:
"output for LLMs, agents, and automation."
),
)
parser.add_argument(
"-V",
"--version",
action="version",
version=f"%(prog)s {importlib.metadata.version('smolvm')}",
)
subparsers = parser.add_subparsers(dest="command")

cleanup = subparsers.add_parser(
Expand Down
4 changes: 2 additions & 2 deletions src/smolvm/facade.py
Original file line number Diff line number Diff line change
Expand Up @@ -806,7 +806,7 @@ def run(

if self._info.status != VMState.RUNNING:
raise SmolVMError(
f"Cannot run command: VM is {self._info.status.value}",
f"VM is not running. Start the VM using vm.start() before running commands (current state: {self._info.status.value})",
{"vm_id": self._vm_id},
)
if not self.can_run_commands():
Expand Down Expand Up @@ -898,7 +898,7 @@ def wait_for_ssh(self, timeout: float = 60.0) -> SmolVM:

if self._info.status != VMState.RUNNING:
raise SmolVMError(
f"Cannot wait for SSH: VM is {self._info.status.value}",
f"VM is not running. Start the VM using vm.start() before waiting for SSH (current state: {self._info.status.value})",
{"vm_id": self._vm_id},
)
if self._info.network is None:
Expand Down
4 changes: 2 additions & 2 deletions tests/test_facade.py
Original file line number Diff line number Diff line change
Expand Up @@ -1127,7 +1127,7 @@ def test_run_on_stopped_vm_raises(
mock_sdk_cls.return_value = mock_sdk

vm = SmolVM(sample_config)
with pytest.raises(SmolVMError, match="VM is stopped"):
with pytest.raises(SmolVMError, match="VM is not running"):
vm.run("echo test")


Expand Down Expand Up @@ -1222,7 +1222,7 @@ def test_expose_local_requires_running_vm(
mock_sdk_cls.return_value = mock_sdk

vm = SmolVM(sample_config)
with pytest.raises(SmolVMError, match="VM is stopped"):
with pytest.raises(SmolVMError, match="VM is not running"):
vm.expose_local(guest_port=8080, host_port=18080)

@patch("smolvm.facade.SmolVMManager")
Expand Down
Loading