Skip to content
Closed
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
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ repos:

- id: pyright-check
name: Pyright (nemoclaw-blueprint)
entry: bash -c 'cd nemoclaw-blueprint && uv run --with pyright pyright .'
entry: bash -c 'cd nemoclaw-blueprint && uv run --with pyright pyright'
language: system
pass_filenames: false
always_run: true
Expand Down
75 changes: 46 additions & 29 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ NEMOCLAW_VERSION="$(resolve_installer_version)"
# ---------------------------------------------------------------------------
if [[ -z "${NO_COLOR:-}" && -t 1 ]]; then
if [[ "${COLORTERM:-}" == "truecolor" || "${COLORTERM:-}" == "24bit" ]]; then
C_GREEN=$'\033[38;2;118;185;0m' # #76B900 — exact NVIDIA green
C_GREEN=$'\033[38;2;118;185;0m' # #76B900 — exact NVIDIA green
else
C_GREEN=$'\033[38;5;148m' # closest 256-color on dark backgrounds
C_GREEN=$'\033[38;5;148m' # closest 256-color on dark backgrounds
fi
C_BOLD=$'\033[1m'
C_DIM=$'\033[2m'
Expand All @@ -44,10 +44,13 @@ fi
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
info() { printf "${C_CYAN}[INFO]${C_RESET} %s\n" "$*"; }
warn() { printf "${C_YELLOW}[WARN]${C_RESET} %s\n" "$*"; }
error() { printf "${C_RED}[ERROR]${C_RESET} %s\n" "$*" >&2; exit 1; }
ok() { printf " ${C_GREEN}✓${C_RESET} %s\n" "$*"; }
info() { printf "${C_CYAN}[INFO]${C_RESET} %s\n" "$*"; }
warn() { printf "${C_YELLOW}[WARN]${C_RESET} %s\n" "$*"; }
error() {
printf "${C_RED}[ERROR]${C_RESET} %s\n" "$*" >&2
exit 1
}
ok() { printf " ${C_GREEN}✓${C_RESET} %s\n" "$*"; }

resolve_default_sandbox_name() {
local registry_file="${HOME}/.nemoclaw/sandboxes.json"
Expand Down Expand Up @@ -95,7 +98,7 @@ print_banner() {
}

print_done() {
local elapsed=$(( SECONDS - _INSTALL_START ))
local elapsed=$((SECONDS - _INSTALL_START))
local sandbox_name
sandbox_name="$(resolve_default_sandbox_name)"
info "=== Installation complete ==="
Expand Down Expand Up @@ -146,15 +149,17 @@ usage() {
# Stdout/stderr are captured; dumped only on failure.
# Falls back to plain output when stdout is not a TTY (CI / piped installs).
spin() {
local msg="$1"; shift
local msg="$1"
shift

if [[ ! -t 1 ]]; then
info "$msg"
"$@"
return
fi

local log; log=$(mktemp)
local log
log=$(mktemp)
"$@" >"$log" 2>&1 &
local pid=$! i=0
local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
Expand Down Expand Up @@ -191,12 +196,13 @@ ORIGINAL_PATH="${PATH:-}"

# Compare two semver strings (major.minor.patch). Returns 0 if $1 >= $2.
version_gte() {
local IFS=.
local -a a=($1) b=($2)
local -a a b
IFS=. read -ra a <<<"$1"
IFS=. read -ra b <<<"$2"
for i in 0 1 2; do
local ai=${a[$i]:-0} bi=${b[$i]:-0}
if (( ai > bi )); then return 0; fi
if (( ai < bi )); then return 1; fi
if ((ai > bi)); then return 0; fi
if ((ai < bi)); then return 1; fi
done
return 0
}
Expand Down Expand Up @@ -265,7 +271,7 @@ ensure_supported_runtime() {
[[ "$node_major" =~ ^[0-9]+$ ]] || error "Could not determine Node.js version from '${node_version}'. ${RUNTIME_REQUIREMENT_MSG}"
[[ "$npm_major" =~ ^[0-9]+$ ]] || error "Could not determine npm version from '${npm_version}'. ${RUNTIME_REQUIREMENT_MSG}"

if (( node_major < MIN_NODE_MAJOR || npm_major < MIN_NPM_MAJOR )); then
if ((node_major < MIN_NODE_MAJOR || npm_major < MIN_NPM_MAJOR)); then
error "Unsupported runtime detected: Node.js ${node_version:-unknown}, npm ${npm_version:-unknown}. ${RUNTIME_REQUIREMENT_MSG} Upgrade Node.js and rerun the installer."
fi

Expand All @@ -288,15 +294,18 @@ install_nodejs() {
local nvm_tmp
nvm_tmp="$(mktemp)"
curl -fsSL "https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh" -o "$nvm_tmp" \
|| { rm -f "$nvm_tmp"; error "Failed to download nvm installer"; }
|| {
rm -f "$nvm_tmp"
error "Failed to download nvm installer"
}
local actual_hash
if command_exists sha256sum; then
actual_hash="$(sha256sum "$nvm_tmp" | awk '{print $1}')"
elif command_exists shasum; then
actual_hash="$(shasum -a 256 "$nvm_tmp" | awk '{print $1}')"
else
warn "No SHA-256 tool found — skipping nvm integrity check"
actual_hash="$NVM_SHA256" # allow execution
actual_hash="$NVM_SHA256" # allow execution
fi
if [[ "$actual_hash" != "$NVM_SHA256" ]]; then
rm -f "$nvm_tmp"
Expand Down Expand Up @@ -341,7 +350,7 @@ get_vram_mb() {
if [[ "$(uname -s)" == "Darwin" ]] && command_exists sysctl; then
local bytes
bytes=$(sysctl -n hw.memsize 2>/dev/null || echo 0)
echo $(( bytes / 1024 / 1024 ))
echo $((bytes / 1024 / 1024))
return
fi
echo 0
Expand Down Expand Up @@ -373,10 +382,10 @@ install_or_upgrade_ollama() {
# Pull the appropriate model based on VRAM
local vram_mb
vram_mb=$(get_vram_mb)
local vram_gb=$(( vram_mb / 1024 ))
local vram_gb=$((vram_mb / 1024))
info "Detected ${vram_gb} GB VRAM"

if (( vram_gb >= 120 )); then
if ((vram_gb >= 120)); then
info "Pulling nemotron-3-super:120b…"
ollama pull nemotron-3-super:120b
else
Expand Down Expand Up @@ -406,13 +415,12 @@ pre_extract_openclaw() {
info "Pre-extracting openclaw@${openclaw_version} with system tar (GH-503 workaround)…"
local tmpdir
tmpdir="$(mktemp -d)"
if npm pack "openclaw@${openclaw_version}" --pack-destination "$tmpdir" > /dev/null 2>&1; then
if npm pack "openclaw@${openclaw_version}" --pack-destination "$tmpdir" >/dev/null 2>&1; then
local tgz
tgz="$(find "$tmpdir" -maxdepth 1 -name 'openclaw-*.tgz' -print -quit)"
if [[ -n "$tgz" && -f "$tgz" ]]; then
if mkdir -p "${install_dir}/node_modules/openclaw" \
&& tar xzf "$tgz" -C "${install_dir}/node_modules/openclaw" --strip-components=1
then
&& tar xzf "$tgz" -C "${install_dir}/node_modules/openclaw" --strip-components=1; then
info "openclaw pre-extracted successfully"
else
warn "Failed to extract openclaw tarball"
Expand All @@ -435,8 +443,8 @@ pre_extract_openclaw() {
install_nemoclaw() {
if [[ -f "./package.json" ]] && grep -q '"name": "nemoclaw"' ./package.json 2>/dev/null; then
info "NemoClaw package.json found in current directory — installing from source…"
spin "Preparing OpenClaw package" bash -lc "$(declare -f pre_extract_openclaw); pre_extract_openclaw \"\$1\"" _ "$(pwd)" || \
warn "Pre-extraction failed — npm install may fail if openclaw tarball is broken"
spin "Preparing OpenClaw package" bash -lc "$(declare -f pre_extract_openclaw); pre_extract_openclaw \"\$1\"" _ "$(pwd)" \
|| warn "Pre-extraction failed — npm install may fail if openclaw tarball is broken"
spin "Installing NemoClaw dependencies" npm install --ignore-scripts
spin "Building NemoClaw plugin" bash -lc 'cd nemoclaw && npm install --ignore-scripts && npm run build'
spin "Linking NemoClaw CLI" npm link
Expand All @@ -449,8 +457,8 @@ install_nemoclaw() {
rm -rf "$nemoclaw_src"
mkdir -p "$(dirname "$nemoclaw_src")"
spin "Cloning NemoClaw source" git clone --depth 1 https://github.com/NVIDIA/NemoClaw.git "$nemoclaw_src"
spin "Preparing OpenClaw package" bash -lc "$(declare -f pre_extract_openclaw); pre_extract_openclaw \"\$1\"" _ "$nemoclaw_src" || \
warn "Pre-extraction failed — npm install may fail if openclaw tarball is broken"
spin "Preparing OpenClaw package" bash -lc "$(declare -f pre_extract_openclaw); pre_extract_openclaw \"\$1\"" _ "$nemoclaw_src" \
|| warn "Pre-extraction failed — npm install may fail if openclaw tarball is broken"
spin "Installing NemoClaw dependencies" bash -lc "cd \"$nemoclaw_src\" && npm install --ignore-scripts"
spin "Building NemoClaw plugin" bash -lc "cd \"$nemoclaw_src\"/nemoclaw && npm install --ignore-scripts && npm run build"
spin "Linking NemoClaw CLI" bash -lc "cd \"$nemoclaw_src\" && npm link"
Expand Down Expand Up @@ -558,9 +566,18 @@ main() {
for arg in "$@"; do
case "$arg" in
--non-interactive) NON_INTERACTIVE=1 ;;
--version|-v) printf "nemoclaw-installer v%s\n" "$NEMOCLAW_VERSION"; exit 0 ;;
--help|-h) usage; exit 0 ;;
*) usage; error "Unknown option: $arg" ;;
--version | -v)
printf "nemoclaw-installer v%s\n" "$NEMOCLAW_VERSION"
exit 0
;;
--help | -h)
usage
exit 0
;;
*)
usage
error "Unknown option: $arg"
;;
esac
done
# Also honor env var
Expand Down
2 changes: 1 addition & 1 deletion nemoclaw-blueprint/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ format:
check:
ruff check .
ruff format --check .
uv run --extra dev --with pyright pyright .
uv run --extra dev --with pyright pyright
1 change: 1 addition & 0 deletions nemoclaw-blueprint/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ ignore = [
pythonVersion = "3.11"
typeCheckingMode = "strict"
include = ["orchestrator", "migrations"]
exclude = ["**/test_*.py"]

[tool.ruff.format]
quote-style = "double"
Expand Down
155 changes: 79 additions & 76 deletions scripts/backup-workspace.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ NC='\033[0m'

info() { echo -e "${GREEN}[backup]${NC} $1"; }
warn() { echo -e "${YELLOW}[backup]${NC} $1"; }
fail() { echo -e "${RED}[backup]${NC} $1" >&2; exit 1; }
fail() {
echo -e "${RED}[backup]${NC} $1" >&2
exit 1
}

usage() {
cat <<EOF
cat <<EOF
Usage:
$(basename "$0") backup <sandbox-name>
$(basename "$0") restore <sandbox-name> [timestamp]
Expand All @@ -31,88 +34,88 @@ Commands:

Backup location: ${BACKUP_BASE}/<timestamp>/
EOF
exit 1
exit 1
}

do_backup() {
local sandbox="$1"
local ts
ts="$(date +%Y%m%d-%H%M%S)"
local dest="${BACKUP_BASE}/${ts}"

mkdir -p "$BACKUP_BASE"
chmod 0700 "${HOME}/.nemoclaw" "$BACKUP_BASE" || \
fail "Failed to set secure permissions on ${HOME}/.nemoclaw — check directory ownership."
mkdir -p "$dest"
chmod 0700 "$dest"

info "Backing up workspace from sandbox '${sandbox}'..."

local count=0
for f in "${FILES[@]}"; do
if openshell sandbox download "$sandbox" "${WORKSPACE_PATH}/${f}" "${dest}/"; then
count=$((count + 1))
else
warn "Skipped ${f} (not found or download failed)"
fi
done

for d in "${DIRS[@]}"; do
if openshell sandbox download "$sandbox" "${WORKSPACE_PATH}/${d}/" "${dest}/${d}/"; then
count=$((count + 1))
else
warn "Skipped ${d}/ (not found or download failed)"
fi
done

if [ "$count" -eq 0 ]; then
fail "No files were backed up. Check that the sandbox '${sandbox}' exists and has workspace files."
local sandbox="$1"
local ts
ts="$(date +%Y%m%d-%H%M%S)"
local dest="${BACKUP_BASE}/${ts}"

mkdir -p "$BACKUP_BASE"
chmod 0700 "${HOME}/.nemoclaw" "$BACKUP_BASE" \
|| fail "Failed to set secure permissions on ${HOME}/.nemoclaw — check directory ownership."
mkdir -p "$dest"
chmod 0700 "$dest"

info "Backing up workspace from sandbox '${sandbox}'..."

local count=0
for f in "${FILES[@]}"; do
if openshell sandbox download "$sandbox" "${WORKSPACE_PATH}/${f}" "${dest}/"; then
count=$((count + 1))
else
warn "Skipped ${f} (not found or download failed)"
fi
done

for d in "${DIRS[@]}"; do
if openshell sandbox download "$sandbox" "${WORKSPACE_PATH}/${d}/" "${dest}/${d}/"; then
count=$((count + 1))
else
warn "Skipped ${d}/ (not found or download failed)"
fi
done

if [ "$count" -eq 0 ]; then
fail "No files were backed up. Check that the sandbox '${sandbox}' exists and has workspace files."
fi

info "Backup saved to ${dest}/ (${count} items)"
info "Backup saved to ${dest}/ (${count} items)"
}

do_restore() {
local sandbox="$1"
local ts="${2:-}"

if [ -z "$ts" ]; then
ts="$(ls -1 "$BACKUP_BASE" 2>/dev/null | sort -r | head -n1)"
[ -n "$ts" ] || fail "No backups found in ${BACKUP_BASE}/"
info "Using most recent backup: ${ts}"
local sandbox="$1"
local ts="${2:-}"

if [ -z "$ts" ]; then
ts="$(ls -1 "$BACKUP_BASE" 2>/dev/null | sort -r | head -n1)"
[ -n "$ts" ] || fail "No backups found in ${BACKUP_BASE}/"
Comment on lines +83 to +84
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle missing backup directory without errexit short-circuit

Line 83 can exit the script early under set -euo pipefail if ${BACKUP_BASE} does not exist, so Line 84’s explicit fail message may never run.

Suggested fix
-    ts="$(ls -1 "$BACKUP_BASE" 2>/dev/null | sort -r | head -n1)"
-    [ -n "$ts" ] || fail "No backups found in ${BACKUP_BASE}/"
+    ts="$(
+      find "$BACKUP_BASE" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' 2>/dev/null \
+        | sort -r \
+        | head -n1
+    )"
+    [ -n "$ts" ] || fail "No backups found in ${BACKUP_BASE}/"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/backup-workspace.sh` around lines 83 - 84, The current pipeline
`ts="$(ls -1 "$BACKUP_BASE" 2>/dev/null | sort -r | head -n1)"` can cause the
script to exit under set -euo pipefail if $BACKUP_BASE doesn't exist, so first
explicitly check that the directory exists and is readable (test -d
"$BACKUP_BASE") before running the ls pipeline; if the test fails call the
existing fail function with the message "No backups found in ${BACKUP_BASE}/" or
a clearer "Backup directory ${BACKUP_BASE} does not exist" and only then run the
ts assignment (or run a safe glob/read with find) so that the explicit fail in
the check always executes instead of being short-circuited by errexit when
computing ts.

info "Using most recent backup: ${ts}"
fi

local src="${BACKUP_BASE}/${ts}"
[ -d "$src" ] || fail "Backup directory not found: ${src}"

info "Restoring workspace to sandbox '${sandbox}' from ${src}..."

local count=0
for f in "${FILES[@]}"; do
if [ -f "${src}/${f}" ]; then
if openshell sandbox upload "$sandbox" "${src}/${f}" "${WORKSPACE_PATH}/"; then
count=$((count + 1))
else
warn "Failed to restore ${f}"
fi
fi

local src="${BACKUP_BASE}/${ts}"
[ -d "$src" ] || fail "Backup directory not found: ${src}"

info "Restoring workspace to sandbox '${sandbox}' from ${src}..."

local count=0
for f in "${FILES[@]}"; do
if [ -f "${src}/${f}" ]; then
if openshell sandbox upload "$sandbox" "${src}/${f}" "${WORKSPACE_PATH}/"; then
count=$((count + 1))
else
warn "Failed to restore ${f}"
fi
fi
done

for d in "${DIRS[@]}"; do
if [ -d "${src}/${d}" ]; then
if openshell sandbox upload "$sandbox" "${src}/${d}/" "${WORKSPACE_PATH}/${d}/"; then
count=$((count + 1))
else
warn "Failed to restore ${d}/"
fi
fi
done

if [ "$count" -eq 0 ]; then
fail "No files were restored. Check that the sandbox '${sandbox}' is running."
done

for d in "${DIRS[@]}"; do
if [ -d "${src}/${d}" ]; then
if openshell sandbox upload "$sandbox" "${src}/${d}/" "${WORKSPACE_PATH}/${d}/"; then
count=$((count + 1))
else
warn "Failed to restore ${d}/"
fi
fi
done

if [ "$count" -eq 0 ]; then
fail "No files were restored. Check that the sandbox '${sandbox}' is running."
fi

info "Restored ${count} items to sandbox '${sandbox}'."
info "Restored ${count} items to sandbox '${sandbox}'."
}

# --- Main ---
Expand All @@ -125,7 +128,7 @@ sandbox="$2"
shift 2

case "$action" in
backup) do_backup "$sandbox" ;;
restore) do_restore "$sandbox" "$@" ;;
*) usage ;;
backup) do_backup "$sandbox" ;;
restore) do_restore "$sandbox" "$@" ;;
*) usage ;;
esac
Loading
Loading