v3: cli init command + CLI telemetry (#948)

* Adding telemetry to the deploy command

* Add ability to opt-out of telemetry in the CLI

* Remove log

* Unifying some telemetry stuff and adding it to the login command

* init command

* Remove extra trigger dirs

* Couple tweaks to the init CLI

* Don’t use npm init -y to create the package.json file
This commit is contained in:
Eric Allam
2024-03-18 10:12:11 +00:00
committed by GitHub
parent a5d83971aa
commit 52c364d691
30 changed files with 2137 additions and 761 deletions
@@ -54,6 +54,8 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
throw new Response(null, { status: 404, statusText: "Organization not found" });
}
const url = new URL(request.url);
return typedjson({
organization: {
id: organization.id,
@@ -61,6 +63,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
slug: organizationSlug,
projectsCount: organization._count.projects,
},
defaultVersion: url.searchParams.get("version") ?? "v2",
});
}
@@ -100,7 +103,7 @@ export const action: ActionFunction = async ({ request, params }) => {
};
export default function NewOrganizationPage() {
const { organization } = useTypedLoaderData<typeof loader>();
const { organization, defaultVersion } = useTypedLoaderData<typeof loader>();
const lastSubmission = useActionData();
const { v3Enabled } = useFeatures();
@@ -144,7 +147,7 @@ export default function NewOrganizationPage() {
<SelectGroup>
<Select
{...conform.input(projectVersion, { type: "select" })}
defaultValue={"v2"}
defaultValue={defaultVersion}
>
<SelectTrigger width="full" size="medium">
<SelectValue placeholder="Project version" />
@@ -0,0 +1,71 @@
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { GetProjectResponseBody, GetProjectsResponseBody } from "@trigger.dev/core/v3";
import { z } from "zod";
import { prisma } from "~/db.server";
import { logger } from "~/services/logger.server";
import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server";
const ParamsSchema = z.object({
projectRef: z.string(),
});
export async function loader({ request, params }: LoaderFunctionArgs) {
logger.info("get project", { url: request.url });
const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request);
if (!authenticationResult) {
return json({ error: "Invalid or Missing Access Token" }, { status: 401 });
}
const parsedParams = ParamsSchema.safeParse(params);
if (!parsedParams.success) {
return json({ error: "Invalid Params" }, { status: 400 });
}
const { projectRef } = parsedParams.data;
const project = await prisma.project.findUnique({
where: {
externalRef: projectRef,
organization: {
deletedAt: null,
members: {
some: {
userId: authenticationResult.userId,
},
},
},
deletedAt: null,
},
include: {
organization: true,
},
});
if (!project) {
return json({ error: "Project not found" }, { status: 404 });
}
if (project.version !== "V3") {
return json({ error: "Project found but was not a v3 project" }, { status: 404 });
}
const result: GetProjectResponseBody = {
id: project.id,
externalRef: project.externalRef,
name: project.name,
slug: project.slug,
createdAt: project.createdAt,
organization: {
id: project.organization.id,
title: project.organization.title,
slug: project.organization.slug,
createdAt: project.organization.createdAt,
},
};
return json(result);
}
+54
View File
@@ -0,0 +1,54 @@
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { GetProjectsResponseBody } from "@trigger.dev/core/v3";
import { prisma } from "~/db.server";
import { logger } from "~/services/logger.server";
import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server";
export async function loader({ request }: LoaderFunctionArgs) {
logger.info("get projects", { url: request.url });
const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request);
if (!authenticationResult) {
return json({ error: "Invalid or Missing Access Token" }, { status: 401 });
}
const projects = await prisma.project.findMany({
where: {
organization: {
deletedAt: null,
members: {
some: {
userId: authenticationResult.userId,
},
},
},
version: "V3",
deletedAt: null,
},
include: {
organization: true,
},
});
if (!projects) {
return json({ error: "Projects not found" }, { status: 404 });
}
const result: GetProjectsResponseBody = projects.map((project) => ({
id: project.id,
externalRef: project.externalRef,
name: project.name,
slug: project.slug,
createdAt: project.createdAt,
organization: {
id: project.organization.id,
title: project.organization.title,
slug: project.organization.slug,
createdAt: project.organization.createdAt,
},
}));
return json(result);
}
+2
View File
@@ -2,6 +2,7 @@ import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { WhoAmIResponse } from "@trigger.dev/core/v3";
import { prisma } from "~/db.server";
import { env } from "~/env.server";
import { logger } from "~/services/logger.server";
import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server";
@@ -29,6 +30,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
const result: WhoAmIResponse = {
userId: authenticationResult.userId,
email: user.email,
dashboardUrl: env.APP_ORIGIN,
};
return json(result);
}
+29
View File
@@ -0,0 +1,29 @@
import { LoaderFunctionArgs, redirect } from "@remix-run/server-runtime";
import { getUsersInvites } from "~/models/member.server";
import { SelectBestProjectPresenter } from "~/presenters/SelectBestProjectPresenter.server";
import { requireUser } from "~/services/session.server";
import { invitesPath, newOrganizationPath, newProjectPath } from "~/utils/pathBuilder";
//this loader chooses the best project to redirect you to, ideally based on the cookie
export const loader = async ({ request }: LoaderFunctionArgs) => {
const user = await requireUser(request);
const url = new URL(request.url);
const presenter = new SelectBestProjectPresenter();
try {
const { organization } = await presenter.call({ userId: user.id, request });
//redirect them to the most appropriate project
return redirect(`${newProjectPath(organization)}${url.search}`);
} catch (e) {
const invites = await getUsersInvites({ email: user.email });
if (invites.length > 0) {
return redirect(invitesPath());
}
//this should only happen if the user has no projects, and no invites
return redirect(newOrganizationPath());
}
};
@@ -0,0 +1,37 @@
import { LoaderFunctionArgs, redirect } from "@remix-run/server-runtime";
import { z } from "zod";
import { prisma } from "~/db.server";
import { requireUserId } from "~/services/session.server";
const ParamsSchema = z.object({
projectRef: z.string(),
});
export async function loader({ params, request }: LoaderFunctionArgs) {
const userId = await requireUserId(request);
const validatedParams = ParamsSchema.parse(params);
const project = await prisma.project.findFirst({
where: {
externalRef: validatedParams.projectRef,
organization: {
members: {
some: {
userId,
},
},
},
},
include: {
organization: true,
},
});
if (!project) {
return new Response("Not found", { status: 404 });
}
// Redirect to the project's runs page
return redirect(`/orgs/${project.organization.slug}/projects/v3/${project.slug}`);
}
+1 -1
View File
@@ -75,6 +75,7 @@
"test": "vitest"
},
"dependencies": {
"@baselime/node-opentelemetry": "^0.4.6",
"@clack/prompts": "^0.7.0",
"@depot/cli": "0.0.1-cli.2.55.0",
"@opentelemetry/api": "^1.7.0",
@@ -118,7 +119,6 @@
"object-hash": "^3.0.0",
"p-throttle": "^6.1.0",
"partysocket": "^0.0.17",
"posthog-node": "^3.1.1",
"proxy-agent": "^6.3.0",
"react": "^18.2.0",
"react-error-boundary": "^4.0.12",
+28
View File
@@ -12,6 +12,8 @@ import {
InitializeDeploymentRequestBody,
StartDeploymentIndexingRequestBody,
GetDeploymentResponseBody,
GetProjectsResponseBody,
GetProjectResponseBody,
} from "@trigger.dev/core/v3";
export class CliApiClient {
@@ -56,6 +58,32 @@ export class CliApiClient {
});
}
async getProject(projectRef: string) {
if (!this.accessToken) {
throw new Error("getProject: No access token");
}
return zodfetch(GetProjectResponseBody, `${this.apiURL}/api/v1/projects/${projectRef}`, {
headers: {
Authorization: `Bearer ${this.accessToken}`,
"Content-Type": "application/json",
},
});
}
async getProjects() {
if (!this.accessToken) {
throw new Error("getProjects: No access token");
}
return zodfetch(GetProjectsResponseBody, `${this.apiURL}/api/v1/projects`, {
headers: {
Authorization: `Bearer ${this.accessToken}`,
"Content-Type": "application/json",
},
});
}
async createBackgroundWorker(projectRef: string, body: CreateBackgroundWorkerRequestBody) {
if (!this.accessToken) {
throw new Error("createBackgroundWorker: No access token");
+86
View File
@@ -1,7 +1,93 @@
import { flattenAttributes, recordSpanException } from "@trigger.dev/core/v3";
import { Command } from "commander";
import { z } from "zod";
import { getTracer, provider } from "../telemetry/tracing";
import { fromZodError } from "zod-validation-error";
import { logger } from "../utilities/logger";
import { outro } from "@clack/prompts";
export const CommonCommandOptions = z.object({
apiUrl: z.string().optional(),
logLevel: z.enum(["debug", "info", "log", "warn", "error", "none"]).default("log"),
skipTelemetry: z.boolean().default(false),
});
export type CommonCommandOptions = z.infer<typeof CommonCommandOptions>;
export function commonOptions(command: Command) {
return command
.option("-a, --api-url <value>", "Override the API URL", "https://api.trigger.dev")
.option(
"-l, --log-level <level>",
"The log level to use (debug, info, log, warn, error, none)",
"log"
)
.option("--skip-telemetry", "Opt-out of sending telemetry");
}
export class SkipLoggingError extends Error {}
export class SkipCommandError extends Error {}
export class OutroCommandError extends SkipCommandError {}
export async function handleTelemetry(action: () => Promise<void>) {
try {
await action();
await provider?.forceFlush();
} catch (e) {
await provider?.forceFlush();
process.exitCode = 1;
}
}
export const tracer = getTracer();
export async function wrapCommandAction<T extends z.AnyZodObject, TResult>(
name: string,
schema: T,
options: unknown,
action: (opts: z.output<T>) => Promise<TResult>
): Promise<TResult> {
return await tracer.startActiveSpan(name, async (span) => {
try {
const parsedOptions = schema.safeParse(options);
if (!parsedOptions.success) {
throw new Error(fromZodError(parsedOptions.error).toString());
}
span.setAttributes({
...flattenAttributes(parsedOptions.data, "cli.options"),
});
logger.loggerLevel = parsedOptions.data.logLevel;
logger.debug(`Running "${name}" with the following options`, {
options: options,
spanContext: span?.spanContext(),
});
const result = await action(parsedOptions.data);
span.end();
return result;
} catch (e) {
if (e instanceof SkipLoggingError) {
recordSpanException(span, e);
} else if (e instanceof OutroCommandError) {
outro("Operation cancelled");
} else if (e instanceof SkipCommandError) {
// do nothing
} else {
recordSpanException(span, e);
logger.error(e instanceof Error ? e.message : String(e));
}
span.end();
throw e;
}
});
}
+5 -26
View File
@@ -1,45 +1,24 @@
import { Command } from "commander";
import { z } from "zod";
import { configureDeployCommand } from "../commands/deploy.js";
import { configureDevCommand } from "../commands/dev.js";
import { loginCommand } from "../commands/login.js";
import { configureInitCommand } from "../commands/init.js";
import { configureLoginCommand } from "../commands/login.js";
import { logoutCommand } from "../commands/logout.js";
import { updateCommand } from "../commands/update.js";
import { configureWhoamiCommand } from "../commands/whoami.js";
import { COMMAND_NAME } from "../consts.js";
import { getVersion } from "../utilities/getVersion.js";
import { printInitialBanner } from "../utilities/initialBanner.js";
import { configureDeployCommand } from "../commands/deploy.js";
export const program = new Command();
export const ApiUrlOptionsSchema = z.object({
apiUrl: z.string(),
});
program
.name(COMMAND_NAME)
.description("Create, run locally and deploy Trigger.dev background tasks.")
.version(getVersion(), "-v, --version", "Display the version number");
program
.command("login")
.description("Login with Trigger.dev so you can perform authenticated actions")
.option(
"-a, --api-url <value>",
"Override the API URL, defaults to https://api.trigger.dev",
"https://api.trigger.dev"
)
.version(getVersion(), "-v, --version", "Display the version number")
.action(async (options) => {
try {
await printInitialBanner(false);
await loginCommand(options);
//todo login command
} catch (e) {
//todo error reporting
throw e;
}
});
configureLoginCommand(program);
configureInitCommand(program);
program
.command("logout")
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -101,7 +101,7 @@ export async function devCommand(dir: string, anyOptions: unknown) {
let watcher;
try {
const devInstance = await startDev(dir, options.data, authorization.config);
const devInstance = await startDev(dir, options.data, authorization.auth);
watcher = devInstance.watcher;
const { waitUntilExit } = devInstance.devReactElement;
await waitUntilExit();
+457
View File
@@ -0,0 +1,457 @@
import { intro, isCancel, log, outro, select, spinner, text } from "@clack/prompts";
import { context, trace } from "@opentelemetry/api";
import {
GetProjectResponseBody,
flattenAttributes,
recordSpanException,
} from "@trigger.dev/core/v3";
import chalk from "chalk";
import { Command } from "commander";
import { execa } from "execa";
import { join, relative, resolve } from "node:path";
import terminalLink from "terminal-link";
import { z } from "zod";
import { CliApiClient } from "../apiClient";
import {
CommonCommandOptions,
OutroCommandError,
SkipCommandError,
SkipLoggingError,
commonOptions,
handleTelemetry,
tracer,
wrapCommandAction,
} from "../cli/common.js";
import { readConfig } from "../utilities/configFiles.js";
import { createFileFromTemplate } from "../utilities/createFileFromTemplate";
import { createFile, pathExists } from "../utilities/fileSystem";
import { getUserPackageManager } from "../utilities/getUserPackageManager";
import { printStandloneInitialBanner } from "../utilities/initialBanner.js";
import { logger } from "../utilities/logger";
import { resolveInternalFilePath } from "../utilities/resolveInternalFilePath";
import { login } from "./login";
const InitCommandOptions = CommonCommandOptions.extend({
projectRef: z.string().optional(),
overrideConfig: z.boolean().default(false),
tag: z.string().default("latest"),
skipPackageInstall: z.boolean().default(false),
});
type InitCommandOptions = z.infer<typeof InitCommandOptions>;
export function configureInitCommand(program: Command) {
return commonOptions(
program
.command("init")
.description("Initialize your existing project for development with Trigger.dev")
.argument("[path]", "The path to the project", ".")
.option(
"-p, --project-ref <project ref>",
"The project ref to use when initializing the project"
)
.option(
"-p, --project-ref <project ref>",
"The project ref to use when initializing the project"
)
.option(
"-t, --tag <package tag>",
"The version of the @trigger.dev/sdk package to install",
"latest"
)
.option("--skip-package-install", "Skip installing the @trigger.dev/sdk package")
.option("--override-config", "Override the existing config file if it exists")
).action(async (path, options) => {
await handleTelemetry(async () => {
await printStandloneInitialBanner(true);
await initCommand(path, options);
});
});
}
export async function initCommand(dir: string, options: unknown) {
return await wrapCommandAction("initCommand", InitCommandOptions, options, async (opts) => {
return await _initCommand(dir, opts);
});
}
async function _initCommand(dir: string, options: InitCommandOptions) {
const span = trace.getSpan(context.active());
intro("Initializing project");
const authorization = await login({ embedded: true, defaultApiUrl: options.apiUrl });
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 `trigger.dev login` to login.");
}
}
span?.setAttributes({
"cli.userId": authorization.userId,
"cli.email": authorization.email,
"cli.config.apiUrl": authorization.auth.apiUrl,
});
if (!options.overrideConfig) {
try {
// check to see if there is an existing trigger.dev config file in the project directory
const result = await readConfig(dir);
outro(
result.status === "file"
? `Project already initialized: Found config file at ${result.path}. Pass --override-config to override`
: "Project already initialized"
);
return;
} catch (e) {
// continue
}
}
const apiClient = new CliApiClient(authorization.auth.apiUrl, authorization.auth.accessToken);
const selectedProject = await selectProject(
apiClient,
authorization.dashboardUrl,
options.projectRef
);
span?.setAttributes({
...flattenAttributes(selectedProject, "cli.project"),
});
logger.debug("Selected project", selectedProject);
log.step(`Configuring project "${selectedProject.name}" (${selectedProject.externalRef})`);
// Install @trigger.dev/sdk package
if (!options.skipPackageInstall) {
await installPackages(dir, options);
} else {
log.info("Skipping package installation");
}
// Create the config file
await writeConfigFile(dir, selectedProject, options);
// Create the trigger dir
await createTriggerDir(dir, options);
const projectDashboard = terminalLink(
"project dashboard",
`${authorization.dashboardUrl}/projects/v3/${selectedProject.externalRef}`
);
log.success("Successfully initialized project for Trigger.dev v3 🫡");
log.info("Next steps:");
log.info(
` 1. To start developing, run ${chalk.green(
"npx trigger.dev@latest dev"
)} in your project directory`
);
log.info(` 2. Visit your ${projectDashboard} to view your newly created tasks.`);
log.info(
` 3. Head over to our ${terminalLink(
"v3 docs",
"https://trigger.dev/docs/v3"
)} to learn more.`
);
log.info(
` 4. Need help? Join our ${terminalLink(
"Discord community",
"https://trigger.dev/discord"
)} or email us at ${chalk.cyan("help@trigger.dev")}`
);
outro(`Project initialized successfully. Happy coding!`);
}
async function createTriggerDir(dir: string, options: InitCommandOptions) {
return await tracer.startActiveSpan("createTriggerDir", async (span) => {
try {
const location = await text({
message: "Where would you like to create the Trigger.dev directory?",
defaultValue: `${dir}/src/trigger`,
placeholder: `${dir}/src/trigger`,
});
if (isCancel(location)) {
throw new OutroCommandError();
}
const triggerDir = resolve(process.cwd(), location);
span.setAttributes({
"cli.triggerDir": triggerDir,
});
if (await pathExists(triggerDir)) {
throw new Error(`Directory already exists at ${triggerDir}`);
}
const exampleSelection = await select({
message: `Choose an example to create in the ${location} directory`,
options: [
{ value: "simple", label: "Simple (Hello World)" },
{
value: "none",
label: "None",
hint: "skip creating an example",
},
],
});
if (isCancel(exampleSelection)) {
throw new OutroCommandError();
}
const example = exampleSelection as string;
span.setAttributes({
"cli.example": example,
});
if (example === "none") {
// Create a .gitkeep file in the trigger dir
await createFile(join(triggerDir, ".gitkeep"), "");
log.step(`Created directory at ${location}`);
span.end();
return;
}
const exampleFile = resolveInternalFilePath(`./templates/examples/${example}.js`);
const outputPath = join(triggerDir, "example.ts");
await createFileFromTemplate({
templatePath: exampleFile,
outputPath,
replacements: {},
});
const relativeOutputPath = relative(process.cwd(), outputPath);
log.step(`Created example file at ${relativeOutputPath}`);
span.end();
} catch (e) {
if (!(e instanceof SkipCommandError)) {
recordSpanException(span, e);
}
span.end();
throw e;
}
});
}
async function installPackages(dir: string, options: InitCommandOptions) {
return await tracer.startActiveSpan("installPackages", async (span) => {
const installSpinner = spinner();
try {
const projectDir = resolve(process.cwd(), dir);
const pkgManager = await getUserPackageManager(projectDir);
span.setAttributes({
"cli.projectDir": projectDir,
"cli.packageManager": pkgManager,
"cli.tag": options.tag,
});
switch (pkgManager) {
case "npm": {
installSpinner.start(`Running npm install @trigger.dev/sdk@${options.tag}`);
await execa("npm", ["install", `@trigger.dev/sdk@${options.tag}`], {
cwd: projectDir,
stdio: options.logLevel === "debug" ? "inherit" : "ignore",
});
break;
}
case "pnpm": {
installSpinner.start(`Running pnpm add @trigger.dev/sdk@${options.tag}`);
await execa("pnpm", ["add", `@trigger.dev/sdk@${options.tag}`], {
cwd: projectDir,
stdio: options.logLevel === "debug" ? "inherit" : "ignore",
});
break;
}
case "yarn": {
installSpinner.start(`Running yarn add @trigger.dev/sdk@${options.tag}`);
await execa("yarn", ["add", `@trigger.dev/sdk@${options.tag}`], {
cwd: projectDir,
stdio: options.logLevel === "debug" ? "inherit" : "ignore",
});
break;
}
}
installSpinner.stop(`@trigger.dev/sdk@${options.tag} installed`);
span.end();
} catch (e) {
installSpinner.stop(
`Failed to install @trigger.dev/sdk@${options.tag}. Rerun command with --log-level debug for more details.`
);
if (!(e instanceof SkipCommandError)) {
recordSpanException(span, e);
}
span.end();
throw e;
}
});
}
async function writeConfigFile(
dir: string,
project: GetProjectResponseBody,
options: InitCommandOptions
) {
return await tracer.startActiveSpan("writeConfigFile", async (span) => {
try {
const spnnr = spinner();
spnnr.start("Creating config file");
const projectDir = resolve(process.cwd(), dir);
const templatePath = resolveInternalFilePath("./templates/trigger.config.mjs");
const outputPath = join(projectDir, "trigger.config.mjs");
span.setAttributes({
"cli.projectDir": projectDir,
"cli.templatePath": templatePath,
"cli.outputPath": outputPath,
});
const result = await createFileFromTemplate({
templatePath,
replacements: {
projectRef: project.externalRef,
},
outputPath,
override: options.overrideConfig,
});
const relativePathToOutput = relative(process.cwd(), outputPath);
spnnr.stop(
result.success
? `Config file created at ${relativePathToOutput}`
: `Failed to create config file: ${result.error}`
);
if (!result.success) {
throw new SkipLoggingError(result.error);
}
span.end();
return result.success;
} catch (e) {
if (!(e instanceof SkipCommandError)) {
recordSpanException(span, e);
}
span.end();
throw e;
}
});
}
async function selectProject(apiClient: CliApiClient, dashboardUrl: string, projectRef?: string) {
return await tracer.startActiveSpan("selectProject", async (span) => {
try {
if (projectRef) {
const projectResponse = await apiClient.getProject(projectRef);
if (!projectResponse.success) {
log.error(
`--project-ref ${projectRef} is not a valid project ref. Request to fetch data resulted in: ${projectResponse.error}`
);
throw new SkipCommandError(projectResponse.error);
}
span.setAttributes({
...flattenAttributes(projectResponse.data, "cli.project"),
});
span.end();
return projectResponse.data;
}
const projectsResponse = await apiClient.getProjects();
if (!projectsResponse.success) {
throw new Error(`Failed to get projects: ${projectsResponse.error}`);
}
if (projectsResponse.data.length === 0) {
const newProjectLink = terminalLink(
"Create new project",
`${dashboardUrl}/projects/new?version=v3`
);
outro(`You don't have any projects yet. ${newProjectLink}`);
throw new SkipCommandError();
}
const selectedProject = await select({
message: "Select an existing Trigger.dev project",
options: projectsResponse.data.map((project) => ({
value: project.externalRef,
label: `${project.name} - ${project.externalRef}`,
hint: project.organization.title,
})),
});
if (isCancel(selectedProject)) {
throw new OutroCommandError();
}
const projectData = projectsResponse.data.find(
(project) => project.externalRef === selectedProject
);
if (!projectData) {
throw new Error("Invalid project ref");
}
span.setAttributes({
...flattenAttributes(projectData, "cli.project"),
});
span.end();
return projectData;
} catch (e) {
if (!(e instanceof SkipCommandError)) {
recordSpanException(span, e);
}
span.end();
throw e;
}
});
}
+264 -108
View File
@@ -1,136 +1,292 @@
import { intro, log, outro, select, spinner } from "@clack/prompts";
import { recordSpanException } from "@trigger.dev/core/v3";
import { Command } from "commander";
import open from "open";
import pRetry, { AbortError } from "p-retry";
import { ApiUrlOptionsSchema } from "../cli/index.js";
import { z } from "zod";
import { CliApiClient } from "../apiClient.js";
import {
CommonCommandOptions,
SkipLoggingError,
commonOptions,
handleTelemetry,
tracer,
wrapCommandAction,
} from "../cli/common.js";
import { chalkLink } from "../utilities/colors.js";
import { readAuthConfigFile, writeAuthConfigFile } from "../utilities/configFiles.js";
import { logger } from "../utilities/logger.js";
import { getVersion } from "../utilities/getVersion.js";
import { printInitialBanner } from "../utilities/initialBanner.js";
import { LoginResult } from "../utilities/session.js";
import { whoAmI } from "./whoami.js";
import { CliApiClient } from "../apiClient.js";
export async function loginCommand(options: any) {
const result = ApiUrlOptionsSchema.safeParse(options);
if (!result.success) {
logger.error(result.error.message);
return;
}
export const LoginCommandOptions = CommonCommandOptions.extend({
apiUrl: z.string(),
});
return login(result.data.apiUrl);
export type LoginCommandOptions = z.infer<typeof LoginCommandOptions>;
export function configureLoginCommand(program: Command) {
return commonOptions(
program
.command("login")
.description("Login with Trigger.dev so you can perform authenticated actions")
)
.version(getVersion(), "-v, --version", "Display the version number")
.action(async (options) => {
await handleTelemetry(async () => {
await printInitialBanner(false);
await loginCommand(options);
});
});
}
export type LoginResult =
| {
success: true;
accessToken: string;
}
| {
success: false;
error: string;
};
export async function loginCommand(options: unknown) {
return await wrapCommandAction("loginCommand", LoginCommandOptions, options, async (opts) => {
return await _loginCommand(opts);
});
}
export async function login(apiUrl: string): Promise<LoginResult> {
const apiClient = new CliApiClient(apiUrl);
async function _loginCommand(options: LoginCommandOptions) {
return login({ defaultApiUrl: options.apiUrl, embedded: false });
}
intro("Logging in to Trigger.dev");
export type LoginOptions = {
defaultApiUrl?: string;
embedded?: boolean;
};
const existingAccessToken = readAuthConfigFile()?.accessToken;
if (existingAccessToken) {
const whoAmiI = await whoAmI();
export async function login(options?: LoginOptions): Promise<LoginResult> {
return await tracer.startActiveSpan("login", async (span) => {
try {
const opts = { defaultApiUrl: "https://api.trigger.dev", embedded: false, ...options };
const continueOption = await select({
message: "You are already logged in.",
options: [
{
value: false,
label: "Exit",
},
{
value: true,
label: "Login with a different account",
},
],
initialValue: false,
});
span.setAttributes({
"cli.config.apiUrl": opts.defaultApiUrl,
});
if (continueOption !== true) {
outro("Already logged in");
return {
success: true,
accessToken: existingAccessToken,
};
}
}
//generate authorization code
const createAuthCodeSpinner = spinner();
createAuthCodeSpinner.start("Creating authorition code");
const authorizationCodeResult = await apiClient.createAuthorizationCode();
if (!authorizationCodeResult.success) {
createAuthCodeSpinner.stop(
`Failed to create authorization code\n${authorizationCodeResult.error}`
);
return {
success: false,
error: authorizationCodeResult.error,
};
}
createAuthCodeSpinner.stop("Created authorization code");
//Link the user to the authorization code
log.step(
`Please visit the following URL to login:\n${chalkLink(authorizationCodeResult.data.url)}`
);
await open(authorizationCodeResult.data.url);
//poll for personal access token (we need to poll for it)
const getPersonalAccessTokenSpinner = spinner();
getPersonalAccessTokenSpinner.start("Waiting for you to login");
try {
const indexResult = await pRetry(
() => getPersonalAccessToken(apiClient, authorizationCodeResult.data.authorizationCode),
{
//this means we're polling, same distance between each attempt
factor: 1,
retries: 60,
minTimeout: 1000,
if (!opts.embedded) {
intro("Logging in to Trigger.dev");
}
);
getPersonalAccessTokenSpinner.stop(`Logged in with token ${indexResult.obfuscatedToken}`);
const authConfig = readAuthConfigFile();
writeAuthConfigFile({ accessToken: indexResult.token, apiUrl });
if (authConfig && authConfig.accessToken) {
const whoAmIResult = await whoAmI(undefined, opts.embedded);
outro("Logged in successfully");
if (!whoAmIResult.success) {
throw new Error(whoAmIResult.error);
} else {
if (!opts.embedded) {
const continueOption = await select({
message: "You are already logged in.",
options: [
{
value: false,
label: "Exit",
},
{
value: true,
label: "Login with a different account",
},
],
initialValue: false,
});
return {
success: true,
accessToken: indexResult.token,
};
} catch (e) {
getPersonalAccessTokenSpinner.stop(`Failed to get access token`);
if (e instanceof AbortError) {
log.error(e.message);
if (continueOption !== true) {
outro("Already logged in");
span.setAttributes({
"cli.userId": whoAmIResult.data.userId,
"cli.email": whoAmIResult.data.email,
"cli.config.apiUrl": authConfig.apiUrl ?? opts.defaultApiUrl,
});
span.end();
return {
ok: true as const,
userId: whoAmIResult.data.userId,
email: whoAmIResult.data.email,
dashboardUrl: whoAmIResult.data.dashboardUrl,
auth: {
accessToken: authConfig.accessToken,
apiUrl: authConfig.apiUrl ?? opts.defaultApiUrl,
},
};
}
} else {
span.setAttributes({
"cli.userId": whoAmIResult.data.userId,
"cli.email": whoAmIResult.data.email,
"cli.config.apiUrl": authConfig.apiUrl ?? opts.defaultApiUrl,
});
span.end();
return {
ok: true as const,
userId: whoAmIResult.data.userId,
email: whoAmIResult.data.email,
dashboardUrl: whoAmIResult.data.dashboardUrl,
auth: {
accessToken: authConfig.accessToken,
apiUrl: authConfig.apiUrl ?? opts.defaultApiUrl,
},
};
}
}
}
if (opts.embedded) {
log.step("You must login to continue.");
}
const apiClient = new CliApiClient(authConfig?.apiUrl ?? opts.defaultApiUrl);
//generate authorization code
const authorizationCodeResult = await createAuthorizationCode(apiClient);
//Link the user to the authorization code
log.step(
`Please visit the following URL to login:\n${chalkLink(authorizationCodeResult.url)}`
);
await open(authorizationCodeResult.url);
//poll for personal access token (we need to poll for it)
const getPersonalAccessTokenSpinner = spinner();
getPersonalAccessTokenSpinner.start("Waiting for you to login");
try {
const indexResult = await pRetry(
() => getPersonalAccessToken(apiClient, authorizationCodeResult.authorizationCode),
{
//this means we're polling, same distance between each attempt
factor: 1,
retries: 60,
minTimeout: 1000,
}
);
getPersonalAccessTokenSpinner.stop(`Logged in with token ${indexResult.obfuscatedToken}`);
writeAuthConfigFile({ accessToken: indexResult.token, apiUrl: opts.defaultApiUrl });
const whoAmIResult = await whoAmI(undefined, opts.embedded);
if (!whoAmIResult.success) {
throw new Error(whoAmIResult.error);
}
if (opts.embedded) {
log.step("Logged in successfully");
} else {
outro("Logged in successfully");
}
span.end();
return {
ok: true as const,
userId: whoAmIResult.data.userId,
email: whoAmIResult.data.email,
dashboardUrl: whoAmIResult.data.dashboardUrl,
auth: {
accessToken: indexResult.token,
apiUrl: authConfig?.apiUrl ?? opts.defaultApiUrl,
},
};
} catch (e) {
getPersonalAccessTokenSpinner.stop(`Failed to get access token`);
if (e instanceof AbortError) {
log.error(e.message);
}
recordSpanException(span, e);
span.end();
return {
ok: false as const,
error: e instanceof Error ? e.message : String(e),
};
}
} catch (e) {
recordSpanException(span, e);
span.end();
if (options?.embedded) {
return {
ok: false as const,
error: e instanceof Error ? e.message : String(e),
};
}
throw e;
}
return {
success: false,
error: e instanceof Error ? e.message : JSON.stringify(e),
};
}
});
}
async function getPersonalAccessToken(apiClient: CliApiClient, authorizationCode: string) {
const token = await apiClient.getPersonalAccessToken(authorizationCode);
return await tracer.startActiveSpan("getPersonalAccessToken", async (span) => {
try {
const token = await apiClient.getPersonalAccessToken(authorizationCode);
if (!token.success) {
throw new AbortError(token.error);
}
if (!token.success) {
throw new AbortError(token.error);
}
if (!token.data.token) {
throw new Error("No token found yet");
}
if (!token.data.token) {
throw new Error("No token found yet");
}
return {
token: token.data.token.token,
obfuscatedToken: token.data.token.obfuscatedToken,
};
span.end();
return {
token: token.data.token.token,
obfuscatedToken: token.data.token.obfuscatedToken,
};
} catch (e) {
if (e instanceof AbortError) {
recordSpanException(span, e);
}
span.end();
throw e;
}
});
}
async function createAuthorizationCode(apiClient: CliApiClient) {
return await tracer.startActiveSpan("createAuthorizationCode", async (span) => {
try {
//generate authorization code
const createAuthCodeSpinner = spinner();
createAuthCodeSpinner.start("Creating authorition code");
const authorizationCodeResult = await apiClient.createAuthorizationCode();
if (!authorizationCodeResult.success) {
createAuthCodeSpinner.stop(
`Failed to create authorization code\n${authorizationCodeResult.error}`
);
throw new SkipLoggingError(
`Failed to create authorization code\n${authorizationCodeResult.error}`
);
}
createAuthCodeSpinner.stop("Created authorization code");
span.end();
return authorizationCodeResult.data;
} catch (e) {
recordSpanException(span, e);
span.end();
throw e;
}
});
}
+16 -12
View File
@@ -14,6 +14,7 @@ type WhoAmIResult =
data: {
userId: string;
email: string;
dashboardUrl: string;
};
}
| {
@@ -44,7 +45,10 @@ export function configureWhoamiCommand(program: Command) {
});
}
export async function whoAmI(options?: WhoamiCommandOptions): Promise<WhoAmIResult> {
export async function whoAmI(
options?: WhoamiCommandOptions,
embedded: boolean = false
): Promise<WhoAmIResult> {
if (options?.logLevel) {
logger.loggerLevel = options?.logLevel;
}
@@ -67,10 +71,7 @@ export async function whoAmI(options?: WhoamiCommandOptions): Promise<WhoAmIResu
};
}
const apiClient = new CliApiClient(
authentication.config.apiUrl,
authentication.config.accessToken
);
const apiClient = new CliApiClient(authentication.auth.apiUrl, authentication.auth.accessToken);
const userData = await apiClient.whoAmI();
if (!userData.success) {
@@ -82,15 +83,18 @@ export async function whoAmI(options?: WhoamiCommandOptions): Promise<WhoAmIResu
};
}
loadingSpinner.stop("Retrieved your account details");
note(
`User ID: ${userData.data.userId}
if (!embedded) {
loadingSpinner.stop("Retrieved your account details");
note(
`User ID: ${userData.data.userId}
Email: ${userData.data.email}
URL: ${chalkLink(authentication.config.apiUrl)}
URL: ${chalkLink(authentication.auth.apiUrl)}
`,
"Account details"
);
"Account details"
);
} else {
loadingSpinner.stop(`Retrieved your account details for ${userData.data.email}`);
}
return userData;
}
@@ -1,51 +0,0 @@
import { PostHog } from "posthog-node";
import { nanoid } from "nanoid";
import { getVersion } from "../utilities/getVersion.js";
const postHogApiKey = "phc_9aSDbJCaDUMdZdHxxMPTvcj7A9fsl3mCgM1RBPmPsl7";
export class TelemetryClient {
#client: PostHog;
#sessionId: string;
#version: string;
constructor() {
this.#client = new PostHog(postHogApiKey, {
host: "https://eu.posthog.com",
flushAt: 1,
});
this.#sessionId = `cli-${nanoid()}`;
this.#version = getVersion();
}
identify(organizationId: string, projectId: string, userId?: string) {
if (userId) {
this.#client.alias({
distinctId: userId,
alias: this.#sessionId,
});
}
this.#client.groupIdentify({
groupType: "organization",
groupKey: organizationId,
});
this.#client.groupIdentify({
groupType: "project",
groupKey: projectId,
});
}
dev = {
started: (path: string, options: Record<string, string | number | boolean>) => {
this.#client.capture({
distinctId: this.#sessionId,
event: "cli_dev_started",
properties: { ...options, path },
});
},
};
}
export const telemetryClient = new TelemetryClient();
+24
View File
@@ -0,0 +1,24 @@
import { BaselimeSDK } from "@baselime/node-opentelemetry";
import { trace } from "@opentelemetry/api";
import { FetchInstrumentation } from "@opentelemetry/instrumentation-fetch";
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import * as packageJson from "../../package.json";
const sdk = new BaselimeSDK({
baselimeKey: "e9f963244f8b092850d42e34a5339b2d5e68070b".split("").reverse().join(""), // this is a joke
instrumentations: [new FetchInstrumentation()],
service: "cli-v3",
serverless: true,
});
function initializeTracing(): NodeTracerProvider | undefined {
if (!process.argv.includes("--skip-telemetry")) {
return sdk.start();
}
}
export const provider = initializeTracing();
export function getTracer() {
return trace.getTracer("trigger.dev cli", packageJson.version);
}
@@ -0,0 +1,14 @@
import { logger, task, wait } from "@trigger.dev/sdk/v3";
export const helloWorldTask = task({
id: "hello-world",
run: async (payload: any, { ctx }) => {
logger.log("Hello, world!", { payload, ctx });
await wait.for({ seconds: 5 });
return {
message: "Hello, world!",
}
},
});
@@ -0,0 +1,16 @@
// @ts-check
/** @type {import('@trigger.dev/sdk/v3').Config} */
export default {
project: "${projectRef}",
retries: {
enabledInDev: false,
default: {
maxAttempts: 3,
minTimeoutInMs: 1000,
maxTimeoutInMs: 10000,
factor: 2,
randomize: true,
},
},
};
@@ -0,0 +1,62 @@
import fs from "fs/promises";
import { pathExists, readFile } from "./fileSystem";
import path from "path";
type Result =
| {
success: true;
alreadyExisted: boolean;
}
| {
success: false;
error: string;
};
export async function createFileFromTemplate(params: {
templatePath: string;
replacements: Record<string, string>;
outputPath: string;
override?: boolean;
}): Promise<Result> {
let template = await readFile(params.templatePath);
if ((await pathExists(params.outputPath)) && !params.override) {
return {
success: true,
alreadyExisted: true,
};
}
try {
const output = replaceAll(template, params.replacements);
const directoryName = path.dirname(params.outputPath);
await fs.mkdir(directoryName, { recursive: true });
await fs.writeFile(params.outputPath, output);
return {
success: true,
alreadyExisted: false,
};
} catch (e) {
if (e instanceof Error) {
return {
success: false,
error: e.message,
};
}
return {
success: false,
error: JSON.stringify(e),
};
}
}
// find strings that match ${varName} and replace with the value from a Record<string, string> where { varName: "value" }
export function replaceAll(input: string, replacements: Record<string, string>) {
let output = input;
for (const [key, value] of Object.entries(replacements)) {
output = output.replace(new RegExp(`\\$\\{${key}\\}`, "g"), value);
}
return output;
}
@@ -41,7 +41,7 @@ After installation, run Trigger.dev with \`npx trigger.dev\`.`
export async function printStandloneInitialBanner(performUpdateCheck = true) {
const packageVersion = getVersion();
let text = `\n${logo()} ${chalkGrey(`${packageVersion}`)}`;
let text = `\n${logo()} ${chalkGrey("(v3 Developer Preview)")}`;
if (performUpdateCheck) {
const maybeNewVersion = await updateCheck();
@@ -2,7 +2,7 @@ import semver from "semver";
import { execa } from "execa";
import { logger } from "./logger";
import { join } from "node:path";
import { readJSONFile } from "./fileSystem";
import { readJSONFile, writeJSONFile } from "./fileSystem";
export type InstallPackagesOptions = { cwd?: string };
@@ -16,7 +16,11 @@ export async function installPackages(
try {
await readJSONFile(join(cwd, "package.json"));
} catch (error) {
await execa("npm", ["init", "-y"], { cwd });
await writeJSONFile(join(cwd, "package.json"), {
name: "temp",
version: "1.0.0",
description: "",
});
}
// Detect with packages have already been installed at the specified version (use semver to compare)
@@ -0,0 +1,5 @@
import { resolve as importResolve } from "import-meta-resolve";
export function resolveInternalFilePath(filePath: string): string {
return new URL(importResolve(filePath, import.meta.url)).href.replace("file://", "");
}
+77 -27
View File
@@ -1,34 +1,84 @@
import { recordSpanException } from "@trigger.dev/core/v3";
import { CliApiClient } from "../apiClient.js";
import { readAuthConfigFile } from "./configFiles.js";
import { getTracer } from "../telemetry/tracing.js";
export async function isLoggedIn() {
const config = readAuthConfigFile();
const tracer = getTracer();
if (!config?.accessToken || !config?.apiUrl) {
return { ok: false as const, error: "You must login first" };
}
const apiClient = new CliApiClient(config.apiUrl, config.accessToken);
const userData = await apiClient.whoAmI();
if (!userData.success) {
return {
ok: false as const,
error: userData.error,
config: {
apiUrl: config.apiUrl,
accessToken: config.accessToken,
},
export type LoginResult =
| {
ok: true;
userId: string;
email: string;
dashboardUrl: string;
auth: {
apiUrl: string;
accessToken: string;
};
}
| {
ok: false;
error: string;
auth?: {
apiUrl: string;
accessToken: string;
};
};
}
return {
ok: true as const,
userId: userData.data.userId,
email: userData.data.email,
config: {
apiUrl: config.apiUrl,
accessToken: config.accessToken,
},
};
export async function isLoggedIn(): Promise<LoginResult> {
return await tracer.startActiveSpan("isLoggedIn", async (span) => {
try {
const config = readAuthConfigFile();
if (!config?.accessToken || !config?.apiUrl) {
span.recordException(new Error("You must login first"));
span.end();
return { ok: false as const, error: "You must login first" };
}
const apiClient = new CliApiClient(config.apiUrl, config.accessToken);
const userData = await apiClient.whoAmI();
if (!userData.success) {
recordSpanException(span, userData.error);
span.end();
return {
ok: false as const,
error: userData.error,
auth: {
apiUrl: config.apiUrl,
accessToken: config.accessToken,
},
};
}
span.setAttributes({
"login.userId": userData.data.userId,
"login.email": userData.data.email,
"login.dashboardUrl": userData.data.dashboardUrl,
});
span.end();
return {
ok: true as const,
userId: userData.data.userId,
email: userData.data.email,
dashboardUrl: userData.data.dashboardUrl,
auth: {
apiUrl: config.apiUrl,
accessToken: config.accessToken,
},
};
} catch (e) {
recordSpanException(span, e);
span.end();
return {
ok: false as const,
error: e instanceof Error ? e.message : "Unknown error",
};
}
});
}
+2 -1
View File
@@ -1,6 +1,7 @@
import { defineConfig } from "tsup";
const isDev = process.env.npm_lifecycle_event === "dev";
const copyTemplates = "cp -r src/templates dist";
export default defineConfig({
clean: false,
@@ -14,7 +15,7 @@ export default defineConfig({
sourcemap: true,
target: "esnext",
outDir: "dist",
onSuccess: isDev ? `node dist/index.js` : "",
onSuccess: isDev ? `${copyTemplates} && node dist/index.js` : copyTemplates,
banner: {
js: "import { createRequire } from 'module';const require = createRequire(import.meta.url);",
},
+21
View File
@@ -5,10 +5,31 @@ import { QueueOptions } from "./messages";
export const WhoAmIResponseSchema = z.object({
userId: z.string(),
email: z.string().email(),
dashboardUrl: z.string(),
});
export type WhoAmIResponse = z.infer<typeof WhoAmIResponseSchema>;
export const GetProjectResponseBody = z.object({
id: z.string(),
externalRef: z.string(),
name: z.string(),
slug: z.string(),
createdAt: z.coerce.date(),
organization: z.object({
id: z.string(),
title: z.string(),
slug: z.string(),
createdAt: z.coerce.date(),
}),
});
export type GetProjectResponseBody = z.infer<typeof GetProjectResponseBody>;
export const GetProjectsResponseBody = z.array(GetProjectResponseBody);
export type GetProjectsResponseBody = z.infer<typeof GetProjectsResponseBody>;
export const GetProjectEnvResponse = z.object({
apiKey: z.string(),
name: z.string(),
@@ -7,7 +7,7 @@ export function flattenAttributes(
const result: Attributes = {};
// Check if obj is null or undefined
if (obj == null) {
if (!obj) {
return result;
}
+82 -4
View File
@@ -1033,6 +1033,7 @@ importers:
packages/cli-v3:
specifiers:
'@baselime/node-opentelemetry': ^0.4.6
'@clack/prompts': ^0.7.0
'@depot/cli': 0.0.1-cli.2.55.0
'@opentelemetry/api': ^1.7.0
@@ -1090,7 +1091,6 @@ importers:
p-retry: ^6.1.0
p-throttle: ^6.1.0
partysocket: ^0.0.17
posthog-node: ^3.1.1
proxy-agent: ^6.3.0
react: ^18.2.0
react-error-boundary: ^4.0.12
@@ -1113,6 +1113,7 @@ importers:
zod: 3.22.3
zod-validation-error: ^1.5.0
dependencies:
'@baselime/node-opentelemetry': 0.4.6_supports-color@9.4.0
'@clack/prompts': 0.7.0
'@depot/cli': 0.0.1-cli.2.55.0
'@opentelemetry/api': 1.7.0
@@ -1156,7 +1157,6 @@ importers:
object-hash: 3.0.0
p-throttle: 6.1.0
partysocket: 0.0.17
posthog-node: 3.1.3
proxy-agent: 6.3.0_supports-color@9.4.0
react: 18.2.0
react-error-boundary: 4.0.12_react@18.2.0
@@ -2179,7 +2179,7 @@ importers:
references/v3-catalog:
specifiers:
'@trigger.dev/sdk': workspace:*
'@trigger.dev/sdk': 2.3.18
'@trigger.dev/tsconfig': workspace:*
'@types/node': 20.4.2
concurrently: ^8.2.0
@@ -2193,7 +2193,7 @@ importers:
tsconfig-paths: ^3.14.1
typescript: 5.1.6
dependencies:
'@trigger.dev/sdk': link:../../packages/trigger-sdk
'@trigger.dev/sdk': 2.3.18
msw: 2.2.1_typescript@5.1.6
openai: 4.28.0
stripe: 12.14.0
@@ -5590,6 +5590,28 @@ packages:
resolution: {integrity: sha512-4iri8i1AqYHJE2DstZYkyEprg6Pq6sKx3xn5FpySk9sNhH7qN2LLlHJCfDTZRILNwQNPD7mATWM0TBui7uC1pA==}
dev: true
/@baselime/node-opentelemetry/0.4.6_supports-color@9.4.0:
resolution: {integrity: sha512-ZsPa4y0WW9/rGELsaAT3oheM19c1EaTOY3W5Rx5CeNzcB7EZAWL8xMF6fNKf6u8Bk/gQ5x6PHKzd2rs3GeRLmQ==}
peerDependencies:
'@trpc/server': ^10.0.0 || ^11.0.0
dependencies:
'@opentelemetry/api': 1.7.0
'@opentelemetry/exporter-trace-otlp-http': 0.48.0_@opentelemetry+api@1.7.0
'@opentelemetry/instrumentation': 0.48.0_rr4fxqkzz7nhz3auplkqn4p6em
'@opentelemetry/instrumentation-http': 0.48.0_rr4fxqkzz7nhz3auplkqn4p6em
'@opentelemetry/resource-detector-aws': 1.3.6_@opentelemetry+api@1.7.0
'@opentelemetry/resources': 1.21.0_@opentelemetry+api@1.7.0
'@opentelemetry/sdk-node': 0.48.0_rr4fxqkzz7nhz3auplkqn4p6em
'@opentelemetry/sdk-trace-node': 1.21.0_@opentelemetry+api@1.7.0
'@types/aws-lambda': 8.10.136
axios: 1.6.2
flat: 6.0.1
undici: 5.28.3
transitivePeerDependencies:
- debug
- supports-color
dev: false
/@bcoe/v8-coverage/0.2.3:
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
dev: true
@@ -17687,6 +17709,11 @@ packages:
react-dom: 18.2.0_react@18.2.0
dev: false
/@trigger.dev/core-backend/2.3.18:
resolution: {integrity: sha512-LVeeerraGeqKNd2gtajQY+mnGWqkYW7Q2r5oWpL5xIZ8aQg3HRhSIfZs1dryexwKlfqnRjGWueGTy2+j1tbzcg==}
engines: {node: '>=18.0.0'}
dev: false
/@trigger.dev/core/2.0.7:
resolution: {integrity: sha512-z86G0sbqu0ePP0eg3jA/v65TOq9ZLCHUpgis/cqaLopsybnMScWt9S9MV1Dq9FVqHHqKvyuLBugZTJwsj+FXGg==}
engines: {node: '>=16.8.0'}
@@ -17696,6 +17723,15 @@ packages:
zod-error: 1.5.0
dev: false
/@trigger.dev/core/2.3.18:
resolution: {integrity: sha512-j2EdCeyMkZ+zlVnnHl5zmBb+YURSw4x75NqQU1G5X08pQAza7G0qEn8DDGIMR5ieUMiHP0WS9oYy/voYdNfibQ==}
engines: {node: '>=18.0.0'}
dependencies:
ulidx: 2.2.1
zod: 3.22.3
zod-error: 1.5.0
dev: false
/@trigger.dev/nextjs/1.0.0_fy3ffmrhk4zoekjz4cwhclpq64:
resolution: {integrity: sha512-Dt32BaAKYNJqYbPcpdzUOfkgpDRIftyd4oj2lBFilIHZUCJpfG1MdYbr4YWFhNE5Z04Pj0I+uN8kaxTkTZp+Ig==}
engines: {node: '>=16.8.0'}
@@ -17737,6 +17773,31 @@ packages:
- utf-8-validate
dev: false
/@trigger.dev/sdk/2.3.18:
resolution: {integrity: sha512-Bjxgl4BbWOAL8rhxeBkl7SzvLLRBMJjiftq/7W7u96MDyPRFUoZZvVMSZzTJufnLBf/xS2JTi8LWU8gzhDJDvw==}
engines: {node: '>=18.0.0'}
dependencies:
'@trigger.dev/core': 2.3.18
'@trigger.dev/core-backend': 2.3.18
chalk: 5.3.0
cronstrue: 2.21.0
debug: 4.3.4
evt: 2.5.7
get-caller-file: 2.0.5
git-remote-origin-url: 4.0.0
git-repo-info: 2.1.1
slug: 6.1.0
terminal-link: 3.0.0
ulid: 2.3.0
uuid: 9.0.0
ws: 8.16.0
zod: 3.22.3
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
dev: false
/@tsconfig/node10/1.0.9:
resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==}
@@ -17795,6 +17856,10 @@ packages:
resolution: {integrity: sha512-vBkIh9AY22kVOCEKo5CJlyCgmSWvasC+SWUxL/x/vOwRobMpI/HG1xp/Ae3AqmSiZeLUbOhW0FCD3ZjqqUxmXw==}
dev: false
/@types/aws-lambda/8.10.136:
resolution: {integrity: sha512-cmmgqxdVGhxYK9lZMYYXYRJk6twBo53ivtXjIUEFZxfxe4TkZTZBK3RRWrY2HjJcUIix0mdifn15yjOAat5lTA==}
dev: false
/@types/aws-lambda/8.10.81:
resolution: {integrity: sha512-C1rFKGVZ8KwqhwBOYlpoybTSRtxu2433ea6JaO3amc6ubEe08yQoFsPa9aU9YqvX7ppeZ25CnCtC4AH9mhtxsQ==}
dev: false
@@ -25672,6 +25737,12 @@ packages:
hasBin: true
dev: true
/flat/6.0.1:
resolution: {integrity: sha512-/3FfIa8mbrg3xE7+wAhWeV+bd7L2Mof+xtZb5dRDKZ+wDvYJK4WDYeIOuOhre5Yv5aQObZrlbRmk3RTSiuQBtw==}
engines: {node: '>=18'}
hasBin: true
dev: false
/flatted/3.2.7:
resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==}
@@ -37959,6 +38030,13 @@ packages:
dependencies:
'@fastify/busboy': 2.0.0
/undici/5.28.3:
resolution: {integrity: sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA==}
engines: {node: '>=14.0'}
dependencies:
'@fastify/busboy': 2.0.0
dev: false
/unfetch/4.2.0:
resolution: {integrity: sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==}
+1 -1
View File
@@ -14,7 +14,7 @@
"start:stripe": "ts-node -r tsconfig-paths/register -r dotenv/config src/stripeUsage.ts"
},
"dependencies": {
"@trigger.dev/sdk": "workspace:*",
"@trigger.dev/sdk": "2.3.18",
"msw": "^2.2.1",
"openai": "^4.28.0",
"stripe": "^12.14.0"
+1 -4
View File
@@ -4,7 +4,7 @@
export default {
project: "yubjwjsfkxnylobaqvqz",
retries: {
enabledInDev: true,
enabledInDev: false,
default: {
maxAttempts: 3,
minTimeoutInMs: 1000,
@@ -13,7 +13,4 @@ export default {
randomize: true,
},
},
handleError: async (payload, error, { ctx, retryAt, retryDelayInMs, retry }) => {
return { skipRetrying: true };
},
};