// // launch.js — building the command that starts or resumes a session. // // Nova exposes no way for an extension to open its built-in terminal or type // into one (no terminal object in the API; `newTerminal:` in the app is an // Objective-C menu selector with no JS bridge). So the extension prepares the // command and puts it on the clipboard; you open Nova's terminal and paste. // // The upside of launching a plain CLI session is that it is a completely // ordinary one: stored under ~/.claude/projects, resumable from any terminal, // and eligible for Remote Control. // const { conf, log, warn } = require("./util.js"); const cli = require("./cli.js"); /** Quote a value for a POSIX shell. */ function shellQuote(value) { const text = String(value); if (/^[A-Za-z0-9_./:@%+=-]+$/.test(text)) return text; return "'" + text.replace(/'/g, `'\\''`) + "'"; } /** * Flags shared by new and resumed sessions, taken from settings so a launched * session matches what the extension is configured for. */ function configuredFlags() { const flags = []; const model = conf("claudenova.model", ""); if (model) flags.push("--model", model); const effort = conf("claudenova.effort", ""); if (effort) flags.push("--effort", effort); const mode = conf("claudenova.permissionMode", ""); if (mode) flags.push("--permission-mode", mode); const extra = String(conf("claudenova.extraArgs", "")).trim(); if (extra) { for (const piece of extra.split(/\s+/)) flags.push(piece); } return flags; } /** * Build a launch command line. * * @param options.resume session id to resume * @param options.remoteControl enable Remote Control for the session * @param options.cd prefix with a `cd` into the workspace * @param options.binary executable path (defaults to the resolved one) */ function buildCommand(options = {}) { const parts = []; if (options.cd !== false && nova.workspace.path) { parts.push("cd", shellQuote(nova.workspace.path), "&&"); } // `claude` on PATH reads better than an absolute path; fall back to the // resolved binary when the shell would not find it. parts.push(options.binary ? shellQuote(options.binary) : "claude"); if (options.resume) { parts.push("--resume", shellQuote(options.resume)); } if (options.remoteControl) { parts.push("--remote-control"); } for (const flag of configuredFlags()) { parts.push(shellQuote(flag)); } return parts.join(" "); } /** * Resolve which executable spelling to use: bare `claude` when the login shell * can find it, otherwise the absolute path we resolved. */ async function preferredBinary() { try { const resolved = await cli.resolveBinary(); const home = nova.environment["HOME"] || ""; // The standard install is on PATH for interactive shells; anything // unusual (a configured path, a version manager shim) is safer spelled // out in full. if (resolved === nova.path.join(home, ".local/bin/claude")) return null; if (resolved === "/opt/homebrew/bin/claude") return null; if (resolved === "/usr/local/bin/claude") return null; return resolved; } catch (err) { log("falling back to bare `claude`:", err.message || err); return null; } } /** * Put a launch command on the clipboard and tell the user it is ready. * Resolves with the command. */ async function copyCommand(options = {}) { const binary = await preferredBinary(); const command = buildCommand(Object.assign({ binary: binary }, options)); try { await nova.clipboard.writeText(command); } catch (err) { warn("could not write to the clipboard:", err); nova.workspace.showErrorMessage( "Could not copy the command. Run this in a terminal:\n\n" + command ); return command; } const request = new NotificationRequest("claudenova-launch"); request.title = options.resume ? "Resume command copied" : "Launch command copied"; request.body = command + "\n\nOpen a terminal and paste it."; request.actions = ["OK", "Show Command"]; try { const response = await nova.notifications.add(request); if (response && response.actionIdx === 1) { nova.workspace.showInformativeMessage(command); } } catch (err) { // Notifications are a courtesy; the clipboard already has the command. log("could not post the launch notification:", err); } return command; } module.exports = { buildCommand, copyCommand, configuredFlags, shellQuote, preferredBinary, };