diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index fafa2b9a9dc..b1866a23329 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -3774,6 +3774,20 @@ function hostCommandExists(commandName: string): boolean { }); } +function ensureOllamaLinuxExtractionDependencies(): void { + if (hostCommandExists("zstd")) return; + console.log( + " The Ollama Linux installer requires zstd for archive extraction. " + + "The next step uses sudo to install zstd; you may be prompted for your password.", + ); + runShell(`if ! command -v apt-get >/dev/null 2>&1; then + echo "ERROR: Ollama requires zstd for extraction, and only apt-based Linux is supported here." >&2 + echo "Install zstd manually (for example, sudo dnf install zstd or sudo pacman -S zstd), then rerun ${cliName()} onboard." >&2 + exit 1 +fi +sudo apt-get update -qq && sudo apt-get install -y -qq --no-install-recommends zstd`); +} + function captureProcessArgs(pid: number): string { return runCapture(["ps", "-p", String(pid), "-o", "args="], { ignoreError: true, @@ -8343,7 +8357,11 @@ async function setupNim( }); sleep(2); } else { - console.log(" Installing Ollama via official installer..."); + ensureOllamaLinuxExtractionDependencies(); + console.log( + " The Ollama installer creates a system user, a systemd service, and writes to /usr/local. " + + "It uses sudo for those steps; you may be prompted for your password.", + ); runShell("set -o pipefail; curl -fsSL https://ollama.com/install.sh | sh"); // Give the just-started ollama.service a moment to bind port // 11434 before we probe or apply the systemd drop-in override. @@ -8368,6 +8386,11 @@ async function setupNim( // start: manual launch with the loopback binding. if (!isWsl() && hasOllamaSystemdUnit) { console.log(" Configuring Ollama systemd loopback override..."); + console.log( + ` Applying an Ollama systemd override (OLLAMA_HOST=127.0.0.1:${OLLAMA_PORT}). ` + + "The next steps use sudo to write the drop-in, reload systemd, and restart the service; " + + "you may be prompted for your password.", + ); const dropInBody = `[Service]\nEnvironment="OLLAMA_HOST=127.0.0.1:${OLLAMA_PORT}"\n`; const tmpDropIn = secureTempFile("nemoclaw-ollama-override", ".conf"); fs.writeFileSync(tmpDropIn, dropInBody, { mode: 0o644 }); diff --git a/test/onboard-selection.test.ts b/test/onboard-selection.test.ts index 88ec7af18c7..f545ff79f06 100644 --- a/test/onboard-selection.test.ts +++ b/test/onboard-selection.test.ts @@ -3765,6 +3765,7 @@ let promptCalls = 0; const messages = []; const updates = []; const runCommands = []; +const events = []; credentials.prompt = async (message) => { promptCalls += 1; @@ -3792,10 +3793,13 @@ runner.runCapture = (command) => { return ""; }; runner.run = (command, opts) => { - runCommands.push(typeof command === "string" ? command : command.join(" ")); + const rendered = typeof command === "string" ? command : command.join(" "); + runCommands.push(rendered); + events.push({ type: "command", value: rendered }); }; runner.runShell = (command, opts) => { runCommands.push(command); + events.push({ type: "command", value: command }); }; registry.updateSandbox = (_name, update) => updates.push(update); @@ -3808,10 +3812,14 @@ const { setupNim } = require(${onboardPath}); (async () => { const originalLog = console.log; const lines = []; - console.log = (...args) => lines.push(args.join(" ")); + console.log = (...args) => { + const line = args.join(" "); + lines.push(line); + events.push({ type: "log", value: line }); + }; try { const result = await setupNim("install-test", null); - originalLog(JSON.stringify({ result, promptCalls, messages, updates, lines, runCommands })); + originalLog(JSON.stringify({ result, promptCalls, messages, updates, lines, runCommands, events })); } finally { console.log = originalLog; } @@ -3846,6 +3854,43 @@ const { setupNim } = require(${onboardPath}); assert.equal(payload.result.provider, "ollama-local"); // Should have run the curl installer (not brew) + const zstdPreflightIndex = payload.runCommands.findIndex((cmd: string) => + cmd.includes("apt-get install -y -qq --no-install-recommends zstd"), + ); + const ollamaInstallerIndex = payload.runCommands.findIndex((cmd: string) => + cmd.includes("ollama.com/install.sh"), + ); + assert.ok(zstdPreflightIndex >= 0, "Should preflight zstd before the Ollama installer"); + assert.ok( + ollamaInstallerIndex > zstdPreflightIndex, + "Should install zstd before running the Ollama installer", + ); + const zstdWarningEventIndex = payload.events.findIndex( + (event: { type: string; value: string }) => + event.type === "log" && event.value.includes("requires zstd for archive extraction"), + ); + const zstdCommandEventIndex = payload.events.findIndex( + (event: { type: string; value: string }) => + event.type === "command" && + event.value.includes("apt-get install -y -qq --no-install-recommends zstd"), + ); + const installerWarningEventIndex = payload.events.findIndex( + (event: { type: string; value: string }) => + event.type === "log" && + event.value.includes("creates a system user, a systemd service, and writes to /usr/local"), + ); + const installerCommandEventIndex = payload.events.findIndex( + (event: { type: string; value: string }) => + event.type === "command" && event.value.includes("ollama.com/install.sh"), + ); + assert.ok( + zstdWarningEventIndex >= 0 && zstdWarningEventIndex < zstdCommandEventIndex, + "Should explain the zstd sudo install before running apt-get", + ); + assert.ok( + installerWarningEventIndex >= 0 && installerWarningEventIndex < installerCommandEventIndex, + "Should explain the Ollama installer sudo usage before running it", + ); assert.ok( payload.runCommands.some((cmd: string) => cmd.includes("ollama.com/install.sh")), "Should use curl installer on Linux", @@ -3937,6 +3982,8 @@ const { setupNim } = require(${onboardPath}); }); assert.equal(result.status, 1); + assert.match(result.stdout, /Applying an Ollama systemd override/); + assert.match(result.stdout, /use sudo to write the drop-in, reload systemd, and restart the service/); assert.match(result.stderr, /Failed to apply Ollama systemd loopback override/); assert.match(result.stderr, /Refusing to continue/); }); @@ -4066,6 +4113,20 @@ const { setupNim } = require(${onboardPath}); assert.equal(payload.promptCalls, 0); assert.equal(payload.result.provider, "ollama-local"); + const zstdPreflightIndex = payload.runCommands.findIndex((cmd: string) => + cmd.includes("apt-get install -y -qq --no-install-recommends zstd"), + ); + const ollamaInstallerIndex = payload.runCommands.findIndex((cmd: string) => + cmd.includes("ollama.com/install.sh"), + ); + assert.ok( + zstdPreflightIndex >= 0, + "Should preflight zstd before the non-interactive Ollama installer", + ); + assert.ok( + ollamaInstallerIndex > zstdPreflightIndex, + "Should install zstd before running the non-interactive Ollama installer", + ); assert.ok( payload.runCommands.some((cmd: string) => cmd.includes("ollama.com/install.sh")), "Should use the Ollama installer when requested non-interactively on a fresh host",