trigger.dev
Version:
A Command-Line Interface for Trigger.dev projects
303 lines • 14.3 kB
JavaScript
import { intro } from "@clack/prompts";
import { resolve } from "node:path";
import { spinner } from "../utilities/windows.js";
import { loadConfig } from "../config.js";
import { verifyDirectory } from "./deploy.js";
import { Option as CommandOption } from "commander";
import { z } from "zod";
import { CliApiClient } from "../apiClient.js";
import { CommonCommandOptions, commonOptions, handleTelemetry, wrapCommandAction, } from "../cli/common.js";
import { watchConfig } from "../config.js";
import { startDevSession } from "../dev/devSession.js";
import { createLockFile } from "../dev/lock.js";
import { chalkError } from "../utilities/cliOutput.js";
import { resolveLocalEnvVars } from "../utilities/localEnvVars.js";
import { printDevBanner, printStandloneInitialBanner } from "../utilities/initialBanner.js";
import { logger } from "../utilities/logger.js";
import { awaitAndDisplayPlatformNotification, fetchPlatformNotification, } from "../utilities/platformNotifications.js";
import { runtimeChecks } from "../utilities/runtimeCheck.js";
import { getProjectClient } from "../utilities/session.js";
import { login } from "./login.js";
import { updateTriggerPackages } from "./update.js";
import { readConfigHasSeenMCPInstallPrompt, writeConfigHasSeenMCPInstallPrompt, } from "../utilities/configFiles.js";
import { confirm, isCancel, log } from "@clack/prompts";
import { installMcpServer } from "./install-mcp.js";
import { tryCatch } from "@trigger.dev/core/utils";
import { VERSION } from "@trigger.dev/core";
import { initiateSkillsInstallWizard } from "./skills.js";
import { getDevBranch } from "@trigger.dev/core/v3";
const DevArchiveCommandOptions = CommonCommandOptions.extend({
branch: z.string().optional(),
config: z.string().optional(),
projectRef: z.string().optional(),
skipUpdateCheck: z.boolean().default(false),
});
const DevCommandOptions = CommonCommandOptions.extend({
debugOtel: z.boolean().default(false),
config: z.string().optional(),
projectRef: z.string().optional(),
branch: z.string().optional(),
skipUpdateCheck: z.boolean().default(false),
skipPlatformNotifications: z.boolean().default(false),
envFile: z.string().optional(),
keepTmpFiles: z.boolean().default(false),
maxConcurrentRuns: z.coerce.number().optional(),
mcp: z.boolean().default(false),
mcpPort: z.coerce.number().optional().default(3333),
analyze: z.boolean().default(false),
disableWarnings: z.boolean().default(false),
skipMCPInstall: z.boolean().default(false),
skipRulesInstall: z.boolean().default(false),
});
export function configureDevCommand(program) {
// `dev` is the root command that defaults to the `start` subcommand,
// maintains existing behaviour for `trigger dev` but `trigger dev --help` a bit different
const devBase = program.command("dev").description("Run your Trigger.dev tasks locally");
commonOptions(devBase
.command("start", { isDefault: true })
.description("Run your Trigger.dev tasks locally")
.option("-c, --config <config file>", "The name of the config file")
.option("-p, --project-ref <project ref>", "The project ref. Required if there is no config file.")
.option("-b, --branch <branch>", "The dev branch to use. If not provided, we'll use the default branch.")
.option("--env-file <env file>", "Path to the .env file to use for the dev session. Defaults to .env in the project directory.")
.option("--max-concurrent-runs <max concurrent runs>", "The maximum number of concurrent runs to allow in the dev session")
.option("--debug-otel", "Enable OpenTelemetry debugging")
.option("--skip-update-check", "Skip checking for @trigger.dev package updates")
.option("--keep-tmp-files", "Keep temporary files after the dev session ends, helpful for debugging")
.option("--mcp", "Start the MCP server")
.option("--mcp-port", "The port to run the MCP server on", "3333")
.addOption(new CommandOption("--analyze", "Analyze the build output and import timings").hideHelp())
.addOption(new CommandOption("--skip-mcp-install", "Skip the Trigger.dev MCP server install wizard").hideHelp())
.addOption(new CommandOption("--skip-rules-install", "Skip the Trigger.dev agent skills install wizard").hideHelp())
.addOption(new CommandOption("--disable-warnings", "Suppress warnings output").hideHelp())
.addOption(new CommandOption("--skip-platform-notifications", "Skip showing platform notifications").hideHelp())).action(async (options) => {
wrapCommandAction("dev", DevCommandOptions, options, async (opts) => {
await devCommand(opts);
});
});
commonOptions(devBase
.command("archive")
.description("Archive a dev branch")
.argument("[path]", "The path to the project", ".")
.option("-b, --branch <branch>", "The dev branch to archive. Defaults to the TRIGGER_DEV_BRANCH environment variable if set.")
.option("--skip-update-check", "Skip checking for @trigger.dev package updates")
.option("-c, --config <config file>", "The name of the config file, found at [path]")
.option("-p, --project-ref <project ref>", "The project ref. Required if there is no config file. This will override the project specified in the config file.")
.option("--env-file <env file>", "Path to the .env file to load into the CLI process. Defaults to .env in the project directory.")).action(async (path, options) => {
await handleTelemetry(async () => {
await printStandloneInitialBanner(true, options.profile);
await devArchiveCommand(path, options);
});
});
}
export async function devCommand(options) {
runtimeChecks();
// Only show these install prompts if the user is in a terminal (not in a Coding Agent)
if (process.stdout.isTTY) {
const skipMCPInstall = typeof options.skipMCPInstall === "boolean" && options.skipMCPInstall;
if (!skipMCPInstall) {
const hasSeenMCPInstallPrompt = readConfigHasSeenMCPInstallPrompt();
if (!hasSeenMCPInstallPrompt) {
const installChoice = await confirm({
message: "Would you like to install the Trigger.dev MCP server?",
initialValue: true,
});
writeConfigHasSeenMCPInstallPrompt(true);
const skipInstall = isCancel(installChoice) || !installChoice;
if (!skipInstall) {
log.step("Welcome to the Trigger.dev MCP server install wizard 🧙");
const [installError] = await tryCatch(installMcpServer({
yolo: false,
tag: VERSION,
logLevel: options.logLevel,
}));
if (installError) {
log.error(`Failed to install MCP server: ${installError.message}`);
}
}
}
}
const skipRulesInstall = typeof options.skipRulesInstall === "boolean" && options.skipRulesInstall;
if (!skipRulesInstall) {
await tryCatch(initiateSkillsInstallWizard({}));
}
}
const authorization = await login({
embedded: true,
silent: true,
defaultApiUrl: options.apiUrl,
profile: options.profile,
});
if (!authorization.ok) {
if (authorization.error === "fetch failed") {
logger.log(`${chalkError("X Error:")} Connecting to the server failed. Please check your internet connection or contact eric@trigger.dev for help.`);
}
else {
logger.log(`${chalkError("X Error:")} You must login first. Use the \`login\` CLI command.\n\n${authorization.error}`);
}
process.exitCode = 1;
return;
}
let watcher;
try {
const devInstance = await startDev({ ...options, cwd: process.cwd(), login: authorization });
watcher = devInstance.watcher;
await devInstance.waitUntilExit();
}
finally {
await watcher?.stop();
}
}
async function startDev(options) {
logger.debug("Starting dev CLI", { options });
let watcher;
let removeLockFile;
try {
if (options.logLevel) {
logger.loggerLevel = options.logLevel;
}
const apiClient = new CliApiClient(options.login.auth.apiUrl, options.login.auth.accessToken);
const notificationPromise = options.skipPlatformNotifications
? undefined
: fetchPlatformNotification({
apiClient,
projectRef: options.projectRef,
});
await printStandloneInitialBanner(true, options.profile);
await awaitAndDisplayPlatformNotification(notificationPromise);
let displayedUpdateMessage = false;
if (!options.skipUpdateCheck) {
displayedUpdateMessage = await updateTriggerPackages(options.cwd, { ...options }, true, true);
}
const envVars = resolveLocalEnvVars(options.envFile);
const branch = getDevBranch({ specified: options.branch ?? envVars.TRIGGER_DEV_BRANCH });
removeLockFile = await createLockFile(options.cwd, branch);
let devInstance;
printDevBanner(displayedUpdateMessage);
if (envVars.TRIGGER_PROJECT_REF) {
logger.debug("Using project ref from env", { ref: envVars.TRIGGER_PROJECT_REF });
}
watcher = await watchConfig({
cwd: options.cwd,
async onUpdate(config) {
logger.debug("Updated config, rerendering", { config });
if (devInstance) {
devInstance.stop();
}
devInstance = await bootDevSession(config);
},
overrides: {
project: options.projectRef ?? envVars.TRIGGER_PROJECT_REF,
},
configFile: options.config,
});
logger.debug("Initial config", watcher.config);
if (branch) {
const upsertResult = await apiClient.upsertBranch(watcher.config.project, {
branch,
env: "development",
});
if (!upsertResult.success) {
logger.error(`Failed to use branch "${branch}": ${upsertResult.error}`);
process.exit(1);
}
}
// eslint-disable-next-line no-inner-declarations
async function bootDevSession(configParam) {
const projectClient = await getProjectClient({
accessToken: options.login.auth.accessToken,
apiUrl: options.login.auth.apiUrl,
projectRef: configParam.project,
env: "dev",
branch,
profile: options.profile,
});
if (!projectClient) {
process.exit(1);
}
return startDevSession({
name: projectClient.name,
branch,
rawArgs: options,
rawConfig: configParam,
client: projectClient.client,
initialMode: "local",
dashboardUrl: options.login.dashboardUrl,
showInteractiveDevSession: true,
keepTmpFiles: options.keepTmpFiles,
});
}
devInstance = await bootDevSession(watcher.config);
const waitUntilExit = async () => { };
return {
watcher,
stop: async () => {
devInstance?.stop();
await watcher?.stop();
removeLockFile?.();
},
waitUntilExit,
};
}
catch (error) {
removeLockFile?.();
await watcher?.stop();
throw error;
}
}
async function devArchiveCommand(dir, options) {
return await wrapCommandAction("devArchiveCommand", DevArchiveCommandOptions, options, async (opts) => {
return await archiveDevBranchCommand(dir, opts);
});
}
async function archiveDevBranchCommand(dir, options) {
intro(`Archiving dev branch`);
if (!options.skipUpdateCheck) {
await updateTriggerPackages(dir, { ...options }, true, true);
}
const cwd = process.cwd();
const projectPath = resolve(cwd, dir);
verifyDirectory(dir, projectPath);
const authorization = await login({
embedded: true,
defaultApiUrl: options.apiUrl,
profile: options.profile,
});
if (!authorization.ok) {
if (authorization.error === "fetch failed") {
throw new Error(`Failed to connect to ${authorization.auth?.apiUrl}. Are you sure it's the correct URL?`);
}
else {
throw new Error(`You must login first. Use the \`login\` CLI command.\n\n${authorization.error}`);
}
}
const resolvedConfig = await loadConfig({
cwd: projectPath,
overrides: { project: options.projectRef },
configFile: options.config,
});
logger.debug("Resolved config", resolvedConfig);
const branch = getDevBranch({ specified: options.branch });
// getDevBranch returns undefined for the default branch (the root dev env),
// which can't be archived. Require the user to name a real branch instead.
if (!branch) {
throw new Error("You need to specify which dev branch to archive (the default branch can't be archived). Use --branch <branch>.");
}
const $buildSpinner = spinner();
$buildSpinner.start(`Archiving "${branch}"`);
const result = await archiveDevBranch(authorization, branch, resolvedConfig.project);
$buildSpinner.stop(result ? `Successfully archived "${branch}"` : `Failed to archive "${branch}".`);
return result;
}
async function archiveDevBranch(authorization, branch, project) {
const apiClient = new CliApiClient(authorization.auth.apiUrl, authorization.auth.accessToken);
const result = await apiClient.archiveBranch(project, "development", branch);
if (result.success) {
return true;
}
else {
logger.error(result.error);
return false;
}
}
//# sourceMappingURL=dev.js.map