First version

This commit is contained in:
2026-08-11 12:38:02 -03:00
parent 42e875d866
commit 89d1a06204
37 changed files with 5660 additions and 4 deletions
+156
View File
@@ -0,0 +1,156 @@
//
// 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,
};