trigger.dev
Version:
A Command-Line Interface for Trigger.dev projects
381 lines • 15.5 kB
JavaScript
import { confirm, intro, isCancel, log, multiselect, outro } from "@clack/prompts";
import chalk from "chalk";
import { Option as CommandOption } from "commander";
import { join } from "node:path";
import * as semver from "semver";
import { z } from "zod";
import { OutroCommandError, wrapCommandAction } from "../cli/common.js";
import { loadConfig } from "../config.js";
import { GithubRulesManifestLoader, loadRulesManifest, LocalRulesManifestLoader, } from "../rules/manifest.js";
import { cliLink } from "../utilities/cliOutput.js";
import { readConfigHasSeenRulesInstallPrompt, readConfigLastRulesInstallPromptVersion, writeConfigHasSeenRulesInstallPrompt, writeConfigLastRulesInstallPromptVersion, } from "../utilities/configFiles.js";
import { pathExists, readFile, safeWriteFile } from "../utilities/fileSystem.js";
import { printStandloneInitialBanner } from "../utilities/initialBanner.js";
import { logger } from "../utilities/logger.js";
import { tryCatch } from "@trigger.dev/core/utils";
const targets = [
"claude-code",
"cursor",
"vscode",
"windsurf",
"gemini-cli",
"cline",
"agents.md",
"amp",
"kilo",
"ruler",
];
const targetLabels = {
"claude-code": "Claude Code",
cursor: "Cursor",
vscode: "VSCode",
windsurf: "Windsurf",
"gemini-cli": "Gemini CLI",
cline: "Cline",
"agents.md": "AGENTS.md (OpenAI Codex CLI, Jules, OpenCode)",
amp: "Sourcegraph AMP",
kilo: "Kilo Code",
ruler: "Ruler",
};
const InstallRulesCommandOptions = z.object({
target: z.enum(targets).array().optional(),
manifestPath: z.string().optional(),
branch: z.string().optional(),
logLevel: z.enum(["debug", "info", "log", "warn", "error", "none"]).optional(),
forceWizard: z.boolean().optional(),
});
export function configureInstallRulesCommand(program) {
return program
.command("install-rules")
.description("Install the Trigger.dev Agent rules files")
.option("--target <targets...>", "Choose the target (or targets) to install the Trigger.dev rules into. We currently support: " +
targets.join(", "))
.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")
.addOption(new CommandOption("--manifest-path <path>", "The path to the rules manifest file. This is useful if you want to install the rules from a local file.").hideHelp())
.addOption(new CommandOption("--branch <branch>", "The branch to install the rules from, the default is main").hideHelp())
.addOption(new CommandOption("--force-wizard", "Force the rules install wizard to run even if the rules have already been installed.").hideHelp())
.action(async (options) => {
await printStandloneInitialBanner(true);
await installRulesCommand(options);
});
}
export async function installRulesCommand(options) {
return await wrapCommandAction("installRulesCommand", InstallRulesCommandOptions, options, async (opts) => {
if (opts.logLevel) {
logger.loggerLevel = opts.logLevel;
}
return await _installRulesCommand(opts);
});
}
async function _installRulesCommand(options) {
if (options.forceWizard) {
await initiateRulesInstallWizard(options);
return;
}
intro("Welcome to the Trigger.dev Agent rules install wizard ");
const manifestLoader = options.manifestPath
? new LocalRulesManifestLoader(options.manifestPath)
: new GithubRulesManifestLoader(options.branch ?? "main");
const manifest = await loadRulesManifest(manifestLoader);
writeConfigLastRulesInstallPromptVersion(manifest.currentVersion);
writeConfigHasSeenRulesInstallPrompt(true);
await installRules(manifest, options);
outro("You're all set! ");
}
export async function initiateRulesInstallWizard(options) {
const manifestLoader = options.manifestPath
? new LocalRulesManifestLoader(options.manifestPath)
: new GithubRulesManifestLoader(options.branch ?? "main");
const manifest = await loadRulesManifest(manifestLoader);
const hasSeenRulesInstallPrompt = readConfigHasSeenRulesInstallPrompt();
if (!hasSeenRulesInstallPrompt) {
writeConfigHasSeenRulesInstallPrompt(true);
writeConfigLastRulesInstallPromptVersion(manifest.currentVersion);
const installChoice = await confirm({
message: "Would you like to install the Trigger.dev code agent rules?",
initialValue: true,
});
const skipInstall = isCancel(installChoice) || !installChoice;
if (skipInstall) {
return;
}
await installRules(manifest, options);
return;
}
const lastRulesInstallPromptVersion = readConfigLastRulesInstallPromptVersion();
if (!lastRulesInstallPromptVersion) {
writeConfigHasSeenRulesInstallPrompt(true);
writeConfigLastRulesInstallPromptVersion(manifest.currentVersion);
const installChoice = await confirm({
message: `A new version of the trigger.dev agent rules is available (${manifest.currentVersion}). Do you want to install it?`,
initialValue: true,
});
const skipInstall = isCancel(installChoice) || !installChoice;
if (skipInstall) {
return;
}
await installRules(manifest, options);
return;
}
if (semver.gt(manifest.currentVersion, lastRulesInstallPromptVersion)) {
writeConfigHasSeenRulesInstallPrompt(true);
writeConfigLastRulesInstallPromptVersion(manifest.currentVersion);
const confirmed = await confirm({
message: `A new version of the trigger.dev agent rules is available (${lastRulesInstallPromptVersion} → ${chalk.greenBright(manifest.currentVersion)}). Do you want to install it?`,
initialValue: true,
});
if (isCancel(confirmed) || !confirmed) {
return;
}
await installRules(manifest, options);
}
return;
}
async function installRules(manifest, opts) {
const [_, config] = await tryCatch(loadConfig({
cwd: process.cwd(),
}));
const currentVersion = await manifest.getCurrentVersion();
const targetNames = await resolveTargets(opts);
if (targetNames.length === 1 && targetNames.includes("unsupported")) {
handleUnsupportedTargetOnly(opts);
return;
}
const results = [];
for (const targetName of targetNames) {
const result = await installRulesForTarget(targetName, currentVersion, opts, config ?? undefined);
if (result) {
results.push(result);
}
}
if (results.length > 0) {
log.step("Installed the following rules files:");
for (const r of results) {
const installationsByLocation = r.installations.reduce((acc, i) => {
if (!acc[i.location]) {
acc[i.location] = [];
}
acc[i.location].push(i.option);
return acc;
}, {});
const locationOutput = Object.entries(installationsByLocation).map(([location]) => `${chalk.greenBright(location)}`);
for (const message of locationOutput) {
log.info(message);
}
}
log.info(`${cliLink("Learn how to use our rules", "https://trigger.dev/docs/agents/rules/overview")}`);
}
}
function handleUnsupportedTargetOnly(options) {
log.info(`${cliLink("Install the rules manually", "https://trigger.dev/docs/agents/rules/overview")}`);
return [];
}
async function installRulesForTarget(targetName, currentVersion, options, config) {
if (targetName === "unsupported") {
// This should not happen as unsupported targets are handled separately
// but if it does, provide helpful output
log.message(`${chalk.yellow("⚠")} Skipping unsupported target - see manual configuration above`);
return;
}
const result = await performInstallForTarget(targetName, currentVersion, options, config);
return result;
}
async function performInstallForTarget(targetName, currentVersion, cmdOptions, config) {
const options = await resolveOptionsForTarget(targetName, currentVersion, cmdOptions);
const installations = await performInstallOptionsForTarget(targetName, options, config);
return {
targetName,
installations,
};
}
async function performInstallOptionsForTarget(targetName, options, config) {
const results = [];
for (const option of options) {
const result = await performInstallOptionForTarget(targetName, option, config);
results.push(result);
}
return results;
}
async function performInstallOptionForTarget(targetName, option, config) {
switch (option.installStrategy) {
case "default": {
return performInstallDefaultOptionForTarget(targetName, option, config);
}
case "claude-code-subagent": {
return performInstallClaudeCodeSubagentOptionForTarget(option);
}
default: {
throw new Error(`Unknown install strategy: ${option.installStrategy}`);
}
}
}
async function performInstallDefaultOptionForTarget(targetName, option, config) {
// Get the path to the rules file
const rulesFilePath = resolveRulesFilePathForTargetOption(targetName, option);
const rulesFileContents = await resolveRulesFileContentsForTarget(targetName, option, config);
const mergeStrategy = await resolveRulesFileMergeStrategyForTarget(targetName);
// Try and read the existing rules file
const rulesFileAbsolutePath = join(process.cwd(), rulesFilePath);
await writeToFile(rulesFileAbsolutePath, rulesFileContents, mergeStrategy, option.name);
return { option, location: rulesFilePath };
}
async function writeToFile(path, contents, mergeStrategy = "overwrite", sectionName) {
const exists = await pathExists(path);
if (exists) {
switch (mergeStrategy) {
case "overwrite": {
await safeWriteFile(path, contents);
break;
}
case "replace": {
const existingContents = await readFile(path);
const pattern = new RegExp(`<!-- TRIGGER.DEV ${sectionName} START -->.*?<!-- TRIGGER.DEV ${sectionName} END -->`, "gs");
// If the section name is not found, just append the new content
if (!pattern.test(existingContents)) {
await safeWriteFile(path, existingContents + "\n\n" + contents);
break;
}
const updatedContent = existingContents.replace(pattern, contents);
await safeWriteFile(path, updatedContent);
break;
}
default: {
throw new Error(`Unknown merge strategy: ${mergeStrategy}`);
}
}
}
else {
await safeWriteFile(path, contents);
}
}
async function performInstallClaudeCodeSubagentOptionForTarget(option) {
const rulesFilePath = ".claude/agents/trigger-dev-task-writer.md";
const rulesFileContents = option.contents;
await writeToFile(rulesFilePath, rulesFileContents, "overwrite", option.name);
return { option, location: rulesFilePath };
}
function resolveRulesFilePathForTargetOption(targetName, option) {
if (option.installStrategy === "claude-code-subagent") {
return ".claude/agents/trigger-dev-task-writer.md";
}
switch (targetName) {
case "claude-code": {
return "CLAUDE.md";
}
case "cursor": {
return `.cursor/rules/trigger.${option.name}.mdc`;
}
case "vscode": {
return `.github/instructions/trigger-${option.name}.instructions.md`;
}
case "windsurf": {
return `.windsurf/rules/trigger-${option.name}.md`;
}
case "gemini-cli": {
return `GEMINI.md`;
}
case "cline": {
return `.clinerules/trigger-${option.name}.md`;
}
case "agents.md": {
return "AGENTS.md";
}
case "amp": {
return "AGENT.md";
}
case "kilo": {
return `.kilocode/rules/trigger-${option.name}.md`;
}
case "ruler": {
return `.ruler/trigger-${option.name}.md`;
}
default: {
throw new Error(`Unknown target: ${targetName}`);
}
}
}
async function resolveRulesFileMergeStrategyForTarget(targetName) {
switch (targetName) {
case "amp":
case "agents.md":
case "gemini-cli":
case "claude-code": {
return "replace";
}
default: {
return "overwrite";
}
}
}
async function resolveRulesFileContentsForTarget(targetName, option, config) {
switch (targetName) {
case "cursor": {
return $output(frontmatter({
description: option.label,
globs: option.applyTo ?? "**/trigger/**/*.ts",
alwaysApply: false,
}), option.contents);
}
case "vscode": {
return $output(frontmatter({
applyTo: option.applyTo ?? "**/trigger/**/*.ts",
}), option.contents);
}
case "windsurf": {
return $output(frontmatter({
trigger: "glob",
globs: option.applyTo ?? "**/trigger/**/*.ts",
}), option.contents);
}
default: {
return $output(`<!-- TRIGGER.DEV ${option.name} START -->`, option.contents, `<!-- TRIGGER.DEV ${option.name} END -->`);
}
}
}
function frontmatter(data) {
return $output("---", ...Object.entries(data).map(([key, value]) => `${key}: ${value}`), "---");
}
function $output(...strings) {
return strings.map((s) => s).join("\n");
}
async function resolveOptionsForTarget(targetName, currentVersion, cmdOptions) {
const possibleOptions = currentVersion.options.filter((option) => !option.client || option.client === targetName);
const selectedOptions = await multiselect({
message: `Choose the rules you want to install for ${targetLabels[targetName]}`,
options: possibleOptions.map((option) => ({
value: option,
label: option.title,
hint: `${option.label} [~${option.tokens} tokens]`,
})),
required: true,
});
if (isCancel(selectedOptions)) {
throw new OutroCommandError("No options selected");
}
return selectedOptions;
}
async function resolveTargets(options) {
if (options.target) {
return options.target;
}
const selectOptions = targets.map((target) => ({
value: target,
label: targetLabels[target],
}));
selectOptions.push({
value: "unsupported",
label: "Unsupported target",
hint: "We don't support this target yet, but you can still install the rules manually.",
});
const $selectOptions = selectOptions;
const selectedTargets = await multiselect({
message: "Select one or more targets to install the rules into",
options: $selectOptions,
required: true,
});
if (isCancel(selectedTargets)) {
throw new OutroCommandError("No targets selected");
}
return selectedTargets;
}
//# sourceMappingURL=install-rules.js.map