diff --git a/bin/lib/onboard.js b/bin/lib/onboard.js index 53069e339..886621d32 100644 --- a/bin/lib/onboard.js +++ b/bin/lib/onboard.js @@ -1453,10 +1453,10 @@ function getNonInteractiveProvider() { anthropiccompatible: "anthropicCompatible", }; const normalized = aliases[providerKey] || providerKey; - const validProviders = new Set(["build", "openai", "anthropic", "anthropicCompatible", "gemini", "ollama", "custom", "nim-local", "vllm"]); + const validProviders = new Set(["build", "openai", "anthropic", "anthropicCompatible", "gemini", "ollama", "custom", "nim-local", "vllm", "install-ollama"]); if (!validProviders.has(normalized)) { console.error(` Unsupported NEMOCLAW_PROVIDER: ${providerKey}`); - console.error(" Valid values: build, openai, anthropic, anthropicCompatible, gemini, ollama, custom, nim-local, vllm"); + console.error(" Valid values: build, openai, anthropic, anthropicCompatible, gemini, ollama, custom, nim-local, vllm, install-ollama"); process.exit(1); } @@ -1931,9 +1931,14 @@ async function setupNim(gpu) { }); } - // On macOS without Ollama, offer to install it - if (!hasOllama && process.platform === "darwin") { - options.push({ key: "install-ollama", label: "Install Ollama (macOS)" }); + // Without Ollama, offer to install it so users always have a local fallback + // (e.g. when the NVIDIA API server is down and cloud keys are unavailable) + if (!hasOllama && !ollamaRunning) { + if (process.platform === "darwin") { + options.push({ key: "install-ollama", label: "Install Ollama (macOS)" }); + } else if (process.platform === "linux") { + options.push({ key: "install-ollama", label: "Install Ollama (Linux)" }); + } } if (options.length > 1) { @@ -1945,8 +1950,15 @@ async function setupNim(gpu) { const providerKey = requestedProvider || "build"; selected = options.find((o) => o.key === providerKey); if (!selected) { - console.error(` Requested provider '${providerKey}' is not available in this environment.`); - process.exit(1); + // install-ollama is valid even when Ollama is already installed — + // fall back to the existing ollama option silently + if (providerKey === "install-ollama") { + selected = options.find((o) => o.key === "ollama"); + } + if (!selected) { + console.error(` Requested provider '${providerKey}' is not available in this environment.`); + process.exit(1); + } } note(` [non-interactive] Provider: ${selected.key}`); } else { @@ -2202,8 +2214,13 @@ async function setupNim(gpu) { } break; } else if (selected.key === "install-ollama") { - console.log(" Installing Ollama via Homebrew..."); - run("brew install ollama", { ignoreError: true }); + if (process.platform === "darwin") { + console.log(" Installing Ollama via Homebrew..."); + run("brew install ollama"); + } else { + console.log(" Installing Ollama via official installer..."); + run("curl -fsSL https://ollama.com/install.sh | sh"); + } console.log(" Starting Ollama..."); run("OLLAMA_HOST=0.0.0.0:11434 ollama serve > /dev/null 2>&1 &", { ignoreError: true }); sleep(2); diff --git a/test/onboard-selection.test.js b/test/onboard-selection.test.js index 6cf6e436a..4d9d4e45b 100644 --- a/test/onboard-selection.test.js +++ b/test/onboard-selection.test.js @@ -1303,4 +1303,103 @@ const { setupNim } = require(${onboardPath}); assert.ok(payload.lines.some((line) => line.includes("Please choose a provider/model again"))); assert.equal(payload.messages.filter((message) => /Choose \[/.test(message)).length, 2); }); + + it("offers install-ollama option on Linux when Ollama is not installed", () => { + const repoRoot = path.join(import.meta.dirname, ".."); + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-install-ollama-")); + const scriptPath = path.join(tmpDir, "install-ollama-check.js"); + const onboardPath = JSON.stringify(path.join(repoRoot, "bin", "lib", "onboard.js")); + const credentialsPath = JSON.stringify(path.join(repoRoot, "bin", "lib", "credentials.js")); + const runnerPath = JSON.stringify(path.join(repoRoot, "bin", "lib", "runner.js")); + const registryPath = JSON.stringify(path.join(repoRoot, "bin", "lib", "registry.js")); + // Simulate: no Ollama installed, no Ollama running, no vLLM — only cloud + install-ollama should appear. + // User picks install-ollama (option 7). The install command is mocked to succeed. + const script = String.raw` +const credentials = require(${credentialsPath}); +const runner = require(${runnerPath}); +const registry = require(${registryPath}); + +let promptCalls = 0; +const messages = []; +const updates = []; +const runCommands = []; + +credentials.prompt = async (message) => { + promptCalls += 1; + messages.push(message); + // Select option 7 (install-ollama) on first prompt, default on model prompt + if (promptCalls === 1) return "7"; + return ""; +}; +credentials.ensureApiKey = async () => {}; +runner.runCapture = (command) => { + // No ollama installed + if (command.includes("command -v ollama")) return ""; + // No ollama running + if (command.includes("localhost:11434/api/tags")) return ""; + // No vLLM running + if (command.includes("localhost:8000/v1/models")) return ""; + // After install, ollama list returns a model + if (command.includes("ollama list")) return "qwen3:8b abc 5 GB now"; + return ""; +}; +runner.run = (command, opts) => { + runCommands.push(command); +}; +registry.updateSandbox = (_name, update) => updates.push(update); + +// Force platform to linux for this test +Object.defineProperty(process, 'platform', { value: 'linux' }); + +const { setupNim } = require(${onboardPath}); + +(async () => { + const originalLog = console.log; + const lines = []; + console.log = (...args) => lines.push(args.join(" ")); + try { + const result = await setupNim("install-test", null); + originalLog(JSON.stringify({ result, promptCalls, messages, updates, lines, runCommands })); + } finally { + console.log = originalLog; + } +})().catch((error) => { + console.error(error); + process.exit(1); +}); +`; + fs.writeFileSync(scriptPath, script); + + const result = spawnSync(process.execPath, [scriptPath], { + cwd: repoRoot, + encoding: "utf-8", + env: { + ...process.env, + HOME: tmpDir, + }, + }); + + assert.equal(result.status, 0, `Process failed: ${result.stderr}`); + assert.notEqual(result.stdout.trim(), "", result.stderr); + const payload = JSON.parse(result.stdout.trim()); + + // Should have shown the "Install Ollama (Linux)" option + assert.ok( + payload.lines.some((line) => line.includes("Install Ollama (Linux)")), + "Should show Install Ollama option on Linux" + ); + + // Should have selected ollama-local provider after install + assert.equal(payload.result.provider, "ollama-local"); + + // Should have run the curl installer (not brew) + assert.ok( + payload.runCommands.some((cmd) => cmd.includes("ollama.com/install.sh")), + "Should use curl installer on Linux" + ); + assert.ok( + !payload.runCommands.some((cmd) => cmd.includes("brew install")), + "Should NOT use brew on Linux" + ); + }); });