trigger.dev
Version:
A Command-Line Interface for Trigger.dev projects
497 lines • 17.4 kB
JavaScript
import { confirm, intro, isCancel, log, multiselect, select } from "@clack/prompts";
import chalk from "chalk";
import { extname } from "node:path";
import { z } from "zod";
import { OutroCommandError, wrapCommandAction } from "../cli/common.js";
import { cliLink } from "../utilities/cliOutput.js";
import { writeConfigHasSeenMCPInstallPrompt } from "../utilities/configFiles.js";
import { expandTilde, safeReadJSONCFile, safeReadTomlFile, writeJSONFile, writeTomlFile, } from "../utilities/fileSystem.js";
import { printStandloneInitialBanner } from "../utilities/initialBanner.js";
import { VERSION } from "../version.js";
const cliVersion = VERSION;
const cliTag = cliVersion.includes("v4-beta") ? "v4-beta" : "latest";
const clients = [
"claude-code",
"cursor",
"vscode",
"zed",
"windsurf",
"gemini-cli",
"crush",
"cline",
"openai-codex",
"opencode",
"amp",
"ruler",
];
const scopes = ["user", "project", "local"];
const clientScopes = {
"claude-code": {
user: "~/.claude.json",
project: "./.mcp.json",
local: "~/.claude.json",
},
cursor: {
user: "~/.cursor/mcp.json",
project: "./.cursor/mcp.json",
},
vscode: {
user: "~/Library/Application Support/Code/User/mcp.json",
project: "./.vscode/mcp.json",
},
zed: {
user: "~/.config/zed/settings.json",
},
windsurf: {
user: "~/.codeium/windsurf/mcp_config.json",
},
"gemini-cli": {
user: "~/.gemini/settings.json",
project: "./.gemini/settings.json",
},
crush: {
user: "~/.config/crush/crush.json",
project: "./crush.json",
local: "./.crush.json",
},
cline: {
user: "~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json",
},
amp: {
user: "~/.config/amp/settings.json",
},
"openai-codex": {
user: "~/.codex/config.toml",
},
opencode: {
user: "~/.config/opencode/opencode.json",
project: "./opencode.json",
},
ruler: {
project: "./.ruler/mcp.json",
},
};
const clientLabels = {
"claude-code": "Claude Code",
cursor: "Cursor",
vscode: "VSCode",
zed: "Zed",
windsurf: "Windsurf",
"gemini-cli": "Gemini CLI",
crush: "Charm Crush",
cline: "Cline",
"openai-codex": "OpenAI Codex CLI",
amp: "Sourcegraph AMP",
opencode: "opencode",
ruler: "Ruler",
};
const InstallMcpCommandOptions = z.object({
projectRef: z.string().optional(),
tag: z.string().default(cliTag),
devOnly: z.boolean().optional(),
yolo: z.boolean().default(false),
scope: z.enum(scopes).optional(),
client: z.enum(clients).array().optional(),
logFile: z.string().optional(),
apiUrl: z.string().optional(),
logLevel: z.enum(["debug", "info", "log", "warn", "error", "none"]).default("log"),
});
export function configureInstallMcpCommand(program) {
return program
.command("install-mcp")
.description("Install the Trigger.dev MCP server")
.option("-p, --project-ref <project ref>", "Scope the mcp server to a specific Trigger.dev project by providing its project ref")
.option("-t, --tag <package tag>", "The version of the trigger.dev CLI package to use for the MCP server", cliTag)
.option("--dev-only", "Restrict the MCP server to the dev environment only")
.option("--yolo", "Install the MCP server into all supported clients")
.option("--scope <scope>", "Choose the scope of the MCP server, either user or project")
.option("--client <clients...>", "Choose the client (or clients) to install the MCP server into. We currently support: " +
clients.join(", "))
.option("--log-file <log file>", "Configure the MCP server to write logs to a file")
.option("-a, --api-url <value>", "Configure the MCP server to specify a custom Trigger.dev API URL")
.option("-l, --log-level <level>", "The CLI log level to use (debug, info, log, warn, error, none). This does not effect the log level of your trigger.dev tasks.", "log")
.action(async (options) => {
await printStandloneInitialBanner(true);
await installMcpCommand(options);
});
}
export async function installMcpCommand(options) {
return await wrapCommandAction("installMcpCommand", InstallMcpCommandOptions, options, async (opts) => {
return await _installMcpCommand(opts);
});
}
async function _installMcpCommand(options) {
intro("Welcome to the Trigger.dev MCP server install wizard 🧙");
await installMcpServer(options);
}
export async function installMcpServer(options) {
const opts = InstallMcpCommandOptions.parse(options);
writeConfigHasSeenMCPInstallPrompt(true);
const devOnly = await resolveDevOnly(opts);
opts.devOnly = devOnly;
const clientNames = await resolveClients(opts);
if (clientNames.length === 1 && clientNames.includes("unsupported")) {
return handleUnsupportedClientOnly(opts);
}
const results = [];
for (const clientName of clientNames) {
const result = await installMcpServerForClient(clientName, opts);
if (result) {
results.push(result);
}
}
if (results.length > 0) {
log.step("Installed to:");
for (const r of results) {
const scopeLabel = `${r.scope.scope}`;
log.message(` • ${r.clientName} (${scopeLabel}) → ${chalk.gray(r.configPath)}`);
}
}
log.info("Next steps:");
log.message(" 1. Restart your MCP client(s) to load the new configuration.");
log.message(' 2. In your client, look for a server named "trigger". It should connect automatically.');
log.message(" 3. Get started with Trigger.dev");
log.message(` Try asking your vibe-coding friend to ${chalk.green("Add trigger.dev to my project")}`);
log.info("More examples:");
log.message(` • ${chalk.green('"Trigger the hello-world task"')}`);
log.message(` • ${chalk.green('"Can you help me debug the prod run run_1234"')}`);
log.message(` • ${chalk.green('"Deploy my trigger project to staging"')}`);
log.message(` • ${chalk.green('"What trigger task handles uploading files to S3?"')}`);
log.message(` • ${chalk.green('"How do I create a scheduled task in Trigger.dev?"')}`);
log.message(` • ${chalk.green('"Search Trigger.dev docs for ffmpeg examples"')}`);
log.info("Helpful links:");
log.message(` • ${cliLink("Trigger.dev docs", "https://trigger.dev/docs")}`);
log.message(` • ${cliLink("MCP docs", "https://trigger.dev/docs/mcp")}`);
log.message(` • Need help? ${cliLink("Join our Discord", "https://trigger.dev/discord")} or email help@trigger.dev`);
return results;
}
function handleUnsupportedClientOnly(options) {
log.info("Manual MCP server configuration");
const args = [`trigger.dev@${options.tag}`, "mcp"];
if (options.logFile) {
args.push("--log-file", options.logFile);
}
if (options.apiUrl) {
args.push("--api-url", options.apiUrl);
}
if (options.devOnly) {
args.push("--dev-only");
}
if (options.projectRef) {
args.push("--project-ref", options.projectRef);
}
if (options.logLevel && options.logLevel !== "log") {
args.push("--log-level", options.logLevel);
}
log.message("Since your client isn't directly supported yet, you'll need to configure it manually:");
log.message("");
log.message(`${chalk.yellow("Command:")} ${chalk.green("npx")}`);
log.message(`${chalk.yellow("Arguments:")} ${chalk.green(args.join(" "))}`);
log.message("");
log.message("Add this MCP server configuration to your client's settings:");
log.message(` • ${chalk.cyan("Server name:")} trigger`);
log.message(` • ${chalk.cyan("Command:")} npx`);
log.message(` • ${chalk.cyan("Args:")} ${args.map((arg) => `"${arg}"`).join(", ")}`);
log.message("");
log.message("Most MCP clients use a JSON configuration format like:");
log.message(chalk.dim(`{
"mcpServers": {
"trigger": {
"command": "npx",
"args": [${args.map((arg) => `"${arg}"`).join(", ")}]
}
}
}`));
return [];
}
async function installMcpServerForClient(clientName, options) {
if (clientName === "unsupported") {
// This should not happen as unsupported clients are handled separately
// but if it does, provide helpful output
log.message(`${chalk.yellow("âš ")} Skipping unsupported client - see manual configuration above`);
return;
}
const scope = await resolveScopeForClient(clientName, options);
// clientSpinner.message(`Installing in ${scope.scope} scope at ${scope.location}`);
const configPath = await performInstallForClient(clientName, scope, options);
// clientSpinner.stop(`Successfully installed in ${clientName} (${configPath})`);
return { configPath, clientName, scope };
}
async function performInstallForClient(clientName, scope, options) {
const config = resolveMcpServerConfig(clientName, options);
const pathComponents = resolveMcpServerConfigJsonPath(clientName, scope);
return await writeMcpServerConfig(scope.location, pathComponents, config);
}
async function writeMcpServerConfig(location, pathComponents, config) {
const fullPath = expandTilde(location);
const extension = extname(fullPath);
switch (extension) {
case ".json": {
let existingConfig = await safeReadJSONCFile(fullPath);
if (!existingConfig) {
existingConfig = {};
}
const newConfig = applyConfigToExistingConfig(existingConfig, pathComponents, config);
await writeJSONFile(fullPath, newConfig, true);
break;
}
case ".toml": {
let existingConfig = await safeReadTomlFile(fullPath);
if (!existingConfig) {
existingConfig = {};
}
const newConfig = applyConfigToExistingConfig(existingConfig, pathComponents, config);
await writeTomlFile(fullPath, newConfig);
break;
}
}
return fullPath;
}
function applyConfigToExistingConfig(existingConfig, pathComponents, config) {
const clonedConfig = structuredClone(existingConfig);
let currentValueAtPath = clonedConfig;
for (let i = 0; i < pathComponents.length; i++) {
const currentPathSegment = pathComponents[i];
if (!currentPathSegment) {
break;
}
if (i === pathComponents.length - 1) {
currentValueAtPath[currentPathSegment] = config;
break;
}
else {
currentValueAtPath[currentPathSegment] = currentValueAtPath[currentPathSegment] || {};
currentValueAtPath = currentValueAtPath[currentPathSegment];
}
}
return clonedConfig;
}
function resolveMcpServerConfigJsonPath(clientName, scope) {
switch (clientName) {
case "cursor": {
return ["mcpServers", "trigger"];
}
case "vscode": {
return ["servers", "trigger"];
}
case "crush": {
return ["mcp", "trigger"];
}
case "windsurf": {
return ["mcpServers", "trigger"];
}
case "gemini-cli": {
return ["mcpServers", "trigger"];
}
case "cline": {
return ["mcpServers", "trigger"];
}
case "amp": {
return ["amp.mcpServers", "trigger"];
}
case "zed": {
return ["context_servers", "trigger"];
}
case "claude-code": {
if (scope.scope === "local") {
const projectPath = process.cwd();
return ["projects", projectPath, "mcpServers", "trigger"];
}
else {
return ["mcpServers", "trigger"];
}
}
case "openai-codex": {
return ["mcp_servers", "trigger"];
}
case "opencode": {
return ["mcp", "trigger"];
}
case "ruler": {
return ["mcpServers", "trigger"];
}
}
}
function resolveMcpServerConfig(clientName, options) {
const args = [`trigger.dev@${options.tag}`, "mcp"];
if (options.logFile) {
args.push("--log-file", options.logFile);
}
if (options.apiUrl) {
args.push("--api-url", options.apiUrl);
}
if (options.devOnly) {
args.push("--dev-only");
}
if (options.projectRef) {
args.push("--project-ref", options.projectRef);
}
switch (clientName) {
case "claude-code": {
return {
command: "npx",
args,
};
}
case "cursor": {
return {
command: "npx",
args,
};
}
case "vscode": {
return {
command: "npx",
args,
};
}
case "crush": {
return {
type: "stdio",
command: "npx",
args,
};
}
case "windsurf": {
return {
command: "npx",
args,
};
}
case "gemini-cli": {
return {
command: "npx",
args,
};
}
case "cline": {
return {
command: "npx",
args,
};
}
case "amp": {
return {
command: "npx",
args,
};
}
case "openai-codex": {
return {
command: "npx",
args,
};
}
case "zed": {
return {
source: "custom",
command: "npx",
args,
};
}
case "opencode": {
return {
type: "local",
command: ["npx", ...args],
enabled: true,
};
}
case "ruler": {
return {
type: "stdio",
command: "npx",
args,
};
}
}
}
async function resolveScopeForClient(clientName, options) {
if (options.scope) {
const location = clientScopes[clientName][options.scope];
if (!location) {
throw new OutroCommandError(`The ${clientName} client does not support the ${options.scope} scope, it only supports ${Object.keys(clientScopes[clientName]).join(", ")} scopes`);
}
return {
scope: options.scope,
location,
};
}
const scopeOptions = resolveScopeOptionsForClient(clientName);
if (scopeOptions.length === 1) {
return {
scope: scopeOptions[0].value.scope,
location: scopeOptions[0].value.location,
};
}
const selectedScope = await select({
message: `Where should the MCP server for ${clientName} be installed?`,
options: scopeOptions,
});
if (isCancel(selectedScope)) {
throw new OutroCommandError("No scope selected");
}
return selectedScope;
}
function resolveScopeOptionsForClient(clientName) {
const $clientScopes = clientScopes[clientName];
const options = Object.entries($clientScopes).map(([scope, location]) => ({
value: { location, scope: scope },
label: scope,
hint: scopeHint(scope, location),
}));
return options;
}
function scopeHint(scope, location) {
switch (scope) {
case "user": {
return `Install for your user account on your machine (${location})`;
}
case "project": {
return `Install in the current project shared with your team (${location})`;
}
case "local": {
return `Install in the current project, local to you only (${location})`;
}
}
}
async function resolveClients(options) {
if (options.client) {
return options.client;
}
if (options.yolo) {
return [...clients];
}
const selectOptions = clients.map((client) => ({
value: client,
label: clientLabels[client],
}));
selectOptions.push({
value: "unsupported",
label: "Unsupported client",
hint: "We don't support this client yet, but you can still install the MCP server manually.",
});
const $selectOptions = selectOptions;
const selectedClients = await multiselect({
message: "Select one or more clients to install the MCP server into",
options: $selectOptions,
required: true,
});
if (isCancel(selectedClients)) {
throw new OutroCommandError("No clients selected");
}
return selectedClients;
}
async function resolveDevOnly(options) {
if (typeof options.devOnly === "boolean") {
return options.devOnly;
}
const devOnly = await confirm({
message: "Restrict the MCP server to the dev environment only?",
initialValue: false,
});
if (isCancel(devOnly)) {
return false;
}
return devOnly;
}
//# sourceMappingURL=install-mcp.js.map