Skip to content
Open
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
35 changes: 26 additions & 9 deletions bin/lib/onboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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) {
Expand All @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down
99 changes: 99 additions & 0 deletions test/onboard-selection.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"
);
});
});