// // cli.js — locating the Claude Code executable and building its environment. // const { conf, log, warn, runCommand } = require("./util.js"); const KEYCHAIN_SERVICE = "unsupervised.claudenova"; const KEYCHAIN_ACCOUNT = "ANTHROPIC_API_KEY"; // Standard install locations, in the order Claude Code's own installer prefers. const CANDIDATE_PATHS = [ "/.local/bin/claude", "/.claude/local/claude", ]; const CANDIDATE_ABSOLUTE = [ "/opt/homebrew/bin/claude", "/usr/local/bin/claude", "/usr/bin/claude", ]; let cachedBinary = null; function isExecutable(path) { try { return nova.fs.access(path, nova.fs.X_OK); } catch (_) { return false; } } /** Ask the user's login shell where `claude` lives — catches nvm, mise, asdf, etc. */ async function resolveViaLoginShell() { const shell = nova.environment["SHELL"] || "/bin/zsh"; const result = await runCommand(shell, ["-l", "-c", "command -v claude"], { timeout: 5000 }); if (result.status === 0) { const path = result.stdout.trim().split("\n").pop(); if (path && path.startsWith("/") && isExecutable(path)) { return path; } } return null; } /** * Find the `claude` executable. * Resolution order: configured path, home-relative installs, system paths, login shell. * The result is cached until `forgetBinary()` is called. */ async function resolveBinary() { if (cachedBinary) return cachedBinary; const configured = conf("claudenova.binaryPath", "").trim(); if (configured) { if (isExecutable(configured)) { cachedBinary = configured; return cachedBinary; } throw new Error( `The configured Claude Code executable is not runnable: ${configured}` ); } const home = nova.environment["HOME"] || ""; const candidates = CANDIDATE_PATHS.map((p) => home + p).concat(CANDIDATE_ABSOLUTE); for (const candidate of candidates) { if (isExecutable(candidate)) { cachedBinary = candidate; log("resolved claude binary:", candidate); return cachedBinary; } } const fromShell = await resolveViaLoginShell(); if (fromShell) { cachedBinary = fromShell; log("resolved claude binary via login shell:", fromShell); return cachedBinary; } throw new Error( "Claude Code was not found. Install it from https://claude.com/product/claude-code, " + "or set the executable path in the extension's settings." ); } function forgetBinary() { cachedBinary = null; } /** The API key the user stored in the keychain, if any. */ function storedApiKey() { try { return nova.credentials.getPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT); } catch (err) { warn("could not read the keychain:", err); return null; } } function setStoredApiKey(key) { nova.credentials.setPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT, key); } function clearStoredApiKey() { try { nova.credentials.removePassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT); } catch (err) { warn("could not clear the keychain entry:", err); } } /** * Environment for spawned `claude` processes: Nova's task environment, plus a * stored API key when the user supplied one instead of signing in. */ function environment(extra = {}) { const env = Object.assign({}, nova.environment, extra); const key = storedApiKey(); if (key) { env["ANTHROPIC_API_KEY"] = key; } // Nova's environment can be sparse; make sure the CLI can find its own tooling. if (!env["PATH"]) { env["PATH"] = "/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin"; } if (!env["HOME"] && nova.environment["HOME"]) { env["HOME"] = nova.environment["HOME"]; } return env; } async function version() { const binary = await resolveBinary(); const result = await runCommand(binary, ["--version"], { env: environment(), timeout: 10000, }); return result.status === 0 ? result.stdout.trim() : null; } module.exports = { resolveBinary, forgetBinary, environment, version, storedApiKey, setStoredApiKey, clearStoredApiKey, KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT, };