Add cold-start CLI installer (#313)

This commit is contained in:
Avanika Narayan
2026-05-05 14:42:48 -07:00
committed by GitHub
parent a3ba63d148
commit e79bc1e196
49 changed files with 2539 additions and 520 deletions
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# bg-orchestrator.sh — detached parent of all background install work.
#
# Spawned by install.sh via `nohup ... & disown`. Runs the Rust
# toolchain install + extension build sequentially, and in parallel
# kicks off model pulls for each model id passed as an argument.
#
# Usage: bg-orchestrator.sh <model-id> [<model-id> ...]
set -euo pipefail
OPENJARVIS_HOME="${OPENJARVIS_HOME:-$HOME/.openjarvis}"
STATE_DIR="$OPENJARVIS_HOME/.state"
SCRIPTS_DIR="$OPENJARVIS_HOME/.scripts"
PID_FILE="$STATE_DIR/bg.pid"
LOG="$STATE_DIR/bg-orchestrator.log"
mkdir -p "$STATE_DIR"
echo "$$" > "$PID_FILE"
cleanup() {
rm -f "$PID_FILE"
}
trap cleanup EXIT
echo "[$(date -u +%FT%TZ)] bg-orchestrator started, pid=$$" >> "$LOG"
# Sequential: install rust, then build extension.
(
"$SCRIPTS_DIR/install-rust.sh" >> "$LOG" 2>&1 \
&& "$SCRIPTS_DIR/build-extension.sh" >> "$LOG" 2>&1
) &
RUST_PID=$!
# Parallel: each model pull.
MODEL_PIDS=()
for model in "$@"; do
"$SCRIPTS_DIR/pull-model.sh" "$model" >> "$LOG" 2>&1 &
MODEL_PIDS+=($!)
done
# Wait for all subprocesses; non-zero exits don't fail the orchestrator
# because per-task state files already record success/failure.
wait "$RUST_PID" || true
for pid in "${MODEL_PIDS[@]}"; do
wait "$pid" || true
done
echo "[$(date -u +%FT%TZ)] bg-orchestrator done" >> "$LOG"
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
# build-extension.sh — build the Rust maturin extension into the venv.
#
# State files under $OPENJARVIS_HOME/.state/:
# extension-built — atomic marker written on success
# extension-failed — written on failure with stderr tail
# extension-build.log — captured stderr/stdout
set -euo pipefail
OPENJARVIS_HOME="${OPENJARVIS_HOME:-$HOME/.openjarvis}"
# Self-heal PATH for cargo: install-rust.sh installs to ~/.cargo/bin, but
# its export doesn't propagate to subsequent subprocess invocations.
export PATH="$HOME/.cargo/bin:$PATH"
SRC_DIR="$OPENJARVIS_HOME/src"
STATE_DIR="$OPENJARVIS_HOME/.state"
LOG="$STATE_DIR/extension-build.log"
BUILT="$STATE_DIR/extension-built"
FAILED="$STATE_DIR/extension-failed"
MANIFEST="$SRC_DIR/rust/crates/openjarvis-python/Cargo.toml"
mkdir -p "$STATE_DIR"
if [[ ! -f "$MANIFEST" ]]; then
echo "build-extension.sh: manifest not found at $MANIFEST" > "$FAILED"
exit 1
fi
cd "$SRC_DIR"
if uv run maturin develop -m "$MANIFEST" >>"$LOG" 2>&1; then
tmp="$BUILT.tmp"
date -u +"%Y-%m-%dT%H:%M:%SZ" > "$tmp"
mv "$tmp" "$BUILT"
rm -f "$FAILED"
exit 0
else
rc=$?
{
echo "build-extension.sh failed (exit=$rc)"
tail -n 50 "$LOG" 2>/dev/null || true
} > "$FAILED"
rm -f "$BUILT"
exit "$rc"
fi
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# install-rust.sh — install Rust toolchain via rustup if cargo not present.
#
# Idempotent: exits 0 immediately if cargo is on PATH.
set -euo pipefail
if command -v cargo >/dev/null 2>&1; then
echo "install-rust.sh: cargo already present, skipping"
exit 0
fi
echo "install-rust.sh: installing Rust toolchain via rustup..."
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
if [[ -d "$HOME/.cargo/bin" ]]; then
export PATH="$HOME/.cargo/bin:$PATH"
fi
if ! command -v cargo >/dev/null 2>&1; then
echo "install-rust.sh: cargo still not on PATH after install; check rustup output" >&2
exit 1
fi
+264
View File
@@ -0,0 +1,264 @@
#!/usr/bin/env bash
# install.sh — OpenJarvis curl-pipe-bash installer.
#
# Usage:
# curl -fsSL https://openjarvis.ai/install.sh | bash
#
# Flags (only used in tests / power users):
# --no-bg-orchestrator Skip the detached background orchestrator
# --minimal Skip foreground model pull (no `qwen3.5:2b`)
# --force Re-run all steps even if state file says done
#
# Environment overrides:
# OPENJARVIS_HOME Install dir (default: $HOME/.openjarvis)
# OPENJARVIS_REPO_URL git repo URL (default: https://github.com/open-jarvis/OpenJarvis.git)
# OPENJARVIS_FORCE_WSL Set 1 to force WSL detection (testing)
set -euo pipefail
# ---- args ----
SKIP_BG=0
MINIMAL=0
FORCE=0
for arg in "$@"; do
case "$arg" in
--no-bg-orchestrator) SKIP_BG=1 ;;
--minimal) MINIMAL=1 ;;
--force) FORCE=1 ;;
*) echo "install.sh: unknown arg: $arg" >&2; exit 2 ;;
esac
done
# ---- root refusal ----
if [[ "$(id -u)" -eq 0 ]]; then
cat >&2 <<'EOF'
install.sh: don't run as root.
OpenJarvis installs to $HOME/.openjarvis, not /usr/local. Re-run as your
regular user (without sudo).
EOF
exit 1
fi
# ---- prereq probe ----
need() {
if ! command -v "$1" >/dev/null 2>&1; then
cat >&2 <<EOF
install.sh: '$1' is required but not found.
Install hints:
macOS: xcode-select --install (provides git, curl)
Debian/Ubuntu: sudo apt install git curl
Fedora/RHEL: sudo dnf install git curl
Arch: sudo pacman -S git curl
EOF
exit 1
fi
}
need git
need curl
# ---- env ----
OPENJARVIS_HOME="${OPENJARVIS_HOME:-$HOME/.openjarvis}"
OPENJARVIS_REPO_URL="${OPENJARVIS_REPO_URL:-https://github.com/open-jarvis/OpenJarvis.git}"
SRC_DIR="$OPENJARVIS_HOME/src"
VENV_DIR="$OPENJARVIS_HOME/.venv"
STATE_DIR="$OPENJARVIS_HOME/.state"
SCRIPTS_DIR="$OPENJARVIS_HOME/.scripts"
STATE_FILE="$STATE_DIR/install-state.json"
mkdir -p "$OPENJARVIS_HOME" "$STATE_DIR" "$SCRIPTS_DIR"
# ---- WSL detection ----
WSL=0
if [[ "${OPENJARVIS_FORCE_WSL:-0}" == "1" ]]; then
WSL=1
elif [[ -f /proc/sys/kernel/osrelease ]] && grep -qi "microsoft" /proc/sys/kernel/osrelease 2>/dev/null; then
WSL=1
fi
# ---- helpers ----
state_done() {
[[ -f "$STATE_FILE" ]] && grep -q "\"$1\":[[:space:]]*true" "$STATE_FILE"
}
mark_done() {
if [[ ! -f "$STATE_FILE" ]]; then
echo '{}' > "$STATE_FILE"
fi
python3 - "$STATE_FILE" "$1" "$WSL" <<'PYEOF'
import json, sys
path, key, wsl = sys.argv[1], sys.argv[2], sys.argv[3]
with open(path) as f:
data = json.load(f)
data[key] = True
data["wsl"] = bool(int(wsl))
with open(path, "w") as f:
json.dump(data, f, indent=2)
PYEOF
}
step() {
local name="$1" desc="$2"; shift 2
if [[ "$FORCE" -ne 1 ]] && state_done "$name"; then
echo "[ok] $desc (already done)"
return 0
fi
echo "[..] $desc"
"$@"
mark_done "$name"
echo "[ok] $desc"
}
# ---- step impls ----
install_uv() {
if command -v uv >/dev/null 2>&1; then
echo " uv already installed"
return 0
fi
curl -LsSf https://astral.sh/uv/install.sh | sh
if ! command -v uv >/dev/null 2>&1; then
export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"
fi
}
clone_repo() {
if [[ "$FORCE" -ne 1 ]] && [[ -d "$SRC_DIR/.git" ]]; then
echo " repo already at $SRC_DIR"
return 0
fi
git clone --depth 1 "$OPENJARVIS_REPO_URL" "$SRC_DIR"
}
copy_scripts() {
cp -f "$SRC_DIR"/scripts/install/*.sh "$SCRIPTS_DIR/"
chmod +x "$SCRIPTS_DIR"/*.sh
}
create_venv() {
uv venv --python 3.11 "$VENV_DIR"
}
editable_install() {
cd "$SRC_DIR"
uv pip install --python "$VENV_DIR/bin/python" -e .
}
install_ollama() {
if command -v ollama >/dev/null 2>&1; then
echo " ollama already installed"
return 0
fi
curl -fsSL https://ollama.com/install.sh | sh
}
start_ollama() {
if pgrep -f "ollama serve" >/dev/null 2>&1; then
echo " ollama serve already running"
return 0
fi
if [[ "$WSL" -eq 1 ]] || ! command -v systemctl >/dev/null 2>&1; then
nohup ollama serve > "$STATE_DIR/ollama.log" 2>&1 &
sleep 1
else
systemctl --user start ollama 2>/dev/null \
|| (nohup ollama serve > "$STATE_DIR/ollama.log" 2>&1 & sleep 1)
fi
}
pull_default_model() {
if [[ "$MINIMAL" -eq 1 ]]; then
echo " --minimal set; skipping model pull"
return 0
fi
ollama pull qwen3.5:2b || echo " warning: ollama pull failed; bg-orchestrator will retry"
}
write_config() {
"$VENV_DIR/bin/jarvis" _bootstrap --write-config \
--engine ollama --model qwen3.5:2b \
--prefer-cloud-when-available
}
install_symlinks() {
mkdir -p "$HOME/.local/bin"
ln -sf "$SCRIPTS_DIR/jarvis-wrapper.sh" "$HOME/.local/bin/jarvis"
ln -sf "$SCRIPTS_DIR/jarvis-uninstall.sh" "$HOME/.local/bin/jarvis-uninstall"
}
ensure_path() {
case ":$PATH:" in
*":$HOME/.local/bin:"*) return 0 ;;
esac
local rc=""
if [[ "$SHELL" == */zsh ]]; then
rc="$HOME/.zshrc"
else
rc="$HOME/.bashrc"
fi
if grep -q "OpenJarvis" "$rc" 2>/dev/null; then
return 0
fi
{
echo ''
echo '# OpenJarvis'
echo 'export PATH="$HOME/.local/bin:$PATH"'
} >> "$rc"
echo " Added ~/.local/bin to PATH in $rc — run: source $rc"
}
detach_bg_orchestrator() {
if [[ "$SKIP_BG" -eq 1 ]]; then
echo " --no-bg-orchestrator set; skipping detach"
return 0
fi
local models
models=$("$VENV_DIR/bin/python" - <<'PYEOF' 2>/dev/null || true
from openjarvis.core.config import detect_hardware, recommend_model
hw = detect_hardware()
tier = recommend_model(hw, "ollama")
TIERS = ["qwen3.5:2b", "qwen3.5:4b", "qwen3.5:9b", "qwen3.5:27b"]
out = set([tier]) if tier else set()
if tier in TIERS:
idx = TIERS.index(tier)
if idx + 1 < len(TIERS):
out.add(TIERS[idx + 1])
print(" ".join(sorted(out)))
PYEOF
)
if [[ -z "$models" ]]; then
models=""
fi
nohup "$SCRIPTS_DIR/bg-orchestrator.sh" $models \
> "$STATE_DIR/bg-orchestrator.log" 2>&1 &
disown
}
# ---- run ----
echo "OpenJarvis installer"
echo " install dir: $OPENJARVIS_HOME"
echo " WSL2: $WSL"
echo
step install_uv "Install uv" install_uv
step clone_repo "Clone OpenJarvis repo" clone_repo
step copy_scripts "Copy install scripts" copy_scripts
step create_venv "Create venv" create_venv
step editable_install "Install OpenJarvis" editable_install
step install_ollama "Install Ollama" install_ollama
step start_ollama "Start Ollama daemon" start_ollama
step pull_default_model "Pull qwen3.5:2b" pull_default_model
step write_config "Write config.toml" write_config
step install_symlinks "Install symlinks" install_symlinks
step ensure_path "Ensure PATH" ensure_path
step detach_bg_orchestrator "Detach background work" detach_bg_orchestrator
cat <<EOF
Done. Type 'jarvis' to start chatting.
Background work continues silently:
- Rust toolchain + maturin extension build
- Bigger model downloads
Run 'jarvis doctor' to check status anytime.
EOF
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
# jarvis-uninstall.sh — clean removal of OpenJarvis from $HOME.
#
# Removes:
# ~/.openjarvis/
# ~/.local/bin/jarvis
# ~/.local/bin/jarvis-uninstall
#
# Does NOT remove: ollama, uv, or the Rust toolchain.
set -euo pipefail
OPENJARVIS_HOME="${OPENJARVIS_HOME:-$HOME/.openjarvis}"
if [[ -f "$OPENJARVIS_HOME/.state/bg.pid" ]]; then
pid=$(cat "$OPENJARVIS_HOME/.state/bg.pid" 2>/dev/null || echo "")
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
echo "Stopping background work (pid=$pid)..."
kill "$pid" 2>/dev/null || true
fi
fi
if command -v ollama >/dev/null 2>&1; then
ollama stop >/dev/null 2>&1 || true
fi
if [[ -d "$OPENJARVIS_HOME" ]]; then
rm -rf "$OPENJARVIS_HOME"
echo "Removed $OPENJARVIS_HOME"
fi
for f in "$HOME/.local/bin/jarvis" "$HOME/.local/bin/jarvis-uninstall"; do
if [[ -L "$f" ]] || [[ -f "$f" ]]; then
rm -f "$f"
echo "Removed $f"
fi
done
cat <<EOF
OpenJarvis removed.
Left intact (may be used by other tools):
- Ollama (uninstall: brew uninstall ollama / rm -f /usr/local/bin/ollama)
- uv (uninstall: rm -rf ~/.local/share/uv ~/.cargo/bin/uv)
- Rust toolchain (uninstall: rustup self uninstall)
EOF
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# jarvis-wrapper.sh — symlinked to ~/.local/bin/jarvis.
# Activates the managed venv and execs the real jarvis CLI.
OPENJARVIS_HOME="${OPENJARVIS_HOME:-$HOME/.openjarvis}"
VENV="$OPENJARVIS_HOME/.venv"
if [[ ! -d "$VENV" ]]; then
echo "jarvis: venv not found at $VENV" >&2
echo "Re-run the installer: curl -fsSL https://openjarvis.ai/install.sh | bash" >&2
exit 1
fi
exec "$VENV/bin/jarvis" "$@"
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
# pull-model.sh — pull an Ollama model with state-file lifecycle.
#
# Usage: pull-model.sh <model-id>
#
# State files written under $OPENJARVIS_HOME/.state/models/:
# <model>.downloading — created on start
# <model>.ready — on success (atomic rename)
# <model>.failed — on failure after retries
# <model>.log — captured stderr
set -euo pipefail
MODEL="${1:-}"
if [[ -z "$MODEL" ]]; then
echo "usage: pull-model.sh <model-id>" >&2
exit 2
fi
OPENJARVIS_HOME="${OPENJARVIS_HOME:-$HOME/.openjarvis}"
STATE_DIR="$OPENJARVIS_HOME/.state/models"
mkdir -p "$STATE_DIR"
DOWNLOADING="$STATE_DIR/${MODEL}.downloading"
READY="$STATE_DIR/${MODEL}.ready"
FAILED="$STATE_DIR/${MODEL}.failed"
LOG="$STATE_DIR/${MODEL}.log"
# Cleanup any prior state for this model.
rm -f "$READY" "$FAILED"
touch "$DOWNLOADING"
MAX_RETRIES=3
attempt=0
last_exit=0
while [[ $attempt -lt $MAX_RETRIES ]]; do
attempt=$((attempt + 1))
if ollama pull "$MODEL" >>"$LOG" 2>&1; then
# Atomic rename: write to .tmp then mv.
tmp="$STATE_DIR/${MODEL}.ready.tmp"
date -u +"%Y-%m-%dT%H:%M:%SZ" > "$tmp"
mv "$tmp" "$READY"
rm -f "$DOWNLOADING"
exit 0
else
last_exit=$?
fi
done
# All retries exhausted.
{
echo "pull-model.sh: $MODEL failed after $MAX_RETRIES attempts (exit=$last_exit)"
tail -n 50 "$LOG" 2>/dev/null || true
} > "$FAILED"
rm -f "$DOWNLOADING"
exit "$last_exit"