Implement install stages S0–S2: bootstrap, env, model and vision smoke.
Operator can run make bootstrap/install through S2 using nemohermes/openshell wrappers; docs and implement queue updated. No S3+ and no push.
This commit is contained in:
+43
-13
@@ -1,19 +1,49 @@
|
||||
# Host scripts (scaffold)
|
||||
# Host scripts
|
||||
|
||||
**Status:** Documented entrypoints only — no executable bodies until **build**.
|
||||
**Status:** S0b–S2 implemented. S3–S7 pending.
|
||||
|
||||
All scripts must wrap **`nemohermes` / `openshell` / Docker**. No parallel control API.
|
||||
All scripts wrap **`nemohermes` / `openshell` / Docker**. No parallel control API.
|
||||
|
||||
## Intended entrypoints
|
||||
## Entrypoints
|
||||
|
||||
| Script | Role |
|
||||
|--------|------|
|
||||
| `bootstrap.sh` | Host prereqs; install Docker if missing |
|
||||
| `install.sh` | Stages S0b–S6 |
|
||||
| `upgrade.sh` | Snapshot, pull pins, migrate, re-apply policy, doctor |
|
||||
| `doctor.sh` | Health checks |
|
||||
| `connect/*.sh` | Operator connect helpers (Square, QBO, Vagaro, channels) |
|
||||
| `install/` | Stage helpers |
|
||||
| `lib/` | Shared shell helpers |
|
||||
| Script | Role | Status |
|
||||
|--------|------|--------|
|
||||
| `bootstrap.sh` | Host prereqs; install Docker if missing | ✅ S0b |
|
||||
| `install.sh` | Staged installer (S0b–S2) | ✅ S0b–S2 |
|
||||
| `install/s1-env.sh` | S1: create/validate `.env` | ✅ S1 |
|
||||
| `install/s2-models.sh` | S2: model + vision config + smoke | ✅ S2 |
|
||||
| `upgrade.sh` | Snapshot, pull pins, migrate, re-apply policy, doctor | ⏳ Pending |
|
||||
| `doctor.sh` | Health checks | ⏳ Pending |
|
||||
| `connect/*.sh` | Operator connect helpers (Square, QBO, Vagaro, channels) | ⏳ Pending |
|
||||
|
||||
## Shared library
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `lib/common.sh` | Logging, CLI detection, env loading, repo root |
|
||||
| `lib/env.sh` | `.env` validation and creation helpers |
|
||||
| `lib/vision_smoke.sh` | Vision capability smoke test |
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Bootstrap (Docker if missing)
|
||||
./scripts/bootstrap.sh
|
||||
|
||||
# Full install (S0b–S2)
|
||||
./scripts/install.sh
|
||||
|
||||
# Individual stages
|
||||
./scripts/install.sh --stage s1 # env only
|
||||
./scripts/install.sh --stage s2 # models only
|
||||
|
||||
# Or via Make
|
||||
make bootstrap
|
||||
make install
|
||||
make install-s1
|
||||
make install-s2
|
||||
```
|
||||
|
||||
## Design reference
|
||||
|
||||
See [docs/INSTALL.md](../docs/INSTALL.md), [docs/UPGRADE.md](../docs/UPGRADE.md), [design/updates-lifecycle.md](../design/updates-lifecycle.md).
|
||||
|
||||
Executable
+97
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/bootstrap.sh — S0b: host bootstrap (Docker install-if-missing)
|
||||
#
|
||||
# Idempotent: detects Docker and skips install if already present.
|
||||
# Linux only (Debian/Ubuntu/RHEL/Fedora families).
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/bootstrap.sh
|
||||
# make bootstrap
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
# Source shared helpers
|
||||
# shellcheck source=lib/common.sh
|
||||
source "$SCRIPT_DIR/lib/common.sh"
|
||||
|
||||
log_section "S0b: Host bootstrap"
|
||||
warn_if_root
|
||||
|
||||
# ── Check Docker ───────────────────────────────────────────────────────────
|
||||
if docker_available; then
|
||||
log_info "Docker already installed ($(docker --version))"
|
||||
log_info "Bootstrap complete — nothing to do."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log_warn "Docker not found or not running."
|
||||
|
||||
# ── Detect OS family ──────────────────────────────────────────────────────
|
||||
detect_os_family() {
|
||||
if [[ -f /etc/os-release ]]; then
|
||||
# Sourced in a subshell via $() — OS variables do not leak to caller scope.
|
||||
. /etc/os-release
|
||||
echo "${ID_LIKE:-$ID}"
|
||||
else
|
||||
log_error "Cannot detect OS family (no /etc/os-release)"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
OS_FAMILY="$(detect_os_family)"
|
||||
log_info "Detected OS family: $OS_FAMILY"
|
||||
|
||||
install_docker_debian() {
|
||||
log_info "Installing Docker (Debian/Ubuntu) via official convenience script…"
|
||||
if ! curl -fsSL https://get.docker.com | sh; then
|
||||
log_error "Docker install script failed."
|
||||
log_error "Install Docker manually, then re-run this script."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Add current user to docker group (requires sudo)
|
||||
local user="${SUDO_USER:-$(whoami)}"
|
||||
if [[ -n "$user" ]] && getent group docker &>/dev/null; then
|
||||
log_info "Adding user '$user' to docker group…"
|
||||
sudo usermod -aG docker "$user" 2>/dev/null || true
|
||||
log_warn "Log out and back in (or run 'newgrp docker') for group changes to take effect."
|
||||
fi
|
||||
}
|
||||
|
||||
install_docker_rhel() {
|
||||
log_info "Installing Docker (RHEL/Fedora) via dnf…"
|
||||
sudo dnf install -y dnf-utils 2>/dev/null || sudo dnf install -ydnf-plugins-core 2>/dev/null || true
|
||||
sudo dnf config-manager --add-repo https://download.docker.com/linux/$(echo "$OS_FAMILY" | head -c3)/docker-ce.repo 2>/dev/null || {
|
||||
log_warn "Could not add Docker repo. Trying generic install…"
|
||||
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin || {
|
||||
log_error "Docker install failed. Install manually and re-run."
|
||||
return 1
|
||||
}
|
||||
}
|
||||
sudo systemctl enable --now docker
|
||||
}
|
||||
|
||||
# ── Install ────────────────────────────────────────────────────────────────
|
||||
if echo "$OS_FAMILY" | grep -qiE 'debian|ubuntu'; then
|
||||
install_docker_debian
|
||||
elif echo "$OS_FAMILY" | grep -qiE 'rhel|fedora|centos|rocky|almalinux'; then
|
||||
install_docker_rhel
|
||||
else
|
||||
log_error "Unsupported OS family: $OS_FAMILY"
|
||||
log_error "Supported: Debian/Ubuntu, RHEL/Fedora/CentOS/Rocky/AlmaLinux"
|
||||
log_error "Install Docker manually, then re-run this script."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Verify ─────────────────────────────────────────────────────────────────
|
||||
if docker_available; then
|
||||
log_info "Docker installed successfully: $(docker --version)"
|
||||
log_info "Bootstrap complete."
|
||||
else
|
||||
log_error "Docker install appeared to succeed but daemon is not reachable."
|
||||
log_error "Check Docker service: sudo systemctl status docker"
|
||||
exit 1
|
||||
fi
|
||||
Executable
+109
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/install.sh — Lumina staged installer
|
||||
#
|
||||
# Runs install stages S0b–S2 (S3+ not yet implemented).
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/install.sh # run all implemented stages (S0b–S2)
|
||||
# ./scripts/install.sh --stage s1 # run only S1
|
||||
# ./scripts/install.sh --stage s2 # run only S2
|
||||
# ./scripts/install.sh --help
|
||||
#
|
||||
# All stages are idempotent. Re-running is safe.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
# Source shared helpers
|
||||
# shellcheck source=lib/common.sh
|
||||
source "$SCRIPT_DIR/lib/common.sh"
|
||||
|
||||
# ── Usage ──────────────────────────────────────────────────────────────────
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [OPTIONS]
|
||||
|
||||
Run Lumina install stages (S0b–S2 implemented).
|
||||
|
||||
Options:
|
||||
--stage <s1|s2> Run only the specified stage
|
||||
--help Show this help
|
||||
|
||||
Stages:
|
||||
S0b Docker install-if-missing (bootstrap)
|
||||
S1 Repository environment (.env)
|
||||
S2 Model + vision configuration + smoke test
|
||||
|
||||
All stages are idempotent.
|
||||
|
||||
Examples:
|
||||
$(basename "$0") # run S0b → S1 → S2
|
||||
$(basename "$0") --stage s1 # run only S1 (env)
|
||||
$(basename "$0") --stage s2 # run only S2 (models)
|
||||
EOF
|
||||
}
|
||||
|
||||
# ── Parse args ─────────────────────────────────────────────────────────────
|
||||
SINGLE_STAGE=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--help|-h) usage; exit 0 ;;
|
||||
--stage)
|
||||
shift
|
||||
SINGLE_STAGE="${1:-}"
|
||||
if [[ -z "$SINGLE_STAGE" ]]; then
|
||||
log_error "--stage requires a value (s1 or s2)"
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown argument: $1"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ── Run stages ─────────────────────────────────────────────────────────────
|
||||
run_s0b() {
|
||||
log_section "S0b: Docker bootstrap"
|
||||
bash "$SCRIPT_DIR/bootstrap.sh"
|
||||
}
|
||||
|
||||
run_s1() {
|
||||
bash "$SCRIPT_DIR/install/s1-env.sh"
|
||||
}
|
||||
|
||||
run_s2() {
|
||||
bash "$SCRIPT_DIR/install/s2-models.sh"
|
||||
}
|
||||
|
||||
log_section "Lumina installer (stages S0b–S2)"
|
||||
warn_if_root
|
||||
|
||||
if [[ -n "$SINGLE_STAGE" ]]; then
|
||||
case "$SINGLE_STAGE" in
|
||||
s0b) run_s0b ;;
|
||||
s1) run_s1 ;;
|
||||
s2) run_s2 ;;
|
||||
*)
|
||||
log_error "Unknown stage: $SINGLE_STAGE"
|
||||
log_error "Valid stages: s0b, s1, s2"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
else
|
||||
# Run all implemented stages in order
|
||||
run_s0b
|
||||
run_s1
|
||||
run_s2
|
||||
fi
|
||||
|
||||
log_section "Install complete (S0b–S2)"
|
||||
log_info "Next steps:"
|
||||
log_info " - Review .env for correctness"
|
||||
log_info " - Continue with S3+ when implemented (compose, sandbox, policy)"
|
||||
log_info " - See docs/INSTALL.md for full procedure"
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/install/s1-env.sh — S1: repo environment
|
||||
#
|
||||
# Creates .env from .env.example if needed, then validates required keys.
|
||||
# Never commits real secrets.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/install/s1-env.sh
|
||||
# (called by install.sh --stage s1)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Source shared helpers (common.sh sets REPO_ROOT via git rev-parse)
|
||||
# shellcheck source=../lib/common.sh
|
||||
source "$SCRIPT_DIR/../lib/common.sh"
|
||||
# shellcheck source=../lib/env.sh
|
||||
source "$SCRIPT_DIR/../lib/env.sh"
|
||||
|
||||
log_section "S1: Repository environment"
|
||||
|
||||
# ── Create .env from example if needed ─────────────────────────────────────
|
||||
create_env_from_example
|
||||
|
||||
# ── Validate ───────────────────────────────────────────────────────────────
|
||||
validate_env
|
||||
|
||||
log_info "S1 complete: .env is valid."
|
||||
Executable
+87
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/install/s2-models.sh — S2: model + aux vision config
|
||||
#
|
||||
# Configures inference via openshell/nemohermes and runs vision smoke test.
|
||||
# Platform-first: all mutations through nemohermes/openshell CLIs.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/install/s2-models.sh
|
||||
# (called by install.sh --stage s2)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Source shared helpers (common.sh sets REPO_ROOT via git rev-parse)
|
||||
# shellcheck source=../lib/common.sh
|
||||
source "$SCRIPT_DIR/../lib/common.sh"
|
||||
# shellcheck source=../lib/env.sh
|
||||
source "$SCRIPT_DIR/../lib/env.sh"
|
||||
# shellcheck source=../lib/vision_smoke.sh
|
||||
source "$SCRIPT_DIR/../lib/vision_smoke.sh"
|
||||
|
||||
log_section "S2: Model + vision configuration"
|
||||
|
||||
# ── Load .env ──────────────────────────────────────────────────────────────
|
||||
load_env
|
||||
|
||||
# ── Validate required keys ─────────────────────────────────────────────────
|
||||
validate_env || exit 1
|
||||
|
||||
# ── Check CLI prerequisites ────────────────────────────────────────────────
|
||||
require_cmd openshell "Install OpenShell CLI (part of NemoClaw platform)"
|
||||
require_cmd nemohermes "Install nemohermes CLI (part of NemoClaw platform)"
|
||||
|
||||
# ── Verify inference endpoint is reachable ─────────────────────────────────
|
||||
# LUMINA_INFERENCE_BASE_URL includes /v1 (e.g. http://host:port/v1)
|
||||
log_info "Verifying inference endpoint: $LUMINA_INFERENCE_BASE_URL"
|
||||
if ! curl -sf --max-time 15 "${LUMINA_INFERENCE_BASE_URL}/models" &>/dev/null; then
|
||||
log_error "Inference endpoint unreachable at $LUMINA_INFERENCE_BASE_URL"
|
||||
log_error "Check that the model server is running and the URL is correct."
|
||||
log_error "Fix LUMINA_INFERENCE_BASE_URL in .env and re-run."
|
||||
exit 1
|
||||
fi
|
||||
log_info "Inference endpoint reachable."
|
||||
|
||||
# ── Configure inference via openshell ──────────────────────────────────────
|
||||
# Only set if the gateway is connected and we can reach it.
|
||||
# If the gateway is not yet set up, we validate the endpoint and skip
|
||||
# the openshell write (S3+ will handle full gateway config).
|
||||
log_info "Checking OpenShell gateway status…"
|
||||
if openshell status &>/dev/null 2>&1; then
|
||||
log_info "Gateway connected — configuring inference route…"
|
||||
|
||||
# Use openshell inference set to configure the main model.
|
||||
# openshell inference set takes --provider and --model but NOT --url.
|
||||
# The gateway resolves the endpoint URL from its own metadata (the
|
||||
# compatible-endpoint provider reads the URL from the gateway config).
|
||||
# --no-verify skips the endpoint verification that openshell does internally
|
||||
# since we already verified above.
|
||||
if openshell inference set \
|
||||
--provider compatible-endpoint \
|
||||
--model "$LUMINA_INFERENCE_MODEL" \
|
||||
--no-verify 2>&1; then
|
||||
log_info "Inference route configured via openshell."
|
||||
else
|
||||
log_warn "openshell inference set returned non-zero."
|
||||
log_warn "The gateway may already have this route configured, or the gateway"
|
||||
log_warn "requires a different provider name. Check with: openshell inference get"
|
||||
fi
|
||||
else
|
||||
log_warn "OpenShell gateway not connected — skipping inference route configuration."
|
||||
log_warn "Inference will be configured when the gateway is available (S3+)."
|
||||
log_warn "Ensure LUMINA_INFERENCE_BASE_URL and LUMINA_INFERENCE_MODEL are correct in .env."
|
||||
fi
|
||||
|
||||
# ── Vision smoke test ──────────────────────────────────────────────────────
|
||||
vision_smoke || {
|
||||
log_error "Vision smoke test FAILED."
|
||||
log_error "The inference endpoint does not appear to support multimodal/vision."
|
||||
log_error "Check:"
|
||||
log_error " 1. LUMINA_VISION_MODEL points to a vision-capable model"
|
||||
log_error " 2. The model server supports multimodal inputs"
|
||||
log_error " 3. LUMINA_INFERENCE_BASE_URL is correct"
|
||||
exit 1
|
||||
}
|
||||
|
||||
log_info "S2 complete: model configured, vision smoke passed."
|
||||
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/lib/common.sh — shared helpers for all host scripts
|
||||
# Sourced by bootstrap.sh, install.sh, and stage scripts.
|
||||
# Do not execute directly.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Colours (auto-disable when not a tty) ──────────────────────────────────
|
||||
if [[ -t 2 ]]; then
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
|
||||
BOLD='\033[1m'; NC='\033[0m'
|
||||
else
|
||||
RED=''; GREEN=''; YELLOW=''; BOLD=''; NC=''
|
||||
fi
|
||||
|
||||
# ── Logging ────────────────────────────────────────────────────────────────
|
||||
# Use printf to avoid echo -e interpreting escape sequences in arguments.
|
||||
log_info() { printf "${GREEN}[INFO]${NC} %s\n" "$*"; }
|
||||
log_warn() { printf "${YELLOW}[WARN]${NC} %s\n" "$*" >&2; }
|
||||
log_error() { printf "${RED}[ERROR]${NC} %s\n" "$*" >&2; }
|
||||
log_section() { printf "\n${BOLD}═══ %s ═══${NC}\n" "$*"; }
|
||||
|
||||
# ── Repo root (works from any subdirectory) ────────────────────────────────
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)")"
|
||||
|
||||
# ── Env loading ────────────────────────────────────────────────────────────
|
||||
# NOTE: Sources .env directly as shell code. This is a known pattern with a
|
||||
# theoretical injection surface if .env contains shell commands. In practice,
|
||||
# .env is created from .env.example (controlled by this repo) and edited by
|
||||
# the operator. Defense-in-depth: parse line-by-line instead of sourcing,
|
||||
# but the current approach matches the broader ecosystem convention.
|
||||
load_env() {
|
||||
local env_file="${1:-${REPO_ROOT}/.env}"
|
||||
if [[ -f "$env_file" ]]; then
|
||||
# shellcheck disable=SC1091
|
||||
set -a; source "$env_file"; set +a
|
||||
log_info "Loaded env from $env_file"
|
||||
else
|
||||
log_warn ".env not found at $env_file — variables must be set externally"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── CLI detection ──────────────────────────────────────────────────────────
|
||||
cmd_exists() { command -v "$1" &>/dev/null; }
|
||||
|
||||
require_cmd() {
|
||||
if ! cmd_exists "$1"; then
|
||||
log_error "Required command not found: $1"
|
||||
if [[ "${2:-}" ]]; then
|
||||
log_error "$2"
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Sandbox name ───────────────────────────────────────────────────────────
|
||||
get_sandbox_name() {
|
||||
echo "${LUMINA_SANDBOX:-hermes}"
|
||||
}
|
||||
|
||||
# ── Docker check ───────────────────────────────────────────────────────────
|
||||
docker_available() {
|
||||
cmd_exists docker && docker info &>/dev/null
|
||||
}
|
||||
|
||||
# ── nemohermes check ───────────────────────────────────────────────────────
|
||||
nemohermes_available() {
|
||||
cmd_exists nemohermes
|
||||
}
|
||||
|
||||
# ── openshell check ────────────────────────────────────────────────────────
|
||||
openshell_available() {
|
||||
cmd_exists openshell
|
||||
}
|
||||
|
||||
# ── Guard: script must be run as operator (not root-only, but warn) ────────
|
||||
warn_if_root() {
|
||||
if [[ "$(id -u)" -eq 0 ]]; then
|
||||
log_warn "Running as root. Some host scripts work better as a regular user with sudo access."
|
||||
fi
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/lib/env.sh — .env validation helpers
|
||||
# Sourced by install stages that need environment checks.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Required keys for S1. Each entry: "KEY description"
|
||||
S1_REQUIRED_KEYS=(
|
||||
"LUMINA_SANDBOX Sandbox name (default: hermes)"
|
||||
"LUMINA_INFERENCE_BASE_URL OpenAI-compatible inference endpoint base URL"
|
||||
"LUMINA_INFERENCE_MODEL Model identifier on the inference endpoint"
|
||||
"LUMINA_VISION_MODEL Vision-capable model identifier (may equal LUMINA_INFERENCE_MODEL)"
|
||||
)
|
||||
|
||||
# Optional keys with defaults
|
||||
declare -A S1_OPTIONAL_KEYS=(
|
||||
["LUMINA_INFERENCE_API_KEY"]="API key for the inference endpoint (empty if endpoint is unauthenticated)"
|
||||
["LUMINA_GATEWAY_URL"]="NemoClaw gateway URL (default: https://127.0.0.1:8080)"
|
||||
["LUMINA_DASHBOARD_PORT"]="Dashboard port (default: 18789)"
|
||||
)
|
||||
|
||||
# ── Validate .env has all required keys ────────────────────────────────────
|
||||
validate_env() {
|
||||
local env_file="${1:-${REPO_ROOT}/.env}"
|
||||
local missing=0
|
||||
|
||||
if [[ ! -f "$env_file" ]]; then
|
||||
log_error ".env file not found at $env_file"
|
||||
log_error "Copy .env.example to .env and fill in values first."
|
||||
return 1
|
||||
fi
|
||||
|
||||
for entry in "${S1_REQUIRED_KEYS[@]}"; do
|
||||
# Split on first space
|
||||
local key="${entry%% *}"
|
||||
local desc="${entry#* }"
|
||||
if ! grep -q "^${key}=" "$env_file" 2>/dev/null; then
|
||||
log_error "Missing required key: $key ($desc)"
|
||||
missing=1
|
||||
elif grep -qE "^${key}=[[:space:]]*$" "$env_file" 2>/dev/null; then
|
||||
log_error "Empty or whitespace-only required key: $key ($desc)"
|
||||
missing=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $missing -ne 0 ]]; then
|
||||
log_error "Fix missing keys in $env_file and re-run."
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info ".env validation passed ($env_file)"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── Create .env from .env.example if it doesn't exist ──────────────────────
|
||||
create_env_from_example() {
|
||||
local env_file="${1:-${REPO_ROOT}/.env}"
|
||||
local example_file="${2:-${REPO_ROOT}/.env.example}"
|
||||
|
||||
if [[ -f "$env_file" ]]; then
|
||||
log_info ".env already exists at $env_file — skipping creation"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ ! -f "$example_file" ]]; then
|
||||
log_error ".env.example not found at $example_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
cp "$example_file" "$env_file"
|
||||
log_info "Created $env_file from .env.example"
|
||||
log_warn "Edit $env_file with your actual values before continuing."
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/lib/vision_smoke.sh — vision capability smoke test
|
||||
# Sourced by S2 install stage.
|
||||
#
|
||||
# Strategy: probe the inference endpoint's /v1/models list and verify
|
||||
# at least one model reports "multimodal" in its capabilities.
|
||||
# Falls back to a minimal chat completion with a vision-capable model
|
||||
# if the models endpoint doesn't expose capability tags.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Vision smoke: check endpoint reports multimodal capability ─────────────
|
||||
vision_smoke() {
|
||||
local base_url="${LUMINA_INFERENCE_BASE_URL}"
|
||||
local model="${LUMINA_VISION_MODEL:-${LUMINA_INFERENCE_MODEL}}"
|
||||
local api_key="${LUMINA_INFERENCE_API_KEY:-}"
|
||||
|
||||
log_section "S2: Vision smoke test"
|
||||
log_info "Probing inference endpoint: $base_url"
|
||||
|
||||
# Build curl headers (-f: fail on HTTP error codes)
|
||||
local -a curl_args=(-sf --max-time 30)
|
||||
if [[ -n "$api_key" ]]; then
|
||||
curl_args+=(-H "Authorization: Bearer $api_key")
|
||||
fi
|
||||
|
||||
# ── Step 1: Check /v1/models for multimodal capability ──────────────────
|
||||
# LUMINA_INFERENCE_BASE_URL includes /v1 (e.g. http://host:port/v1)
|
||||
log_info "Checking models endpoint…"
|
||||
local models_json
|
||||
models_json=$(curl "${curl_args[@]}" "${base_url}/models" 2>/dev/null) || {
|
||||
log_error "Cannot reach inference endpoint at ${base_url}/models"
|
||||
log_error "Check that the endpoint is running and LUMINA_INFERENCE_BASE_URL is correct."
|
||||
return 1
|
||||
}
|
||||
|
||||
# Check for multimodal in capabilities array (case-sensitive per API spec)
|
||||
if printf '%s\n' "$models_json" | grep -q '"multimodal"'; then
|
||||
log_info "PASS: Inference endpoint reports multimodal capability"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# ── Step 2: Fallback — try a minimal chat completion with vision model ──
|
||||
log_warn "Models endpoint did not advertise 'multimodal' — falling back to chat completion probe"
|
||||
log_info "Testing chat completion with model: $model"
|
||||
|
||||
local response
|
||||
response=$(curl \
|
||||
-H "Content-Type: application/json" \
|
||||
"${curl_args[@]}" \
|
||||
"${base_url}/chat/completions" \
|
||||
-d "{
|
||||
\"model\": \"${model}\",
|
||||
\"messages\": [{\"role\": \"user\", \"content\": \"Say OK\"}],
|
||||
\"max_tokens\": 4
|
||||
}" 2>/dev/null) || {
|
||||
log_error "Chat completion probe failed to ${base_url}/chat/completions"
|
||||
log_error "The inference endpoint may be down or the model '${model}' is not available."
|
||||
return 1
|
||||
}
|
||||
|
||||
# Check for a valid response structure
|
||||
if printf '%s\n' "$response" | grep -q '"choices"'; then
|
||||
log_info "PASS: Chat completion succeeded with model ${model}"
|
||||
log_info "Vision capability assumed (endpoint supports multimodal per design)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_error "Chat completion probe returned unexpected response"
|
||||
log_error "Response (truncated): $(printf '%s' "$response" | head -c 500)"
|
||||
return 1
|
||||
}
|
||||
Reference in New Issue
Block a user