Skip to content

Commit 349adc4

Browse files
committed
Added an AI help link to the CLI
1 parent 284ef26 commit 349adc4

File tree

11 files changed

+326
-204
lines changed

11 files changed

+326
-204
lines changed

.changeset/red-chairs-begin.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"trigger.dev": patch
3+
---
4+
5+
Added AI assistance link when you have build errors
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { type LoaderFunctionArgs, redirect } from "@remix-run/server-runtime";
2+
import { z } from "zod";
3+
import { prisma } from "~/db.server";
4+
import { env } from "~/env.server";
5+
import { logger } from "~/services/logger.server";
6+
import { requireUserId } from "~/services/session.server";
7+
import { v3EnvironmentPath, v3ProjectPath, v3TestPath } from "~/utils/pathBuilder";
8+
9+
const ParamsSchema = z.object({
10+
projectRef: z.string(),
11+
});
12+
13+
export async function loader({ params, request }: LoaderFunctionArgs) {
14+
const userId = await requireUserId(request);
15+
16+
const validatedParams = ParamsSchema.parse(params);
17+
18+
const project = await prisma.project.findFirst({
19+
where: {
20+
externalRef: validatedParams.projectRef,
21+
organization: {
22+
members: {
23+
some: {
24+
userId,
25+
},
26+
},
27+
},
28+
},
29+
include: {
30+
organization: true,
31+
},
32+
});
33+
34+
if (!project) {
35+
return new Response("Not found", { status: 404 });
36+
}
37+
38+
const url = new URL(request.url);
39+
const query = url.searchParams.get("q");
40+
41+
if (!query) {
42+
return new Response("No query", { status: 404 });
43+
}
44+
45+
const newUrl = new URL(
46+
v3EnvironmentPath({ slug: project.organization.slug }, { slug: project.slug }, { slug: "dev" }),
47+
env.LOGIN_ORIGIN
48+
);
49+
newUrl.searchParams.set("aiHelp", query);
50+
51+
return redirect(newUrl.toString());
52+
}

packages/cli-v3/src/build/bundle.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,15 @@ export type BundleResult = {
5151
stop: (() => Promise<void>) | undefined;
5252
};
5353

54+
export class BundleError extends Error {
55+
constructor(
56+
message: string,
57+
public readonly issues?: esbuild.Message[]
58+
) {
59+
super(message);
60+
}
61+
}
62+
5463
export async function bundleWorker(options: BundleOptions): Promise<BundleResult> {
5564
const { resolvedConfig } = options;
5665

@@ -129,7 +138,7 @@ export async function bundleWorker(options: BundleOptions): Promise<BundleResult
129138
await currentContext.watch();
130139
result = await initialBuildResultPromise;
131140
if (result.errors.length > 0) {
132-
throw new Error("Failed to build");
141+
throw new BundleError("Failed to build", result.errors);
133142
}
134143

135144
stop = async function () {

packages/cli-v3/src/cli/common.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { outro } from "@clack/prompts";
99
import { chalkError } from "../utilities/cliOutput.js";
1010
import { CLOUD_API_URL } from "../consts.js";
1111
import { readAuthConfigCurrentProfileName } from "../utilities/configFiles.js";
12+
import { BundleError } from "../build/bundle.js";
1213

1314
export const CommonCommandOptions = z.object({
1415
apiUrl: z.string().optional(),
@@ -54,7 +55,7 @@ export async function wrapCommandAction<T extends z.AnyZodObject, TResult>(
5455
schema: T,
5556
options: unknown,
5657
action: (opts: z.output<T>) => Promise<TResult>
57-
): Promise<TResult> {
58+
): Promise<TResult | undefined> {
5859
return await tracer.startActiveSpan(name, async (span) => {
5960
try {
6061
const parsedOptions = schema.safeParse(options);
@@ -86,6 +87,8 @@ export async function wrapCommandAction<T extends z.AnyZodObject, TResult>(
8687
outro("Operation cancelled");
8788
} else if (e instanceof SkipCommandError) {
8889
// do nothing
90+
} else if (e instanceof BundleError) {
91+
process.exit();
8992
} else {
9093
recordSpanException(span, e);
9194

packages/cli-v3/src/commands/dev.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { ResolvedConfig } from "@trigger.dev/core/v3/build";
22
import { Command } from "commander";
33
import { z } from "zod";
4-
import { CommonCommandOptions, commonOptions, wrapCommandAction } from "../cli/common.js";
4+
import {
5+
CommonCommandOptions,
6+
commonOptions,
7+
SkipLoggingError,
8+
wrapCommandAction,
9+
} from "../cli/common.js";
510
import { watchConfig } from "../config.js";
611
import { DevSessionInstance, startDevSession } from "../dev/devSession.js";
712
import { chalkError } from "../utilities/cliOutput.js";
@@ -12,6 +17,7 @@ import { getProjectClient, LoginResultOk } from "../utilities/session.js";
1217
import { login } from "./login.js";
1318
import { updateTriggerPackages } from "./update.js";
1419
import { createLockFile } from "../dev/lock.js";
20+
import { BundleError } from "../build/bundle.js";
1521

1622
const DevCommandOptions = CommonCommandOptions.extend({
1723
debugOtel: z.boolean().default(false),

packages/cli-v3/src/dev/devOutput.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
import { TaskRunError, TaskRunErrorCodes } from "@trigger.dev/core/v3/schemas";
99
import { DevCommandOptions } from "../commands/dev.js";
1010
import {
11+
aiHelpLink,
1112
chalkError,
1213
chalkGrey,
1314
chalkLink,
@@ -24,6 +25,7 @@ import {
2425
import { eventBus, EventBusEventArgs } from "../utilities/eventBus.js";
2526
import { logger } from "../utilities/logger.js";
2627
import { Socket } from "socket.io-client";
28+
import { BundleError } from "../build/bundle.js";
2729

2830
export type DevOutputOptions = {
2931
name: string | undefined;
@@ -45,6 +47,23 @@ export function startDevOutput(options: DevOutputOptions) {
4547
logger.log(chalkGrey("○ Building background worker…"));
4648
};
4749

50+
const buildFailed = (...[target, error]: EventBusEventArgs<"buildFailed">) => {
51+
const errorText = error instanceof Error ? error.message : "Unknown error";
52+
const stack = error instanceof Error ? error.stack : undefined;
53+
54+
let issues: string[] = [];
55+
56+
if (error instanceof BundleError) {
57+
issues = error.issues?.map((issue) => `${issue.text} (${issue.location?.file})`) ?? [];
58+
}
59+
60+
aiHelpLink({
61+
dashboardUrl,
62+
project: config.project,
63+
query: `Build failed:\n ${errorText}\n${issues.join("\n")}\n${stack}`,
64+
});
65+
};
66+
4867
const workerSkipped = () => {
4968
logger.log(chalkGrey("○ No changes detected, skipping build…"));
5069
};
@@ -83,12 +102,18 @@ export function startDevOutput(options: DevOutputOptions) {
83102
...[buildManifest, error]: EventBusEventArgs<"backgroundWorkerIndexingError">
84103
) => {
85104
if (error instanceof TaskIndexingImportError) {
105+
let errorText = "";
86106
for (const importError of error.importErrors) {
87107
prettyError(
88108
`Could not import ${importError.file}`,
89109
importError.stack ?? importError.message
90110
);
111+
errorText += `Could not import ${importError.file}:\n ${
112+
importError.stack ?? importError.message
113+
}\n`;
91114
}
115+
116+
aiHelpLink({ dashboardUrl, project: config.project, query: errorText });
92117
} else if (error instanceof TaskMetadataParseError) {
93118
const errorStack = createTaskMetadataFailedErrorStack({
94119
version: "v1",
@@ -97,11 +122,21 @@ export function startDevOutput(options: DevOutputOptions) {
97122
});
98123

99124
prettyError(`Could not parse task metadata`, errorStack);
125+
aiHelpLink({
126+
dashboardUrl,
127+
project: config.project,
128+
query: `Could not parse task metadata:\n ${errorStack}`,
129+
});
100130
} else {
101131
const errorText = error instanceof Error ? error.message : "Unknown error";
102132
const stack = error instanceof Error ? error.stack : undefined;
103133

104134
prettyError(`Build failed: ${errorText}`, stack);
135+
aiHelpLink({
136+
dashboardUrl,
137+
project: config.project,
138+
query: `Build failed:\n ${errorText}\n${stack}`,
139+
});
105140
}
106141
};
107142

@@ -184,6 +219,7 @@ export function startDevOutput(options: DevOutputOptions) {
184219

185220
eventBus.on("rebuildStarted", rebuildStarted);
186221
eventBus.on("buildStarted", buildStarted);
222+
eventBus.on("buildFailed", buildFailed);
187223
eventBus.on("workerSkipped", workerSkipped);
188224
eventBus.on("backgroundWorkerInitialized", backgroundWorkerInitialized);
189225
eventBus.on("runStarted", runStarted);
@@ -195,6 +231,7 @@ export function startDevOutput(options: DevOutputOptions) {
195231
return () => {
196232
eventBus.off("rebuildStarted", rebuildStarted);
197233
eventBus.off("buildStarted", buildStarted);
234+
eventBus.off("buildFailed", buildFailed);
198235
eventBus.off("workerSkipped", workerSkipped);
199236
eventBus.off("backgroundWorkerInitialized", backgroundWorkerInitialized);
200237
eventBus.off("runStarted", runStarted);

packages/cli-v3/src/dev/devSession.ts

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { clearTmpDirs, EphemeralDirectory, getTmpDir } from "../utilities/tempDi
2424
import { startDevOutput } from "./devOutput.js";
2525
import { startWorkerRuntime } from "./devSupervisor.js";
2626
import { startMcpServer, stopMcpServer } from "./mcpServer.js";
27+
import { aiHelpLink } from "../utilities/cliOutput.js";
2728

2829
export type DevSessionOptions = {
2930
name: string | undefined;
@@ -172,24 +173,34 @@ export async function startDevSession({
172173
async function runBundle() {
173174
eventBus.emit("buildStarted", "dev");
174175

175-
// Use glob to find initial entryPoints
176-
// Use chokidar to watch for entryPoints changes (e.g. added or removed?)
177-
// When there is a change, update entryPoints and start a new build with watch: true
178-
const bundleResult = await bundleWorker({
179-
target: "dev",
180-
cwd: rawConfig.workingDir,
181-
destination: destination.path,
182-
watch: true,
183-
resolvedConfig: rawConfig,
184-
plugins: [...pluginsFromExtensions, onEnd],
185-
jsxFactory: rawConfig.build.jsx.factory,
186-
jsxFragment: rawConfig.build.jsx.fragment,
187-
jsxAutomatic: rawConfig.build.jsx.automatic,
188-
});
176+
try {
177+
// Use glob to find initial entryPoints
178+
// Use chokidar to watch for entryPoints changes (e.g. added or removed?)
179+
// When there is a change, update entryPoints and start a new build with watch: true
180+
const bundleResult = await bundleWorker({
181+
target: "dev",
182+
cwd: rawConfig.workingDir,
183+
destination: destination.path,
184+
watch: true,
185+
resolvedConfig: rawConfig,
186+
plugins: [...pluginsFromExtensions, onEnd],
187+
jsxFactory: rawConfig.build.jsx.factory,
188+
jsxFragment: rawConfig.build.jsx.fragment,
189+
jsxAutomatic: rawConfig.build.jsx.automatic,
190+
});
189191

190-
await updateBundle(bundleResult);
192+
await updateBundle(bundleResult);
191193

192-
return bundleResult.stop;
194+
return bundleResult.stop;
195+
} catch (error) {
196+
if (error instanceof Error) {
197+
eventBus.emit("buildFailed", "dev", error);
198+
} else {
199+
eventBus.emit("buildFailed", "dev", new Error(String(error)));
200+
}
201+
202+
throw error;
203+
}
193204
}
194205

195206
const stopBundling = await runBundle();

packages/cli-v3/src/utilities/cliOutput.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,27 @@ export function prettyWarning(header: string, body?: string, footer?: string) {
116116
);
117117
}
118118

119+
export function aiHelpLink({
120+
dashboardUrl,
121+
project,
122+
query,
123+
}: {
124+
dashboardUrl: string;
125+
project: string;
126+
query: string;
127+
}) {
128+
const searchParams = new URLSearchParams();
129+
130+
//the max length for a URL is 1950 characters
131+
const clippedQuery = query.slice(0, 1950);
132+
133+
searchParams.set("q", clippedQuery);
134+
const url = new URL(`/projects/${project}/ai-help`, dashboardUrl);
135+
url.search = searchParams.toString();
136+
137+
log.message(chalkLink(cliLink("💡 Get a fix for this error using AI", url.toString())));
138+
}
139+
119140
export function cliLink(text: string, url: string, options?: TerminalLinkOptions) {
120141
return terminalLink(text, url, {
121142
fallback: (text, url) => `${text} ${url}`,

packages/cli-v3/src/utilities/eventBus.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Socket } from "socket.io-client";
1111
export type EventBusEvents = {
1212
rebuildStarted: [BuildTarget];
1313
buildStarted: [BuildTarget];
14+
buildFailed: [BuildTarget, Error];
1415
workerSkipped: [];
1516
backgroundWorkerInitialized: [BackgroundWorker];
1617
backgroundWorkerIndexingError: [BuildManifest, Error];

0 commit comments

Comments
 (0)