Files
claude-nova/Scripts/launch.js
T

148 lines
4.6 KiB
JavaScript

//
// 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,
};