UNPKG

trigger.dev

Version:

A Command-Line Interface for Trigger.dev projects

287 lines • 14.7 kB
import { formatDurationMilliseconds } from "@trigger.dev/core/v3"; import { DEFAULT_DEV_BRANCH } from "@trigger.dev/core/v3/utils/gitBranch"; import { createTaskMetadataFailedErrorStack, DuplicateTaskIdsError, formatErrorCauses, TaskIndexingImportError, TaskMetadataParseError, taskRunErrorEnhancer, taskRunErrorToString, } from "@trigger.dev/core/v3/errors"; import { TaskRunErrorCodes } from "@trigger.dev/core/v3/schemas"; import { isAgent } from "../utilities/agent.js"; import { aiHelpLink, chalkError, chalkGrey, chalkLink, chalkRun, chalkSuccess, chalkTask, chalkWarning, chalkWorker, cliLink, isLinksSupported, prettyError, prettyPrintDate, prettyWarning, } from "../utilities/cliOutput.js"; import { eventBus } from "../utilities/eventBus.js"; import { logger } from "../utilities/logger.js"; import { formatScheduleWarnings } from "../utilities/scheduleWarnings.js"; import { BundleError } from "../build/bundle.js"; import { analyzeWorker } from "../utilities/analyze.js"; import { SCHEDULE_PLAN_LIMIT_HEADER, SchedulePlanLimitError } from "./errors.js"; export function startDevOutput(options) { const { branch, dashboardUrl, config } = options; const baseUrl = `${dashboardUrl}/projects/v3/${config.project}`; // The version new runs execute on. A failed build leaves the previous worker running. let workerVersion; let printedAgentLinks = false; // A person can follow this link to the dashboard's AI help. An agent already has the error. const askAiForHelp = (query) => { if (!isAgent) { aiHelpLink({ dashboardUrl, project: config.project, query }); } }; const logBuildFailed = (willRebuild) => { const bullet = chalkError("○"); const failed = chalkError("Build failed"); if (!willRebuild) { logger.log(`${bullet} ${failed}${chalkGrey(". Fix the errors above and run dev again.")}`); return; } const runsText = workerVersion ? `, runs still use ${workerVersion}` : ""; logger.log(`${bullet} ${failed}${chalkGrey(`${runsText}. Fix the errors above and it will rebuild automatically.`)}`); }; const rebuildStarted = (...[target]) => { logger.log(chalkGrey("○ Rebuilding local worker…")); }; const buildStarted = (...[target]) => { logger.log(chalkGrey("○ Building local worker…")); }; const buildFailed = (...[target, error]) => { const errorText = error instanceof Error ? error.message : "Unknown error"; const stack = error instanceof Error ? error.stack : undefined; let issues = []; if (error instanceof BundleError) { issues = error.issues?.map((issue) => `${issue.text} (${issue.location?.file})`) ?? []; } askAiForHelp(`Build failed:\n ${errorText}\n${issues.join("\n")}\n${stack}`); // Only the first build emits this, and the dev command exits after it logBuildFailed(false); }; const rebuildFailed = () => { logBuildFailed(true); }; const workerSkipped = () => { logger.log(chalkGrey(workerVersion ? `○ No changes detected, keeping local worker ${workerVersion}` : "○ No changes detected, skipping build…")); }; const backgroundWorkerInitialized = (...[worker]) => { analyzeWorker(worker, options.args.analyze, options.args.disableWarnings); if (!options.args.disableWarnings && worker.warnings.length > 0) { prettyWarning("Schedule policy applied", formatScheduleWarnings(worker.warnings)); } const logParts = []; const testUrl = `${dashboardUrl}/projects/v3/${config.project}/test?environment=dev`; const runsUrl = `${dashboardUrl}/projects/v3/${config.project}/runs?envSlug=dev`; const pipe = chalkGrey("|"); const bullet = chalkGrey("○"); const arrow = chalkGrey("->"); logParts.push(bullet); const testLink = chalkLink(cliLink("Test tasks", testUrl)); const runsLink = chalkLink(cliLink("View runs", runsUrl)); const runtime = chalkGrey(`[${worker.build.runtime}]`); const workerStarted = chalkGrey(`Local worker ready on branch: ${branch ?? DEFAULT_DEV_BRANCH}`); workerVersion = worker.serverWorker.version; logParts.push(workerStarted, runtime, arrow, chalkWorker(workerVersion)); if (isLinksSupported && !isAgent) { logParts.push(pipe, testLink, pipe, runsLink); } logger.log(logParts.join(" ")); if (isAgent) { logAgentWorkerDetails(worker, { testUrl, runsUrl, includeLinks: !printedAgentLinks }); printedAgentLinks = true; } }; const backgroundWorkerIndexingError = (...[buildManifest, error]) => { if (error instanceof TaskIndexingImportError) { let errorText = ""; for (const importError of error.importErrors) { prettyError(`Could not import ${importError.file}`, importError.stack ?? importError.message); errorText += `Could not import ${importError.file}:\n ${importError.stack ?? importError.message}\n`; } askAiForHelp(errorText); } else if (error instanceof TaskMetadataParseError) { const errorStack = createTaskMetadataFailedErrorStack({ version: "v1", zodIssues: error.zodIssues, tasks: error.tasks, }); prettyError(`Could not parse task metadata`, errorStack); askAiForHelp(`Could not parse task metadata:\n ${errorStack}`); } else if (error instanceof DuplicateTaskIdsError) { const body = error.collisions .map(({ id, filePaths }) => { const distinct = Array.from(new Set(filePaths)); return distinct.length === 1 ? `${chalkTask(id)} was defined more than once in ${distinct[0]}` : `${chalkTask(id)} was defined in:\n${distinct.map((f) => ` ${f}`).join("\n")}`; }) .join("\n\n"); prettyError("Duplicate task ids detected", `${body}\n\nTask ids must be unique across your project (including scheduled tasks). Please rename one of them.`, cliLink("View the task docs", "https://trigger.dev/docs/tasks/overview")); askAiForHelp(`Duplicate task ids: ${error.collisions.map((c) => c.id).join(", ")}`); } else if (error instanceof SchedulePlanLimitError) { prettyError(SCHEDULE_PLAN_LIMIT_HEADER, error.message); // The dev command exits after this error, so there's no rebuild to wait for return; } else { const errorText = error instanceof Error ? error.message : "Unknown error"; const stack = error instanceof Error ? error.stack : undefined; prettyError(`Build failed: ${errorText}`, stack); askAiForHelp(`Build failed:\n ${errorText}\n${stack}`); } logBuildFailed(true); }; const runStarted = (...[worker, execution]) => { if (!worker.serverWorker) { return; } // ○ Mar 27 09:17:25.653 -> View logs | 20240326.20 | create-avatar | run_slufhjdfiv8ejnrkw9dsj.1 const logsUrl = `${baseUrl}/runs/${execution.run.id}`; const pipe = chalkGrey("|"); const bullet = chalkGrey("○"); const link = chalkLink(cliLink("View logs", logsUrl)); const timestampPrefix = chalkGrey(prettyPrintDate(execution.attempt.startedAt)); const workerPrefix = chalkWorker(worker.serverWorker.version); const taskPrefix = chalkTask(execution.task.id); const runId = chalkRun(`${execution.run.id}.${execution.attempt.number}`); const logParts = [bullet, timestampPrefix, chalkGrey("->")]; if (isLinksSupported && !isAgent) { logParts.push(link, pipe); } logParts.push(workerPrefix, pipe, taskPrefix, pipe, runId); if (isAgent) { logParts.push(pipe, logsUrl); } logger.log(logParts.join(" ")); }; const runCompleted = (...[worker, execution, completion, durationMs]) => { const retryingText = chalkGrey(!completion.ok && completion.skippedRetrying ? " (retrying skipped)" : !completion.ok && completion.retry !== undefined ? ` (retrying in ${completion.retry.delay}ms)` : ""); const resultText = !completion.ok ? completion.error.type === "INTERNAL_ERROR" && (completion.error.code === TaskRunErrorCodes.TASK_EXECUTION_ABORTED || completion.error.code === TaskRunErrorCodes.TASK_RUN_CANCELLED) ? chalkWarning("Cancelled") : `${chalkError("Error")}${retryingText}` : chalkSuccess("Success"); const errorText = !completion.ok ? formatErrorLog(completion.error) : "retry" in completion ? `retry in ${completion.retry}ms` : ""; const elapsedText = chalkGrey(`(${formatDurationMilliseconds(durationMs, { style: "short" })})`); const timestampPrefix = chalkGrey(prettyPrintDate()); const logsUrl = `${baseUrl}/runs/${execution.run.id}`; const pipe = chalkGrey("|"); const bullet = chalkGrey("○"); const link = chalkLink(cliLink("View logs", logsUrl)); const workerPrefix = chalkWorker(worker.serverWorker.version); const taskPrefix = chalkTask(execution.task.id); const runId = chalkRun(`${execution.run.id}.${execution.attempt.number}`); const logParts = [bullet, timestampPrefix, chalkGrey("->")]; if (isLinksSupported && !isAgent) { logParts.push(link, pipe); } logParts.push(workerPrefix, pipe, taskPrefix, pipe, runId, pipe, resultText); logger.log(`${logParts.join(" ")} ${elapsedText}${errorText}`); if (isAgent && completion.ok) { const output = formatRunOutput(completion.output, completion.outputType); if (output !== undefined) { logger.log(`${chalkGrey(" Output:")} ${output}`); } } }; const socketConnectionDisconnected = (reason) => { logger.log(chalkGrey(`○ Connection was lost: ${reason}`)); }; const socketConnectionReconnected = (reason) => { logger.log(chalkGrey(`○ Connection was restored`)); }; eventBus.on("rebuildStarted", rebuildStarted); eventBus.on("buildStarted", buildStarted); eventBus.on("buildFailed", buildFailed); eventBus.on("rebuildFailed", rebuildFailed); eventBus.on("workerSkipped", workerSkipped); eventBus.on("backgroundWorkerInitialized", backgroundWorkerInitialized); eventBus.on("runStarted", runStarted); eventBus.on("runCompleted", runCompleted); eventBus.on("backgroundWorkerIndexingError", backgroundWorkerIndexingError); eventBus.on("socketConnectionDisconnected", socketConnectionDisconnected); eventBus.on("socketConnectionReconnected", socketConnectionReconnected); return () => { eventBus.off("rebuildStarted", rebuildStarted); eventBus.off("buildStarted", buildStarted); eventBus.off("buildFailed", buildFailed); eventBus.off("rebuildFailed", rebuildFailed); eventBus.off("workerSkipped", workerSkipped); eventBus.off("backgroundWorkerInitialized", backgroundWorkerInitialized); eventBus.off("runStarted", runStarted); eventBus.off("runCompleted", runCompleted); eventBus.off("backgroundWorkerIndexingError", backgroundWorkerIndexingError); eventBus.off("socketConnectionDisconnected", socketConnectionDisconnected); eventBus.off("socketConnectionReconnected", socketConnectionReconnected); }; } const MAX_AGENT_TASK_IDS = 50; const MAX_AGENT_OUTPUT_LENGTH = 1_000; // Agents can't click terminal links or open the dashboard, so they get the task ids and plain URLs function logAgentWorkerDetails(worker, { testUrl, runsUrl, includeLinks }) { const taskIds = worker.manifest?.tasks.map((task) => task.id) ?? []; const shownTaskIds = taskIds.slice(0, MAX_AGENT_TASK_IDS); const hiddenCount = taskIds.length - shownTaskIds.length; logger.log(chalkGrey(` Tasks: ${shownTaskIds.length > 0 ? shownTaskIds.join(", ") : "none"}${hiddenCount > 0 ? `, and ${hiddenCount} more` : ""}`)); if (includeLinks) { logger.log(chalkGrey(` Test tasks: ${testUrl}`)); logger.log(chalkGrey(` View runs: ${runsUrl}`)); } } /** A run's output as one line, or undefined when the run didn't return anything. */ export function formatRunOutput(output, outputType) { if (output === undefined) { return undefined; } if (outputType === "application/store") { return "(too large to show here, view it in the dashboard)"; } let text = output; if (outputType === "application/super+json") { try { // Show the plain JSON part, without superjson's type metadata text = JSON.stringify(JSON.parse(output).json); } catch { // Show the raw output } } if (text.length <= MAX_AGENT_OUTPUT_LENGTH) { return text; } return `${text.slice(0, MAX_AGENT_OUTPUT_LENGTH)}… (${text.length - MAX_AGENT_OUTPUT_LENGTH} more characters)`; } function formatErrorLog(error) { switch (error.type) { case "INTERNAL_ERROR": { // Already labelled "Cancelled" if (error.code === TaskRunErrorCodes.TASK_EXECUTION_ABORTED || error.code === TaskRunErrorCodes.TASK_RUN_CANCELLED) { return ""; } const enhancedError = taskRunErrorEnhancer(error); const link = enhancedError.link ? `\n${enhancedError.link.name}: ${enhancedError.link.href}` : ""; const stackTrace = error.stackTrace ? `\n${error.stackTrace}` : ""; return `\n\n${chalkError("X Error:")} ${taskRunErrorToString(enhancedError)}${link}${stackTrace}\n`; } case "STRING_ERROR": { return `\n\n${chalkError("X Error:")} ${error.raw}\n`; } case "CUSTOM_ERROR": { return `\n\n${chalkError("X Error:")} ${error.raw}\n`; } case "BUILT_IN_ERROR": { const stackTrace = error.stackTrace.replace(/^Error: /, chalkError("X Error: ")); return `\n\n${stackTrace}${formatErrorCauses(error.causes, { stackTrace: true })}\n`; } } } //# sourceMappingURL=devOutput.js.map