Merge pull request #71 from triggerdotdev/dev

Onboarding improvements using the new create-trigger CLI
This commit is contained in:
Eric Allam
2023-02-27 13:07:30 +00:00
committed by GitHub
97 changed files with 3216 additions and 705 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"@trigger.dev/sdk": patch
---
Make the schema an optional param for customEvent and webhookEvent
+11 -6
View File
@@ -13,12 +13,17 @@ export function CopyText({
className,
onCopied,
}: CopyTextProps) {
const onClick = useCallback(() => {
navigator.clipboard.writeText(value);
if (onCopied) {
onCopied();
}
}, [value, onCopied]);
const onClick = useCallback(
(event: React.MouseEvent<HTMLDivElement>) => {
event.preventDefault();
event.stopPropagation();
navigator.clipboard.writeText(value);
if (onCopied) {
onCopied();
}
},
[value, onCopied]
);
return (
<div onClick={onClick} className={`${className}`}>
+49 -1
View File
@@ -1,6 +1,7 @@
import { ClipboardIcon } from "@heroicons/react/24/outline";
import { CheckIcon, ClipboardIcon } from "@heroicons/react/24/outline";
import classNames from "classnames";
import { useCallback, useState } from "react";
import { EnvironmentIcon } from "~/routes/resources/environment";
import { CopyText } from "./CopyText";
const variantStyle = {
@@ -16,6 +17,7 @@ const variantStyle = {
export type CopyTextButtonProps = {
value: string;
text?: string;
className?: string;
variant?: "slate" | "blue" | "darkTransparent" | "lightTransparent" | "text";
};
@@ -23,6 +25,7 @@ export type CopyTextButtonProps = {
export function CopyTextButton({
value,
className,
text,
variant = "blue",
}: CopyTextButtonProps) {
const [copied, setCopied] = useState(false);
@@ -57,3 +60,48 @@ export function CopyTextButton({
</CopyText>
);
}
const panelVariantStyle = {
primary:
"truncate text-indigo-300 bg-indigo-700/50 pl-3.5 pr-2 py-3 rounded border border-indigo-600 flex items-center justify-between gap-2 hover:cursor-pointer hover:bg-indigo-600/50 hover:border-indigo-600 transition",
slate:
"flex w-full items-center justify-between gap-2 truncate rounded bg-slate-850 py-2 pl-2.5 pr-1 transition hover:cursor-pointer hover:bg-slate-800 hover:text-slate-300",
text: "flex w-full items-center justify-between gap-2 truncate rounded bg-transparent py-2 pl-2.5 pr-1 transition hover:cursor-pointer hover:border-slate-700/50 hover:bg-slate-700/50",
};
export type CopyTextPanelProps = {
value: string;
text?: string;
className?: string;
variant?: "primary" | "slate" | "text";
};
export function CopyTextPanel({
value,
text,
className,
variant = "primary",
}: CopyTextPanelProps) {
const [copied, setCopied] = useState(false);
const onCopied = useCallback(() => {
setCopied(true);
setTimeout(() => {
setCopied(false);
}, 1500);
}, [setCopied]);
return (
<CopyText value={value} onCopied={onCopied} className="w-full">
{copied ? (
<div className={classNames(className, panelVariantStyle[variant])}>
<span className="truncate font-mono text-sm">{text ?? value}</span>
<CheckIcon className="h-5 w-5 min-w-[1.25rem] text-green-500" />
</div>
) : (
<div className={classNames(className, panelVariantStyle[variant])}>
<span className="truncate font-mono text-sm">{text ?? value}</span>
<ClipboardIcon className="h-4 w-4 min-w-[1.25rem]" />
</div>
)}
</CopyText>
);
}
@@ -1,6 +1,6 @@
import classNames from "classnames";
const baseClasses = "px-12 py-10";
const baseClasses = "px-4 py-4 md:px-8 md:py-6 lg:px-12 lg:py-10";
export function Container({
children,
@@ -1,5 +1,10 @@
import { Popover, Transition } from "@headlessui/react";
import { BookmarkIcon, ChevronUpDownIcon } from "@heroicons/react/24/outline";
import {
BookmarkIcon,
BuildingOffice2Icon,
ChevronUpDownIcon,
UserIcon,
} from "@heroicons/react/24/outline";
import { CheckIcon, PlusIcon } from "@heroicons/react/24/solid";
import { Link } from "@remix-run/react";
import classNames from "classnames";
@@ -33,10 +38,10 @@ export function OrganizationMenu() {
<Popover.Button
className={`
${open ? "" : "text-opacity-90"}
inline-flex justify-between items-center rounded text-white bg-transparent pl-2.5 pr-2 py-2 text-sm hover:bg-slate-800 focus:outline-none`}
inline-flex items-center justify-between rounded bg-transparent py-2 pl-2.5 pr-2 text-sm text-white hover:bg-slate-800 focus:outline-none`}
>
<BookmarkIcon
className={`h-5 w-5 mr-2 ${dimmedClassNames}`}
<BuildingOffice2Icon
className={`mr-2 h-5 w-5 ${dimmedClassNames}`}
aria-hidden="true"
/>
<span className="transition">
@@ -67,9 +72,9 @@ export function OrganizationMenu() {
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute left-0 z-30 mt-3 w-screen min-w-max max-w-xs max-h-[70vh] translate-x-0 transform px-4 sm:px-0">
<Popover.Panel className="absolute left-0 z-30 mt-3 max-h-[70vh] w-screen min-w-max max-w-xs translate-x-0 transform px-4 sm:px-0">
<div className="overflow-hidden rounded-lg ring-1 ring-black ring-opacity-5">
<div className="relative grid gap-y-1 py-1 bg-slate-700 grid-cols-1">
<div className="relative grid grid-cols-1 gap-y-1 bg-slate-700 py-1">
{organizations.map((organization) => {
return (
<Popover.Button
@@ -77,16 +82,23 @@ export function OrganizationMenu() {
as={Link}
to={`/orgs/${organization.slug}`}
className={classNames(
"flex items-center justify-between gap-1.5 mx-1 px-3 py-2 text-white rounded hover:bg-slate-800 transition",
"mx-1 flex items-center justify-between gap-1.5 rounded px-3 py-2 text-white transition hover:bg-slate-800",
organization.slug === currentOrganization?.slug &&
"!bg-slate-800"
)}
>
<div className="flex items-center gap-2">
<BookmarkIcon
className="h-5 w-5 z-100"
aria-hidden="true"
/>
{organization.title === "Personal Workspace" ? (
<UserIcon
className="z-100 h-5 w-5 text-slate-400"
aria-hidden="true"
/>
) : (
<BuildingOffice2Icon
className="z-100 h-5 w-5 text-slate-400"
aria-hidden="true"
/>
)}
<span className="block truncate">
{organization.title}
</span>
@@ -99,7 +111,7 @@ export function OrganizationMenu() {
);
})}
<Popover.Button as={Link} to={`/orgs/new`}>
<div className="flex items-center gap-2 mx-1 pl-2.5 py-2 rounded hover:bg-slate-800 transition">
<div className="mx-1 flex items-center gap-2 rounded py-2 pl-2.5 transition hover:bg-slate-800">
<PlusIcon
className="h-5 w-5 text-green-500"
aria-hidden="true"
@@ -1,29 +1,37 @@
import {
SquaresPlusIcon,
EyeIcon,
EyeSlashIcon,
InformationCircleIcon,
} from "@heroicons/react/20/solid";
import {
ArrowsRightLeftIcon,
UsersIcon,
ForwardIcon,
ChevronLeftIcon,
ArrowTopRightOnSquareIcon,
PhoneArrowUpRightIcon,
EnvelopeIcon,
BeakerIcon,
ClipboardDocumentCheckIcon,
Squares2X2Icon,
ChevronLeftIcon,
Cog6ToothIcon,
EnvelopeIcon,
ForwardIcon,
PhoneArrowUpRightIcon,
PlusCircleIcon,
Squares2X2Icon,
SquaresPlusIcon,
UsersIcon,
} from "@heroicons/react/24/outline";
import { Link, NavLink } from "@remix-run/react";
import { useState } from "react";
import invariant from "tiny-invariant";
import { useCurrentEnvironment } from "~/hooks/useEnvironments";
import {
useCurrentOrganization,
useOrganizations,
} from "~/hooks/useOrganizations";
import { useCurrentWorkflow } from "~/hooks/useWorkflows";
import { EnvironmentIcon } from "~/routes/resources/environment";
import { titleCase } from "~/utils";
import { CopyTextPanel } from "../CopyTextButton";
import { TertiaryA, TertiaryButton } from "../primitives/Buttons";
import { Body } from "../primitives/text/Body";
import { Header1 } from "../primitives/text/Headers";
import invariant from "tiny-invariant";
import { CopyText } from "../CopyText";
import { useCurrentEnvironment } from "~/hooks/useEnvironments";
export function SideMenuContainer({ children }: { children: React.ReactNode }) {
return <div className="grid h-full grid-cols-[300px_2fr]">{children}</div>;
@@ -160,6 +168,8 @@ function SideMenu({
const organization = useCurrentOrganization();
invariant(organization, "Organization must be defined");
const [isShowingKeys, setIsShowingKeys] = useState(false);
return (
<div className="flex min-h-0 flex-1 flex-col border-r border-slate-800 bg-slate-950">
<div className="flex flex-1 flex-col overflow-y-auto pb-4">
@@ -199,34 +209,59 @@ function SideMenu({
))}
</div>
<div className="flex flex-col gap-6">
<ul className="ml-3 mr-2 flex flex-col gap-6">
<ul className="ml-3 mr-2 flex flex-col gap-2">
<li className="flex w-full items-center justify-between">
<TertiaryA
href="https://docs.trigger.dev/guides/environments"
target="_blank"
className="group flex items-center gap-1 transition"
>
<InformationCircleIcon className="h-4 w-4 text-slate-500 transition group-hover:text-slate-400" />
<Body
size="extra-small"
className={`overflow-hidden text-slate-300 transition group-hover:text-slate-400 ${menuSmallTitleStyle}`}
>
API keys
</Body>
</TertiaryA>
{!isShowingKeys ? (
<TertiaryButton
onClick={() => setIsShowingKeys(true)}
className="group mr-1.5 transition before:text-xs before:text-slate-400 hover:before:content-['Show_keys']"
>
<EyeIcon className="h-4 w-4 text-slate-500 transition group-hover:text-slate-400" />
</TertiaryButton>
) : (
<TertiaryButton
onClick={() => setIsShowingKeys(false)}
className="group mr-1.5 transition before:text-xs before:text-slate-400 hover:before:content-['Hide_keys']"
>
<EyeSlashIcon className="h-4 w-4 text-slate-500 transition group-hover:text-slate-400" />
</TertiaryButton>
)}
</li>
{organization.environments.map((environment) => {
return (
<li
key={environment.id}
className="flex w-full flex-col justify-between gap-1.5"
className="flex w-full flex-col justify-between"
>
<div className="flex justify-between">
<Body
size="extra-small"
className={`overflow-hidden text-slate-300 ${menuSmallTitleStyle}`}
>
{environment.slug} api key
</Body>
{/* <CopyTextButton
variant="text"
<div className="relative flex items-center">
<EnvironmentIcon
slug={environment.slug}
className="absolute top-4 left-2"
/>
<CopyTextPanel
value={environment.apiKey}
/> */}
</div>
<div className="relative select-all overflow-hidden rounded-sm border border-slate-800 p-1 pl-2 text-sm text-slate-400">
<span className="pointer-events-none absolute right-7 top-0 block h-6 w-20 bg-gradient-to-r from-transparent to-slate-950"></span>
<CopyText
value={environment.apiKey}
className="group absolute right-0 top-0 flex h-full w-7 items-center justify-center rounded-sm border-l border-slate-800 bg-slate-950 transition hover:cursor-pointer hover:bg-slate-900 active:bg-green-900"
>
<ClipboardDocumentCheckIcon className="h-5 w-5 group-active:text-green-500" />
</CopyText>
{environment.apiKey}
text={
isShowingKeys
? environment.apiKey
: `${titleCase(environment.slug)}`
}
variant="slate"
className="pl-6 text-slate-300 hover:text-slate-300"
/>
</div>
</li>
);
@@ -1,8 +1,15 @@
import classNames from "classnames";
import { Header1 } from "./Headers";
export function Title({ children }: { children: React.ReactNode }) {
export function Title({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) {
return (
<Header1 size="extra-large" className="mb-6 text-slate-200">
<Header1 size="extra-large" className={classNames("mb-6 text-slate-200")}>
{children}
</Header1>
);
@@ -35,7 +35,7 @@ export function TemplateCard({
{template.description}
</Body>
</div>
<div className="flex flex-row gap-x-1">
<div className="flex gap-x-1">
{template.services.map((service) => (
<div key={service.service} className="">
<ApiLogoIcon
@@ -1,19 +1,21 @@
import { ArrowRightIcon } from "@heroicons/react/20/solid";
import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/outline";
import classNames from "classnames";
import { Fragment } from "react";
import type { TemplateListItem } from "~/presenters/templateListPresenter.server";
import { ApiLogoIcon } from "../code/ApiLogoIcon";
import { OctoKitty } from "../GitHubLoginButton";
import { TertiaryA, ToxicLink } from "../primitives/Buttons";
import { CopyTextPanel } from "../CopyTextButton";
import { SecondaryA } from "../primitives/Buttons";
import { Body } from "../primitives/text/Body";
import { Header1 } from "../primitives/text/Headers";
export function TemplateOverview({
template,
className,
commandFlags,
}: {
template: TemplateListItem;
className?: string;
commandFlags?: string;
}) {
const { docsHTML, imageUrl } = template;
@@ -21,21 +23,25 @@ export function TemplateOverview({
<div
className={classNames(
className,
"grid w-full grid-cols-1 gap-8 rounded-lg bg-slate-850 pl-8 text-left md:grid-cols-[20rem_minmax(0,_1fr)]"
"grid w-full grid-cols-1 gap-8 rounded-lg bg-slate-850 pl-8 text-left lg:grid-cols-[24rem_minmax(0,_1fr)]"
)}
>
<div className="sticky top-4 flex h-max flex-col rounded-r">
<TemplateDetails template={template} className="hidden md:flex" />
<div className="flex h-max flex-col rounded-r lg:sticky lg:top-4">
<TemplateDetails
template={template}
commandFlags={commandFlags}
className="hidden lg:flex"
/>
</div>
<div className="flex h-full w-full flex-col rounded">
<div className="z-90 h-fit w-full transition group-hover:opacity-90">
<div className="hidden h-fit w-full transition group-hover:opacity-90 lg:block">
<img
src={imageUrl}
alt=""
className="h-full w-full rounded-t object-cover"
alt="Template hero image"
className="h-full w-full rounded-t-md object-cover"
/>
</div>
<TemplateDetails template={template} className="md:hidden" />
<TemplateDetails template={template} className="lg:hidden" />
<div className="flex rounded-b bg-slate-900/75 p-8">
<div
className="prose prose-sm prose-invert min-w-full [&>pre]:bg-[rgb(17,23,41)]"
@@ -52,9 +58,11 @@ export function TemplateOverview({
function TemplateDetails({
className,
template,
commandFlags,
}: {
className?: string;
template: TemplateListItem;
commandFlags?: string;
}) {
const { title, description, repositoryUrl, id } = template;
return (
@@ -74,7 +82,7 @@ function TemplateDetails({
</Body>
<div className="ml-2 h-px w-full bg-slate-800" />
</div>
<div className="mb-4 flex gap-x-1">
<div className="mb-6 flex gap-x-1">
{template.services.map((service) => (
<Fragment key={service.service}>
<ApiLogoIcon
@@ -92,26 +100,50 @@ function TemplateDetails({
<div className="mb-2 flex items-center">
<Body
size="extra-small"
className="uppercase tracking-wide text-slate-500"
className="whitespace-nowrap uppercase tracking-wide text-slate-500"
>
Repo
Help and guides
</Body>
<div className="ml-2 h-px w-full bg-slate-800" />
</div>
<TertiaryA href={repositoryUrl} target="_blank" className="mb-8">
<OctoKitty className="h-4 w-4" />
<Body size="small" className="truncate font-mono">
{repositoryUrl.replace("https://github.com/triggerdotdev", "")}
<div className="mb-8 grid grid-cols-2 gap-2">
<SecondaryA
href={repositoryUrl}
target="_blank"
className="!max-w-full"
>
View Repo
<ArrowTopRightOnSquareIcon className="ml-1 h-4 w-4" />
</SecondaryA>
<SecondaryA
href="https://docs.trigger.dev"
target="_blank"
className="!max-w-full"
>
View Docs
<ArrowTopRightOnSquareIcon className="ml-1 h-4 w-4" />
</SecondaryA>
</div>
<div className="mb-2 flex items-center">
<Body
size="extra-small"
className="whitespace-nowrap uppercase tracking-wide text-slate-500"
>
Get started
</Body>
</TertiaryA>
<ToxicLink
size="large"
className="group flex h-12 min-w-full"
to={`../../templates/add?templateId=${id}`}
>
<span> Use this template </span>
<ArrowRightIcon className="ml-1 h-5 w-5 transition group-hover:translate-x-0.5" />
</ToxicLink>
<div className="ml-2 h-px w-full bg-slate-800" />
</div>
<Body className="mb-4 text-slate-400">
Run this command in your terminal to create a new project using this
template.
</Body>
<CopyTextPanel
text={`npm create trigger ${template.slug}`}
value={`npm create trigger@latest ${template.slug} ${
commandFlags ? ` ${commandFlags}` : ""
}`}
className="mb-8"
/>
</div>
);
}
@@ -1,19 +1,21 @@
import { XCircleIcon } from "@heroicons/react/24/solid";
import { XMarkIcon } from "@heroicons/react/24/outline";
import { Link } from "@remix-run/react";
import classNames from "classnames";
import { Fragment, useState } from "react";
import type { TemplateListItem } from "~/presenters/templateListPresenter.server";
import { ApiLogoIcon } from "../code/ApiLogoIcon";
import { CopyTextPanel } from "../CopyTextButton";
import { StyledDialog } from "../primitives/Dialog";
import { Body } from "../primitives/text/Body";
import { Header1 } from "../primitives/text/Headers";
import { TemplateOverview } from "./TemplateOverview";
export function TemplatesGrid({
templates,
openInNewPage,
commandFlags,
}: {
templates: Array<TemplateListItem>;
openInNewPage: boolean;
commandFlags?: string;
}) {
const [openedTemplate, setOpenedTemplate] = useState<TemplateListItem | null>(
null
@@ -28,17 +30,22 @@ export function TemplatesGrid({
show={isOpen}
as={Fragment}
>
<StyledDialog.Panel className="relative mx-auto flex max-h-[80vh] max-w-5xl items-start gap-2 overflow-hidden overflow-y-auto rounded-md">
{openedTemplate && <TemplateOverview template={openedTemplate} />}
<StyledDialog.Panel className="relative mx-auto flex max-h-[80vh] max-w-6xl items-start gap-2 overflow-hidden overflow-y-auto rounded-md border border-slate-700">
{openedTemplate && (
<TemplateOverview
template={openedTemplate}
commandFlags={commandFlags}
/>
)}
<button
onClick={() => setOpenedTemplate(null)}
className="sticky top-0 text-slate-600 transition hover:text-slate-500"
className="group sticky top-2 -ml-[48px] rounded text-slate-400 transition hover:bg-slate-800/70 hover:text-slate-500"
>
<XCircleIcon className="h-10 w-10" />
<XMarkIcon className="h-8 w-8 transition group-hover:text-slate-300" />
</button>
</StyledDialog.Panel>
</StyledDialog.Dialog>
<div className="grid w-full grid-cols-1 items-start justify-start gap-4 md:grid-cols-2 lg:grid-cols-3">
<div className="grid w-full grid-cols-1 items-start justify-start gap-5 md:grid-cols-2 lg:grid-cols-3">
{templates.map((template) => {
return (
<TemplateButtonOrLink
@@ -46,34 +53,26 @@ export function TemplatesGrid({
template={template}
openInNewPage={openInNewPage}
onClick={() => setOpenedTemplate(template)}
className="p-5"
>
<div className="h-32 w-full bg-slate-600 transition group-hover:opacity-90">
<div className="w-full transition group-hover:opacity-90 group-hover:shadow-lg">
<img
src={template.imageUrl}
alt=""
className="h-32 w-full object-cover"
alt={template.title}
className="h-32 w-full rounded-md object-cover"
/>
</div>
<div className="flex h-full flex-col place-content-between p-4">
<div className="flex flex-col gap-y-2 ">
<Header1 size="small" className="font-semibold">
{template.title}
</Header1>
<Body size="small" className="text-slate-400">
{template.description}
</Body>
</div>
<div className="mt-2 flex flex-row gap-x-1">
{template.services.map((service) => (
<div key={service.service} className="">
<ApiLogoIcon
integration={service}
size="regular"
className="mt-2 flex h-8 w-8 items-center justify-center rounded border-[1px] border-slate-700 bg-slate-900 transition group-hover:border-slate-600 group-hover:bg-slate-900/80"
/>
</div>
))}
</div>
<div className="flex h-full w-full flex-col justify-between">
<Header1 size="regular" className="py-6 text-slate-100">
{template.title}
</Header1>
<CopyTextPanel
value={`npm create trigger@latest ${template.slug}${
commandFlags ? ` ${commandFlags}` : ``
}`}
text={`npm create trigger ${template.slug}`}
className=""
/>
</div>
</TemplateButtonOrLink>
);
@@ -88,18 +87,20 @@ function TemplateButtonOrLink({
openInNewPage,
onClick,
children,
className,
}: {
template: TemplateListItem;
openInNewPage: boolean;
onClick: (e: React.MouseEvent) => void;
children: React.ReactNode;
className?: string;
}) {
const classNames =
"group flex w-full flex-col self-stretch overflow-hidden rounded-md border border-slate-700 bg-slate-800 text-left text-slate-200 shadow-md transition hover:cursor-pointer hover:border-slate-500 hover:bg-slate-700/30 disabled:opacity-50";
const cardStyles =
"group flex w-full p-5 flex-col self-stretch overflow-hidden rounded-md border border-slate-700/70 bg-slate-800 text-left text-slate-200 shadow-md transition hover:cursor-pointer hover:border-slate-600 hover:bg-slate-700/50 disabled:opacity-50";
if (openInNewPage) {
return (
<Link to={template.slug} className={classNames}>
<Link to={template.slug} className={classNames(cardStyles, className)}>
{children}
</Link>
);
@@ -109,7 +110,7 @@ function TemplateButtonOrLink({
key={template.title}
type="button"
onClick={onClick}
className="group flex w-full flex-col self-stretch overflow-hidden rounded-md border border-slate-700 bg-slate-800 text-left text-slate-200 shadow-md transition hover:cursor-pointer hover:border-slate-500 hover:bg-slate-700/30 disabled:opacity-50"
className={cardStyles}
>
{children}
</button>
@@ -0,0 +1,102 @@
import { InformationCircleIcon } from "@heroicons/react/20/solid";
import {
ArrowTopRightOnSquareIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
import { Fragment, useState } from "react";
import type { TemplateListItem } from "~/presenters/templateListPresenter.server";
import { CopyTextPanel } from "../CopyTextButton";
import { PrimaryA, TertiaryButton } from "../primitives/Buttons";
import { StyledDialog } from "../primitives/Dialog";
import { Body } from "../primitives/text/Body";
import { Header4 } from "../primitives/text/Headers";
import { SubTitle } from "../primitives/text/SubTitle";
import { TemplatesGrid } from "../templates/TemplatesGrid";
export function WorkflowOnboarding({
apiKey,
templates,
}: {
apiKey: string;
templates: TemplateListItem[];
}) {
let [isOpen, setIsOpen] = useState(false);
return (
<>
<StyledDialog.Dialog
onClose={(e) => setIsOpen(false)}
appear
show={isOpen}
as={Fragment}
>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center">
<StyledDialog.Panel className="mx-auto flex max-w-2xl flex-col justify-center overflow-hidden rounded-md border border-slate-700 bg-slate-850 text-left">
<div className="flex w-full items-center justify-between py-3 pr-3 pl-5">
<div className="flex w-full items-center gap-2">
<Header4 size="small" className="text-slate-300">
Setup Trigger.dev manually
</Header4>
</div>
<button
onClick={(e) => setIsOpen(false)}
className="group rounded p-1 text-slate-600 transition hover:bg-slate-700 hover:text-slate-500"
>
<XMarkIcon className="h-6 w-6 text-slate-600 transition group-hover:text-slate-400" />
</button>
</div>
<div className="flex h-full w-full flex-col overflow-hidden bg-slate-800 p-5">
<div className="grid grid-cols-[minmax(0,_1fr),_4rem,_minmax(0,_1fr)]">
<div className="flex h-full flex-col justify-between">
<Body className="mb-4 text-slate-400">
Add Trigger.dev to an existing Node.js repo.
</Body>
<PrimaryA
href="https://docs.trigger.dev/getting-started#manual-setup"
target="_blank"
>
Manual setup docs
<ArrowTopRightOnSquareIcon className="ml-1 h-4 w-4" />
</PrimaryA>
</div>
<div className="flex flex-col items-center gap-2">
<div className="h-full w-px bg-slate-700"></div>
<Body size="small" className="uppercase text-slate-600">
or
</Body>
<div className="h-full w-px bg-slate-700"></div>
</div>
<div className="flex h-full flex-col justify-between">
<Body className="mb-4 text-slate-400">
Setup a new Node.js project ready for Trigger.dev by
running this command.
</Body>
<CopyTextPanel
value={`npm create trigger@latest --apiKey ${apiKey}`}
text={`npm create trigger --apiKey ${apiKey}`}
/>
</div>
</div>
</div>
</StyledDialog.Panel>
</div>
</div>
</StyledDialog.Dialog>
<div className="mb-2 flex w-full items-center justify-between">
<SubTitle className="mb-0">Get started with a template</SubTitle>
<TertiaryButton onClick={(e) => setIsOpen(true)}>
<InformationCircleIcon className="h-4 w-4" />
Setup manually instead
</TertiaryButton>
</div>
<div>
<TemplatesGrid
openInNewPage={false}
templates={templates}
commandFlags={`-k ${apiKey}`}
/>
</div>
</>
);
}
+2
View File
@@ -0,0 +1,2 @@
export const LIVE_ENVIRONMENT = "live";
export const DEV_ENVIRONMENT = "development";
+15
View File
@@ -1,3 +1,4 @@
import { DEV_ENVIRONMENT } from "~/consts";
import type { RuntimeEnvironment } from "~/models/runtimeEnvironment.server";
import { useMatchesData } from "~/utils";
@@ -48,3 +49,17 @@ export function useCurrentEnvironment(): RuntimeEnvironment | undefined {
);
return currentEnvironment;
}
export function useDevEnvironment(): RuntimeEnvironment | undefined {
const routeMatch = useMatchesData("routes/__app/orgs/$organizationSlug");
if (
!routeMatch ||
!isRuntimeEnvironments(routeMatch.data.organization.environments)
) {
return undefined;
}
return routeMatch.data.organization.environments.find(
(environment: any) => environment.slug === DEV_ENVIRONMENT
);
}
+7 -6
View File
@@ -3,11 +3,12 @@ import type { loader as appLoader } from "~/routes/__app";
import type { loader as orgLoader } from "~/routes/__app/orgs/$organizationSlug";
import { hydrateObject, useMatchesData } from "~/utils";
export type MatchedOrganization = UseDataFunctionReturn<
typeof appLoader
>["organizations"][number];
export function useOrganizations() {
return (
getOrganizationsFromMatchesData("routes/__app") ??
getOrganizationsFromMatchesData("routes/__public")
);
return useOrganizationsFromMatchesData(["routes/__app", "routes/__public"]);
}
export function useCurrentOrganization() {
@@ -34,8 +35,8 @@ export function useIsNewOrganizationPage(): boolean {
return !!routeMatch;
}
function getOrganizationsFromMatchesData(path: string) {
const routeMatch = useMatchesData(path);
function useOrganizationsFromMatchesData(paths: string[]) {
const routeMatch = useMatchesData(paths);
if (!routeMatch || !routeMatch.data.organizations) {
return undefined;
@@ -4,6 +4,7 @@ import slug from "slug";
import { customAlphabet } from "nanoid";
import { generateTwoRandomWords } from "~/utils/randomWords";
import { taskQueue } from "~/services/messageBroker.server";
import { DEV_ENVIRONMENT, LIVE_ENVIRONMENT } from "~/consts";
export type { Organization } from ".prisma/client";
@@ -67,6 +68,11 @@ export function getOrganizations({ userId }: { userId: User["id"] }) {
return prisma.organization.findMany({
where: { users: { some: { id: userId } } },
orderBy: { createdAt: "desc" },
include: {
environments: {
orderBy: { slug: "asc" },
},
},
});
}
@@ -144,8 +150,8 @@ export async function createOrganization({
if (organization) {
// Create the dev and prod environments
await createEnvironment(organization, "development");
await createEnvironment(organization, "live");
await createEnvironment(organization, DEV_ENVIRONMENT);
await createEnvironment(organization, LIVE_ENVIRONMENT);
await taskQueue.publish("ORGANIZATION_CREATED", {
id: organization.id,
@@ -5,6 +5,7 @@ import { renderMarkdown } from "~/services/renderMarkdown.server";
import type { TemplateListItem } from "./templateListPresenter.server";
import { WorkflowsPresenter } from "../presenters/workflowsPresenter.server";
import { getServiceMetadatas } from "~/models/integrations.server";
import { DEV_ENVIRONMENT, LIVE_ENVIRONMENT } from "~/consts";
export class OrganizationTemplatePresenter {
#prismaClient: PrismaClient;
@@ -71,10 +72,10 @@ export class OrganizationTemplatePresenter {
const developmentApiKey =
organizationTemplate.organization.environments.find(
(e) => e.slug === "development"
(e) => e.slug === DEV_ENVIRONMENT
)?.apiKey;
const liveApiKey = organizationTemplate.organization.environments.find(
(e) => e.slug === "live"
(e) => e.slug === LIVE_ENVIRONMENT
)?.apiKey;
return {
@@ -20,6 +20,7 @@ export class TemplateListPresenter {
async data(): Promise<{ templates: Array<TemplateListItem> }> {
const templates = await this.#prismaClient.template.findMany({
orderBy: { priority: "asc" },
where: { isLive: true },
});
const serviceMetadatas = await getServiceMetadatas(true);
@@ -3,10 +3,11 @@ import type { PrismaClient } from "~/db.server";
import { prisma } from "~/db.server";
import { getRuntimeEnvironment } from "~/models/runtimeEnvironment.server";
import { WorkflowsPresenter } from "../presenters/workflowsPresenter.server";
import { TemplateListPresenter } from "./templateListPresenter.server";
export type WorkflowListItem = Awaited<
ReturnType<WorkflowListPresenter["data"]>
>[number];
>["workflows"][number];
export class WorkflowListPresenter {
#prismaClient: PrismaClient;
@@ -28,11 +29,19 @@ export class WorkflowListPresenter {
});
invariant(runtimeEnvironment, "Runtime environment not found");
const templatesPresenter = new TemplateListPresenter();
const workflowsPresenter = new WorkflowsPresenter();
return workflowsPresenter.data(
const workflows = await workflowsPresenter.data(
{ organization: { slug: organizationSlug }, isArchived: false },
runtimeEnvironment.id
);
const { templates } = await templatesPresenter.data();
return {
workflows,
templates,
};
}
}
@@ -45,6 +45,7 @@ export class WorkflowStartPresenter {
orderBy: {
priority: "asc",
},
where: { isLive: true },
});
return {
+50 -15
View File
@@ -1,17 +1,29 @@
import { BookmarkIcon, PlusIcon } from "@heroicons/react/24/outline";
import {
BuildingOffice2Icon,
PlusIcon,
UserIcon,
} from "@heroicons/react/24/outline";
import { Link } from "@remix-run/react";
import classNames from "classnames";
import { CopyTextPanel } from "~/components/CopyTextButton";
import { Body } from "~/components/primitives/text/Body";
import { Header4 } from "~/components/primitives/text/Headers";
import type { MatchedOrganization } from "~/hooks/useOrganizations";
import { useOrganizations } from "~/hooks/useOrganizations";
import type { Organization } from "~/models/organization.server";
import { environmentShortName } from "~/utils";
export default function AppLayout() {
const organizations = useOrganizations();
return (
<>
<div className="flex items-center justify-center m-20">
<ul className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 max-w-8xl gap-2">
<div className="flex h-80 w-full items-center justify-center bg-slate-900/50">
<h1 className="relative bottom-6 text-4xl text-slate-400">
Your Organizations
</h1>
</div>
<div className="flex items-center justify-center">
<ul className="-mt-24 grid max-w-7xl grid-cols-2 gap-2 lg:grid-cols-3">
{organizations ? (
<OrganizationGrid organizations={organizations} />
) : (
@@ -23,12 +35,14 @@ export default function AppLayout() {
<Link
to="orgs/new"
className={classNames(
"border-2 border-slate-800 text-center hover:border-transparent hover:bg-slate-800/50 hover:shadow-md",
"h-full border border-slate-700 hover:border-transparent hover:bg-[rgb(38,51,71)] hover:shadow-md",
boxClasses
)}
>
<PlusIcon className="h-10 w-10 text-green-500" />
New Organization
<Header4 size="small" className="mb-10">
New Organization
</Header4>
</Link>
</li>
</ul>
@@ -40,7 +54,7 @@ export default function AppLayout() {
function OrganizationGrid({
organizations,
}: {
organizations: Organization[];
organizations: MatchedOrganization[];
}) {
return (
<>
@@ -54,26 +68,47 @@ function OrganizationGrid({
);
}
const boxClasses =
"flex flex-col gap-4 items-center justify-center min-h-40 rounded-lg px-6 py-6 min-h-[15rem] transition";
function OrganizationGridItem({
organization,
}: {
organization: Organization;
organization: MatchedOrganization;
}) {
return (
<li key={organization.id} className="w-full h-full">
<li key={organization.id} className="h-full w-full">
<Link
to={`orgs/${organization.slug}`}
className={classNames(
"bg-slate-800 shadow-md text-center hover:bg-slate-800/50",
"border border-slate-700 bg-slate-800 hover:bg-[rgb(38,51,71)]",
boxClasses
)}
>
<BookmarkIcon className="h-10 w-10" />
{organization.title}{" "}
{organization.title === "Personal Workspace" ? (
<UserIcon className="h-10 w-10 text-slate-300" aria-hidden="true" />
) : (
<BuildingOffice2Icon
className="h-10 w-10 text-blue-500"
aria-hidden="true"
/>
)}
<Header4 size="large" className="mb-10 text-slate-300">
{organization.title}
</Header4>
<div className="grid w-full grid-cols-2 gap-2">
{organization.environments.map((environment) => (
<div key={environment.id} className="flex w-full items-center">
<CopyTextPanel
value={environment.apiKey}
text={`${environmentShortName(environment.slug)} API Key`}
variant="slate"
className="w-full text-slate-500"
/>
</div>
))}
</div>
</Link>
</li>
);
}
const boxClasses =
"flex flex-col gap-4 w-80 text-center shadow-md items-center justify-center rounded-lg px-2 pb-2 pt-14 min-h-full transition";
@@ -3,11 +3,11 @@ import { typedjson, useTypedLoaderData } from "remix-typedjson";
import invariant from "tiny-invariant";
import { CreateNewWorkflow } from "~/components/CreateNewWorkflow";
import { Container } from "~/components/layout/Container";
import { PanelInfo } from "~/components/layout/PanelInfo";
import { PrimaryLink } from "~/components/primitives/Buttons";
import { SubTitle } from "~/components/primitives/text/SubTitle";
import { Title } from "~/components/primitives/text/Title";
import { WorkflowList } from "~/components/workflows/workflowList";
import { WorkflowOnboarding } from "~/components/workflows/WorkflowOnboarding";
import { useDevEnvironment } from "~/hooks/useEnvironments";
import { useCurrentOrganization } from "~/hooks/useOrganizations";
import { getRuntimeEnvironmentFromRequest } from "~/models/runtimeEnvironment.server";
import { WorkflowListPresenter } from "~/presenters/workflowListPresenter.server";
@@ -22,8 +22,7 @@ export const loader = async ({ request, params }: LoaderArgs) => {
const presenter = new WorkflowListPresenter();
try {
const workflows = await presenter.data(params.organizationSlug, currentEnv);
return typedjson({ workflows });
return typedjson(await presenter.data(params.organizationSlug, currentEnv));
} catch (error: any) {
console.error(error);
throw new Response("Error ", { status: 400 });
@@ -31,30 +30,33 @@ export const loader = async ({ request, params }: LoaderArgs) => {
};
export default function Page() {
const { workflows } = useTypedLoaderData<typeof loader>();
const { workflows, templates } = useTypedLoaderData<typeof loader>();
const currentOrganization = useCurrentOrganization();
const currentEnv = useDevEnvironment();
if (currentOrganization === undefined) {
return <></>;
}
if (currentEnv === undefined) {
return <></>;
}
return (
<Container>
<Title>Workflows</Title>
{workflows.length === 0 ? (
<>
<SubTitle>0 workflows</SubTitle>
<PanelInfo
message="You don't have any workflows yet. They will appear here once
connected."
className="mb-4 max-w-4xl p-4 pr-6"
>
<PrimaryLink to={`/orgs/${currentOrganization.slug}/workflows/new`}>
Create first workflow
</PrimaryLink>
</PanelInfo>
<Title>Create your first workflow</Title>
<div className="max-w-6xl">
<WorkflowOnboarding
templates={templates}
apiKey={currentEnv.apiKey}
/>
</div>
</>
) : (
<>
<Title>Workflows</Title>
<SubTitle>
{workflows.length} active workflow{workflows.length > 1 ? "s" : ""}
</SubTitle>
@@ -109,7 +109,7 @@ export default function Integrations() {
</div>
<div className="flex flex-col items-center justify-center text-center leading-relaxed text-slate-400">
<span className="px-2.5">Need an integration?</span>
<span className="px-6 text-base text-slate-200">
<span className="mb-4 px-6 text-base text-slate-200">
Let us know!
</span>
</div>
@@ -1,125 +1,29 @@
import CheckIcon from "@heroicons/react/20/solid/CheckIcon";
import {
CloudIcon,
HomeIcon,
RocketLaunchIcon,
} from "@heroicons/react/24/outline";
import XCircleIcon from "@heroicons/react/24/solid/XCircleIcon";
import { Link, useFetcher } from "@remix-run/react";
import classNames from "classnames";
import { Fragment, useEffect, useState } from "react";
import { Panel } from "~/components/layout/Panel";
import { onboarding } from "~/components/onboarding/classNames";
import { StepNumber } from "~/components/onboarding/StepNumber";
import { PrimaryButton } from "~/components/primitives/Buttons";
import { StyledDialog } from "~/components/primitives/Dialog";
import { Body } from "~/components/primitives/text/Body";
import { Header3 } from "~/components/primitives/text/Headers";
import { SubTitle } from "~/components/primitives/text/SubTitle";
import { useUser } from "~/hooks/useUser";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { WorkflowOnboarding } from "~/components/workflows/WorkflowOnboarding";
import { useDevEnvironment } from "~/hooks/useEnvironments";
import { useCurrentOrganization } from "~/hooks/useOrganizations";
import { TemplateListPresenter } from "~/presenters/templateListPresenter.server";
export const loader = async () => {
const presenter = new TemplateListPresenter();
return typedjson(await presenter.data());
};
export default function NewWorkflowStep1Page() {
return <Step1 />;
}
const { templates } = useTypedLoaderData<typeof loader>();
const currentOrganization = useCurrentOrganization();
const currentEnv = useDevEnvironment();
function Step1() {
const user = useUser();
const fetcher = useFetcher();
let [isOpen, setIsOpen] = useState(false);
useEffect(() => {
if (fetcher.state === "submitting") {
setIsOpen(false);
}
}, [fetcher.state, setIsOpen]);
if (currentOrganization === undefined) {
return <></>;
}
if (currentEnv === undefined) {
return <></>;
}
return (
<>
<StyledDialog.Dialog
onClose={(e) => setIsOpen(false)}
appear
show={isOpen}
as={Fragment}
>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4">
<StyledDialog.Panel className="mx-auto flex max-w-xl items-start gap-2 overflow-hidden">
<div className="flex h-full w-full flex-col overflow-hidden rounded-md bg-slate-800 text-left">
<div className="relative flex flex-col items-center justify-between gap-5 overflow-hidden border-b border-slate-850/80 bg-blue-400 px-4 py-12">
<CloudIcon className="absolute top-2 -left-4 h-28 w-28 animate-pulse text-white/70" />
<CloudIcon className="absolute top-16 right-16 h-16 w-16 animate-pulse text-white/70" />
<RocketLaunchIcon className="h-20 w-20 animate-[float_3s_ease-in-out_infinite] text-slate-800" />
<Header3 className="font-semibold">
Cloud hosting coming soon
</Header3>
</div>
<div className="p-6">
<Body className="mb-4 text-slate-400">
We're preparing to launch a cloud hosting service for your
Trigger.dev workflows that will make it as easy to deploy
your workflows as a git push.
</Body>
<div className="flex w-full justify-end">
<fetcher.Form
action="/resources/cloud-waitlist"
method="post"
>
{user.isOnCloudWaitlist ? (
<PrimaryButton
type="submit"
className="mt-2 w-full"
disabled
>
<CheckIcon className="-m-1 h-4 w-4 text-green-500" />
Already on the waitlist
</PrimaryButton>
) : (
<PrimaryButton type="submit" className="mt-2 w-full">
Notify me when it's ready
</PrimaryButton>
)}
</fetcher.Form>
</div>
</div>
</div>
<button
onClick={() => setIsOpen(false)}
className="sticky top-0 text-slate-300 transition hover:text-slate-200"
>
<XCircleIcon className="h-10 w-10" />
</button>
</StyledDialog.Panel>
</div>
</div>
</StyledDialog.Dialog>
<div className={classNames(onboarding.maxWidth, "mb-6")}>
<SubTitle className="flex items-center">
<StepNumber active stepNumber="1" />
Where do you want your workflow hosted?
</SubTitle>
<Panel className="flex w-full items-center justify-between">
<div className="grid w-full grid-cols-2 gap-x-4">
<Link to="step2" className={onboarding.buttonStyles}>
<HomeIcon className="h-10 w-10 text-green-400" />
<Header3>I'll host the workflow myself</Header3>
<Body size="small" className="text-slate-400">
I will deploy the code to my own servers.
</Body>
</Link>
<button
type="button"
onClick={(e) => setIsOpen(true)}
className={onboarding.buttonStyles}
>
<CloudIcon className="h-10 w-10 text-blue-400" />
<Header3>Host the workflow for me in the cloud</Header3>
<Body size="small" className="text-slate-400">
Trigger.dev can host and handle the servers for me.
</Body>
</button>
</div>
</Panel>
</div>
</>
<div className="max-w-6xl">
<WorkflowOnboarding templates={templates} apiKey={currentEnv.apiKey} />
</div>
);
}
@@ -1,35 +0,0 @@
import classNames from "classnames";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { BackToStep1, BackToStep2 } from "~/components/onboarding/BackToSteps";
import { onboarding } from "~/components/onboarding/classNames";
import { StepNumber } from "~/components/onboarding/StepNumber";
import { SubTitle } from "~/components/primitives/text/SubTitle";
import { TemplatesGrid } from "~/components/templates/TemplatesGrid";
import { TemplateListPresenter } from "~/presenters/templateListPresenter.server";
export const loader = async () => {
const presenter = new TemplateListPresenter();
return typedjson(await presenter.data());
};
export default function Step3NewRepo1() {
const { templates } = useTypedLoaderData<typeof loader>();
return (
<div className={classNames(onboarding.maxWidth)}>
<div className="flex items-center justify-between">
<BackToStep1 />
</div>
<div className="flex items-center justify-between">
<BackToStep2 text="I'll start with a template" />
</div>
<div className="mb-6">
<SubTitle className="flex items-center">
<StepNumber active stepNumber="3" />
Which template would you like to use?
</SubTitle>
<TemplatesGrid templates={templates} openInNewPage={false} />
</div>
</div>
);
}
@@ -41,6 +41,7 @@ import { RunsTable } from "~/components/runs/RunsTable";
import { TriggerBody } from "~/components/triggers/Trigger";
import { TriggerTypeIcon } from "~/components/triggers/TriggerIcons";
import { triggerLabel } from "~/components/triggers/triggerLabel";
import { DEV_ENVIRONMENT } from "~/consts";
import { useConnectionSlots } from "~/hooks/useConnectionSlots";
import { useCurrentEnvironment } from "~/hooks/useEnvironments";
import { useCurrentOrganization } from "~/hooks/useOrganizations";
@@ -107,7 +108,7 @@ export default function Page() {
className="mb-6"
message={`This workflow hasn't been connected in the ${environment.slug} environment yet.`}
></PanelWarning>
{environment.slug === "development" ? (
{environment.slug === DEV_ENVIRONMENT ? (
<ConnectToDevelopmentInstructions environment={environment} />
) : (
<ConnectToLiveInstructions environment={environment} />
@@ -399,7 +400,7 @@ export default function Page() {
<Disclosure.Panel className="p-6">
<div className="mb-1 flex items-baseline gap-2">
<SubTitle className="text-slate-300">
Trigger your workflow from the source
Trigger your workflow for real
</SubTitle>
<span className="relative -top-0.5 rounded-full bg-blue-700 px-2 pt-1 pb-0.5 text-[0.6rem] font-medium uppercase tracking-wider text-blue-200">
Recommended
@@ -9,9 +9,10 @@ import { PanelInfo } from "~/components/layout/PanelInfo";
import { PanelWarning } from "~/components/layout/PanelWarning";
import { PrimaryButton, TertiaryLink } from "~/components/primitives/Buttons";
import { Select } from "~/components/primitives/Select";
import { SubTitle } from "~/components/primitives/text/SubTitle";
import { Title } from "~/components/primitives/text/Title";
import { useCurrentOrganization } from "~/hooks/useOrganizations";
import { useCurrentWorkflow } from "~/hooks/useWorkflows";
import { CurrentWorkflow, useCurrentWorkflow } from "~/hooks/useWorkflows";
import { getRuntimeEnvironmentFromRequest } from "~/models/runtimeEnvironment.server";
import { WorkflowTestPresenter } from "~/presenters/testPresenter.server";
import { requireUserId } from "~/services/session.server";
@@ -64,14 +65,17 @@ export default function Page() {
</TertiaryLink>
</PanelInfo>
) : (
<Panel className="mt-4">
<Tester
organizationSlug={organization.slug}
workflowSlug={workflow.slug}
eventNames={workflow.eventNames}
initialValue={JSON.stringify(payload, null, 2)}
/>
</Panel>
<>
<SubTitle>{workflowType(workflow)}</SubTitle>
<Panel>
<Tester
organizationSlug={organization.slug}
workflowSlug={workflow.slug}
eventNames={workflow.eventNames}
initialValue={JSON.stringify(payload, null, 2)}
/>
</Panel>
</>
)}
</>
);
@@ -135,3 +139,16 @@ function Tester({
</div>
);
}
function workflowType(workflow: CurrentWorkflow) {
switch (workflow?.type) {
case "WEBHOOK":
return "This test will simulate receiving this JSON payload for this webhook.";
case "SCHEDULE":
return "This test will simulate receiving a scheduled trigger from this datetime string.";
case "CUSTOM_EVENT":
return "This test will simulate receiving this JSON payload for this custom event.";
default:
return "This workflow hasn't been connected.";
}
}
@@ -1,10 +1,9 @@
import { Outlet } from "@remix-run/react";
import { Container } from "~/components/layout/Container";
export default function TemplatesLayout() {
return (
<Container>
<div className="px-4 py-6 md:px-8 md:py-6 lg:px-12 lg:py-10">
<Outlet />
</Container>
</div>
);
}
@@ -26,7 +26,7 @@ export default function TemplateSlugPage() {
<div className="mx-auto max-w-[1188px]">
<Link
to="/templates"
className="mb-4 -ml-1 flex w-max items-center justify-start gap-2 text-sm text-slate-500 transition hover:text-slate-300"
className="mb-4 ml-4 flex w-max items-center justify-start gap-2 text-sm text-slate-500 transition hover:text-slate-300 lg:-ml-1"
>
<ArrowLeftIcon className="h-3 w-3" />
Choose a different Template
@@ -15,10 +15,10 @@ export default function TemplateList() {
return (
<div className="mx-auto mt-4 flex max-w-6xl flex-col lg:mt-6">
<h1 className="mb-6 text-center font-title text-5xl font-semibold text-slate-200">
Choose your Template
Choose a template
</h1>
<Header2 size="small" className="mb-8 text-center text-slate-400">
Quickly get started with your workflow by using a pre-built example.
<Header2 size="small" className="mb-16 text-center text-slate-400">
Quickly get started with your workflow by using a pre-built template.
</Header2>
<TemplatesGrid templates={templates} openInNewPage={true} />
</div>
@@ -0,0 +1,41 @@
import type { ActionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { z } from "zod";
import { analytics } from "~/services/analytics.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
const BodySchema = z.object({
id: z.string(),
event: z.string(),
properties: z.record(z.union([z.string(), z.number()]), z.any()),
});
export async function action({ request }: ActionArgs) {
// first make sure this is a POST request
if (request.method.toUpperCase() !== "POST") {
return json({ error: "Method not allowed" }, { status: 405 });
}
const rawBody = await request.json();
const body = BodySchema.parse(rawBody);
// Next authenticate the request
const authenticatedEnv = await authenticateApiRequest(request);
const event = {
userId: body.id,
event: body.event,
properties: {
...body.properties,
environmentType: authenticatedEnv?.slug,
},
organizationId: authenticatedEnv?.organizationId,
environmentId: authenticatedEnv?.id,
};
console.log("Capturing event", event);
analytics.telemetry.capture(event);
return json({ status: "OK" });
}
@@ -14,5 +14,6 @@ export async function loader({ request }: LoaderArgs) {
return json({
organizationId: authenticatedEnv.organizationId,
env: authenticatedEnv.slug,
organizationSlug: authenticatedEnv.organization.slug,
});
}
@@ -37,8 +37,10 @@ export async function action({ request, params }: ActionArgs) {
case "validationError": {
return json({ error: result.errors }, { status: 400 });
}
case "isArchived": {
return json({ id: result.data.id });
}
case "success":
return json(result.data);
return json({ id: result.data.workflow.id });
}
}
@@ -0,0 +1,11 @@
import { json } from "@remix-run/server-runtime";
import { prisma } from "~/db.server";
export async function loader() {
const templates = await prisma.template.findMany({
orderBy: { priority: "asc" },
where: { isLive: true },
});
return json(templates);
}
@@ -0,0 +1,66 @@
import type { ActionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { z } from "zod";
import { env } from "~/env.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { RegisterWorkflow } from "~/services/workflows/registerWorkflow.server";
// PUT /api/v2/internal/workflows/:workflowP
export async function action({ request, params }: ActionArgs) {
// first make sure this is a PUT request
if (request.method.toUpperCase() !== "PUT") {
return json({ error: "Method not allowed" }, { status: 405 });
}
// Next authenticate the request
const authenticatedEnv = await authenticateApiRequest(request);
if (!authenticatedEnv) {
return json({ error: "Invalid or Missing API key" }, { status: 401 });
}
// Now parse the request body
const body = await request.json();
// And the params
const { workflowP } = z.object({ workflowP: z.string() }).parse(params);
const registerWorkflow = new RegisterWorkflow();
const result = await registerWorkflow.call(
workflowP,
body,
authenticatedEnv.organization,
authenticatedEnv
);
switch (result.status) {
case "validationError": {
return json({ error: result.errors }, { status: 400 });
}
case "isArchived": {
return json({ error: "Workflow is archived" }, { status: 400 });
}
case "success": {
const { workflow, environment, organization, isNew } = result.data;
const data = {
workflow: {
id: workflow.id,
slug: workflow.slug,
},
environment: {
id: environment.id,
slug: environment.slug,
},
organization: {
id: organization.id,
slug: organization.slug,
},
url: `${env.APP_ORIGIN}/orgs/${organization.slug}/workflows/${workflow.slug}`,
};
return json(data, { status: isNew ? 201 : 200 });
}
}
}
@@ -78,7 +78,7 @@ export function EnvironmentMenu() {
<Popover.Button
className={`
${open ? "" : ""}
inline-flex justify-between gap-2 items-center rounded text-white bg-transparent pl-3.5 pr-2 py-2 text-sm hover:bg-slate-800 focus:outline-none`}
inline-flex items-center justify-between gap-2 rounded bg-transparent py-2 pl-3.5 pr-2 text-sm text-white hover:bg-slate-800 focus:outline-none`}
>
<EnvironmentIcon slug={currentEnvironment.slug} />
<span className="transition">
@@ -90,7 +90,7 @@ export function EnvironmentMenu() {
</span>
<ChevronUpDownIcon
className={`${open ? "" : ""}
ml-1 h-5 w-5 transition duration-150 ease-in-out text-slate-500`}
ml-1 h-5 w-5 text-slate-500 transition duration-150 ease-in-out`}
aria-hidden="true"
/>
</Popover.Button>
@@ -105,7 +105,7 @@ export function EnvironmentMenu() {
>
<Popover.Panel className="absolute left-0 z-30 mt-3 w-screen min-w-max max-w-xs translate-x-0 transform px-4 sm:px-0">
<div className="overflow-hidden rounded-lg shadow-lg ring-1 ring-black ring-opacity-5">
<div className="relative grid gap-y-1 py-1 bg-slate-700 grid-cols-1">
<div className="relative grid grid-cols-1 gap-y-1 bg-slate-700 py-1">
{environments.map((environment) => {
return (
<Popover.Button
@@ -115,7 +115,7 @@ export function EnvironmentMenu() {
name="environment"
value={environment.slug}
className={classNames(
"flex items-center justify-between gap-1.5 mx-1 px-3 py-2 text-white rounded hover:bg-slate-800 transition",
"mx-1 flex items-center justify-between gap-1.5 rounded px-3 py-2 text-white transition hover:bg-slate-800",
environment.slug === currentEnvironment?.slug &&
"!bg-slate-800"
)}
@@ -144,7 +144,13 @@ export function EnvironmentMenu() {
);
}
function EnvironmentIcon({ slug }: { slug: string }) {
export function EnvironmentIcon({
slug,
className,
}: {
slug: string;
className?: string;
}) {
let color = "bg-emerald-500";
if (slug === "live") {
color = "bg-orange-500";
@@ -152,8 +158,9 @@ function EnvironmentIcon({ slug }: { slug: string }) {
return (
<span
className={classNames(
"rounded-full block w-[0.35rem] h-[0.35rem]",
color
"block h-[0.35rem] w-[0.35rem] rounded-full",
color,
className
)}
/>
);
@@ -223,6 +223,30 @@ class BehaviouralAnalytics {
},
};
telemetry = {
capture: ({
userId,
event,
properties,
organizationId,
environmentId,
}: {
userId: string;
event: string;
properties: Record<string | number, any>;
organizationId?: string;
environmentId?: string;
}) => {
this.#capture({
userId,
event,
eventProperties: properties,
organizationId,
environmentId,
});
},
};
#capture(event: CaptureEvent) {
if (this.client === undefined) return;
let groups: Record<string, string> = {};
@@ -791,6 +791,7 @@ function createTaskQueue() {
"x-ttl": run.workflow.triggerTtlInSeconds,
"x-is-test": run.isTest ? "true" : "false",
"x-app-origin": env.APP_ORIGIN,
"x-attempt": String(run.attemptCount),
},
{
eventTimestamp: run.event.timestamp.getTime(),
@@ -7,7 +7,7 @@ import { prisma } from "~/db.server";
import type { Organization } from "~/models/organization.server";
import type { RuntimeEnvironment } from "~/models/runtimeEnvironment.server";
import type { Workflow } from "~/models/workflow.server";
import { appEventPublisher, taskQueue } from "../messageBroker.server";
import { taskQueue } from "../messageBroker.server";
export class RegisterWorkflow {
#prismaClient: PrismaClient;
@@ -31,7 +31,7 @@ export class RegisterWorkflow {
};
}
const workflow = await this.#upsertWorkflow(
const { workflow, isNew } = await this.#upsertWorkflow(
slug,
validation.data,
organization
@@ -39,7 +39,7 @@ export class RegisterWorkflow {
if (workflow.isArchived) {
return {
status: "success" as const,
status: "isArchived" as const,
data: { id: workflow.id },
};
}
@@ -73,7 +73,7 @@ export class RegisterWorkflow {
return {
status: "success" as const,
data: { id: workflow.id },
data: { workflow, environment, organization, isNew },
};
}
@@ -172,9 +172,11 @@ export class RegisterWorkflow {
await taskQueue.publish("WORKFLOW_CREATED", {
id: workflow.id,
});
return { workflow, isNew: true };
}
return workflow;
return { workflow, isNew: false };
}
async upsertSource(
+20 -6
View File
@@ -1,6 +1,6 @@
import type { RouteMatch } from "@remix-run/react";
import { useMatches } from "@remix-run/react";
import { useMemo } from "react";
import { DEV_ENVIRONMENT } from "./consts";
const DEFAULT_REDIRECT = "/";
@@ -33,18 +33,23 @@ export function safeRedirect(
* @returns {JSON|undefined} The router data or undefined if not found
*/
export function useMatchesData(
id: string,
id: string | string[],
debug: boolean = false
): RouteMatch | undefined {
const matchingRoutes = useMatches();
if (debug) {
console.log("matchingRoutes", matchingRoutes);
}
const route = useMemo(
() => matchingRoutes.find((route) => route.id === id),
[matchingRoutes, id]
);
const paths = Array.isArray(id) ? id : [id];
// Get the first matching route
const route = paths.reduce((acc, path) => {
if (acc) return acc;
return matchingRoutes.find((route) => route.id === path);
}, undefined as RouteMatch | undefined);
return route;
}
@@ -124,3 +129,12 @@ export function titleCase(original: string): string {
export function dateDifference(date1: Date, date2: Date) {
return Math.abs(date1.getTime() - date2.getTime());
}
export const environmentShortName = (slug: string) =>
slug === DEV_ENVIRONMENT ? "Dev" : "Live";
// Takes an api key (either trigger_live_xxxx or trigger_development_xxxx) and returns trigger_live_********
export const obfuscateApiKey = (apiKey: string) => {
const [prefix, slug, secretPart] = apiKey.split("_");
return `${prefix}_${slug}_${"*".repeat(secretPart.length)}`;
};
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Template" ADD COLUMN "isLive" BOOLEAN NOT NULL DEFAULT true;
+7 -5
View File
@@ -342,7 +342,7 @@ model IntegrationRequest {
params Json
endpoint String
version String @default("1")
version String @default("1")
externalService ExternalService @relation(fields: [externalServiceId], references: [id], onDelete: Cascade, onUpdate: Cascade)
externalServiceId String
@@ -485,10 +485,10 @@ model WorkflowRunStep {
idempotencyKey String
ts String
type WorkflowRunStepType
input Json?
output Json?
context Json
type WorkflowRunStepType
input Json?
output Json?
context Json
displayProperties Json?
createdAt DateTime @default(now())
@@ -625,6 +625,8 @@ model Template {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
isLive Boolean @default(true)
organizationTemplates OrganizationTemplate[]
}
+52 -20
View File
@@ -22,18 +22,32 @@ async function readTemplateDocsFile(slug: string) {
async function seed() {
console.log(`Database has been seeded. 🌱`);
const basicStarter = {
repositoryUrl: "https://github.com/triggerdotdev/basic-starter",
const blankStarter = {
repositoryUrl: "https://github.com/triggerdotdev/blank-starter",
imageUrl:
"https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/51a2a621-577a-4648-a087-bc5381259a00/public",
title: "A blank starter project with a simple Custom Event trigger",
shortTitle: "Basic Starter",
description: "This is a great place to start if you're new to Trigger.",
"https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/3812f076-38a2-4456-b809-2fc61dd77800/public",
title: "A blank starter ready to run your own workflow",
shortTitle: "Blank Starter",
description:
"This is a great place to start if you want to build your own workflow from scratch.",
priority: 0,
services: [],
workflowIds: ["basic-starter"],
markdownDocs: await readTemplateDocsFile("basic-starter"),
runLocalDocs: await readTemplateDocsFile("basic-starter-local"),
workflowIds: [],
markdownDocs: await readTemplateDocsFile("blank-starter"),
};
const helloWorld = {
repositoryUrl: "https://github.com/triggerdotdev/hello-world",
imageUrl:
"https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/634cfd11-e499-48ca-aaf5-b2404642f600/public",
title: "A Hello World with a simple custom event trigger",
shortTitle: "Hello World",
description:
"This is a great place to start if you're new to Trigger.dev and want to learn how to build a simple workflow.",
priority: 10,
services: [],
workflowIds: ["hello-world"],
markdownDocs: await readTemplateDocsFile("hello-world"),
};
const scheduledHealthcheck = {
@@ -44,7 +58,7 @@ async function seed() {
shortTitle: "Scheduled Healthcheck",
description:
"This will run every 5 minutes and send a Slack message if a website url returns a non-200 response.",
priority: 1,
priority: 20,
services: ["slack"],
workflowIds: ["scheduled-healthcheck"],
markdownDocs: await readTemplateDocsFile("scheduled-healthcheck"),
@@ -55,11 +69,11 @@ async function seed() {
repositoryUrl: "https://github.com/triggerdotdev/github-stars-to-slack",
imageUrl:
"https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/5b3964be-9a7b-4a7e-1837-b283e501b900/public",
title: "Slack notifications when a GitHub repo is starred",
title: "Post to Slack every time a GitHub repo is starred",
shortTitle: "GitHub stars to Slack",
description:
"When a GitHub repo is starred, post information about the user to Slack.",
priority: 1,
priority: 30,
services: ["github", "slack"],
workflowIds: ["github-stars-to-slack"],
markdownDocs: await readTemplateDocsFile("github-stars-to-slack"),
@@ -74,7 +88,7 @@ async function seed() {
shortTitle: "GitHub issues to Slack",
description:
"When a GitHub issue is created or modified, post a message and link to the issue in a specific Slack channel.",
priority: 1,
priority: 40,
services: ["github", "slack"],
workflowIds: ["github-issues-to-slack"],
markdownDocs: await readTemplateDocsFile("github-issues-to-slack"),
@@ -86,11 +100,11 @@ async function seed() {
"https://github.com/triggerdotdev/resend-welcome-drip-campaign",
imageUrl:
"https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/cce3b770-b6f9-40ef-baf3-f01c20686700/public",
title: "Send a welcome drip campaign to new users",
title: "Send an email drip campaign when a new user signs up",
shortTitle: "Resend.com drip campaign",
description:
"When a new user is created, send them a welcome drip campaign from Resend.com and react.email.",
priority: 2,
priority: 50,
services: ["resend"],
workflowIds: ["resend-welcome-drip-campaign"],
markdownDocs: await readTemplateDocsFile("resend-welcome-drip-campaign"),
@@ -99,12 +113,30 @@ async function seed() {
),
};
await prisma.template.upsert({
where: { slug: "basic-starter" },
update: basicStarter,
create: {
await prisma.template.updateMany({
where: {
slug: "basic-starter",
...basicStarter,
},
data: {
isLive: false,
},
});
await prisma.template.upsert({
where: { slug: "blank-starter" },
update: blankStarter,
create: {
slug: "blank-starter",
...blankStarter,
},
});
await prisma.template.upsert({
where: { slug: "hello-world" },
update: helloWorld,
create: {
slug: "hello-world",
...helloWorld,
},
});
@@ -1,31 +0,0 @@
## 💻 Run locally
First, in your terminal of choice, clone the repo and install dependencies:
```sh
git clone https://github.com/triggerdotdev/basic-starter.git
cd basic-starter
npm install
```
Then execute the following command to create a `.env` file with your development Trigger.dev API Key:
```sh
echo "TRIGGER_API_KEY=<APIKEY>" >> .env
```
And finally you are ready to run the process:
```sh
npm run dev
```
You should see a message output in your terminal like the following:
```
[trigger.dev] ✨ Connected and listening for events [basic-starter]
```
## 🧪 Test it
The [Basic Starter README](https://github.com/triggerdotdev/basic-starter) has more details on how to test this template.
@@ -1,32 +1,52 @@
This repo is a very simple starting point for creating your Trigger.dev workflows.
Currently this repo only has a single [customEvent](https://docs.trigger.dev/triggers/custom-events) trigger:
```ts
import { Trigger, customEvent } from "@trigger.dev/sdk";
import { z } from "zod";
new Trigger({
// Give your Trigger a stable ID
id: "basic-starter",
name: "Basic Starter",
// Trigger on a custom event, see https://docs.trigger.dev/triggers/custom-events
id: "hello-world",
name: "Template: Hello World",
// Trigger on the custom event named "your.event", see https://docs.trigger.dev/triggers/custom-events
on: customEvent({
name: "basic.starter",
// Use zod to verify event payload. See https://docs.trigger.dev/guides/zod
schema: z.object({ id: z.string() }),
name: "your.event",
}),
// The run functions gets called once per "basic.starter" event
// The run functions gets called once per "your.event" event
async run(event, ctx) {
// Call external services, add delays, and more here.
await ctx.logger.info("Hello world from inside trigger.dev");
await ctx.waitFor("waiting...", { seconds: 10 });
// Returned data will become the run "output" and is optional
return event;
await ctx.logger.info("Hello world from inside trigger.dev");
},
}).listen();
```
## 📺 Go Live
After you are happy with your campaign and deploy it live to Render.com (or some other hosting service), you can send custom events that Trigger your workflow using the [sendEvent](https://docs.trigger.dev/reference/send-event) function from the `@trigger.dev/sdk`, or simply by making requests to our [`events`](https://docs.trigger.dev/api-reference/events/sendEvent) API endpoint.
Here is an example of sending the custom event to trigger the workflow contained in this repo using `fetch`:
```ts
const event = {
name: "your.event",
payload: {
hello: "world",
},
};
const response = await fetch("https://app.trigger.dev/api/v1/events", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.TRIGGER_API_KEY}`,
},
body: JSON.stringify({
id: randomUUID(),
event,
}),
});
```
## ✍️ Customize
You can easily adapt this workflow to a different event relevant to your app. For example, we have a workflow that runs when a user is created and it looks like this:
@@ -57,32 +77,3 @@ new Trigger({
```
Be sure to check out more over on our [docs](https://docs.trigger.dev)
## 📺 Go Live
After you are happy with your campaign and deploy it live to Render.com (or some other hosting service), you can send custom events that Trigger your workflow using the [sendEvent](https://docs.trigger.dev/reference/send-event) function from the `@trigger.dev/sdk`, or simply by making requests to our [`events`](https://docs.trigger.dev/api-reference/events/sendEvent) API endpoint.
Here is an example of sending the custom event to trigger the workflow contained in this repo using `fetch`:
```ts
const eventId = ulid(); // Generate a unique event ID
const event = {
name: "basic.starter",
payload: {
// This should match the zod schema provided in the `customEvent.schema` option
id: "user_1234",
},
};
const response = await fetch("https://app.trigger.dev/api/v1/events", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.TRIGGER_API_KEY}`,
},
body: JSON.stringify({
id: eventId,
event,
}),
});
```
@@ -1,6 +1,4 @@
## ✨ Trigger.dev GitHub Issues to Slack
This repo contains a [GitHub IssueEvent](https://docs.trigger.dev/integrations/apis/github/events/issues) Trigger that will run whenever an issue action is performed in a GitHub repository:
This template contains a [GitHub IssueEvent](https://docs.trigger.dev/integrations/apis/github/events/issues) Trigger that will run whenever an issue action is performed in a GitHub repository:
```ts
import { Trigger } from "@trigger.dev/sdk";
@@ -1,4 +1,4 @@
This repo contains a [GitHub newStarEvent](https://docs.trigger.dev/integrations/apis/github/events/new-star) Trigger that will run whenever the specified repository gets a new ⭐️:
This template contains a [GitHub newStarEvent](https://docs.trigger.dev/integrations/apis/github/events/new-star) Trigger that will run whenever the specified repository gets a new ⭐️:
```ts
import { Trigger } from "@trigger.dev/sdk";
+79
View File
@@ -0,0 +1,79 @@
Currently this template only has a single [customEvent](https://docs.trigger.dev/triggers/custom-events) trigger:
```ts
import { Trigger, customEvent } from "@trigger.dev/sdk";
new Trigger({
// Give your Trigger a stable ID
id: "hello-world",
name: "Template: Hello World",
// Trigger on the custom event named "your.event", see https://docs.trigger.dev/triggers/custom-events
on: customEvent({
name: "your.event",
}),
// The run functions gets called once per "your.event" event
async run(event, ctx) {
await ctx.waitFor("waiting...", { seconds: 10 });
await ctx.logger.info("Hello world from inside trigger.dev");
},
}).listen();
```
## 📺 Go Live
After you are happy with your campaign and deploy it live to Render.com (or some other hosting service), you can send custom events that Trigger your workflow using the [sendEvent](https://docs.trigger.dev/reference/send-event) function from the `@trigger.dev/sdk`, or simply by making requests to our [`events`](https://docs.trigger.dev/api-reference/events/sendEvent) API endpoint.
Here is an example of sending the custom event to trigger the workflow contained in this repo using `fetch`:
```ts
const event = {
name: "your.event",
payload: {
hello: "world",
},
};
const response = await fetch("https://app.trigger.dev/api/v1/events", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.TRIGGER_API_KEY}`,
},
body: JSON.stringify({
id: randomUUID(),
event,
}),
});
```
## ✍️ Customize
You can easily adapt this workflow to a different event relevant to your app. For example, we have a workflow that runs when a user is created and it looks like this:
```ts
import { Trigger, customEvent } from "@trigger.dev/sdk";
import * as slack from "@trigger.dev/slack";
import { z } from "zod";
new Trigger({
id: "new-user",
name: "New user",
on: customEvent({
name: "user.created",
schema: z.object({ id: z.string() }),
}),
async run(event, ctx) {
const user = await prisma.user.find({
where: { id: event.id },
});
await slack.postMessage("🚨", {
channelName: "new-users",
text: `New user signed up: ${user.email}`,
});
},
}).listen();
```
Be sure to check out more over on our [docs](https://docs.trigger.dev)
@@ -1,4 +1,4 @@
This repo contains a [customEvent](https://docs.trigger.dev/triggers/custom-events) Trigger that will send an example drip email campaign using [Resend.com](https://resend.com/) and [react.email](https://react.email/)
This template contains a [customEvent](https://docs.trigger.dev/triggers/custom-events) Trigger that will send an example drip email campaign using [Resend.com](https://resend.com/) and [react.email](https://react.email/)
> Resend.com is currently in private beta, but if you signup for their waitlist, give us a shout on [our Discord](https://discord.gg/CzBqJnYq9r) and we'll help you get in.
@@ -1,6 +1,4 @@
## ✨ Trigger.dev Scheduled Healthcheck
This repo contains a [Scheduled](https://docs.trigger.dev/triggers/scheduled) Trigger that will run every 5 minutes and send a Slack message if a website url returns a non-200 response:
This template contains a [Scheduled](https://docs.trigger.dev/triggers/scheduled) Trigger that will run every 5 minutes and send a Slack message if a website url returns a non-200 response:
```ts
new Trigger({
@@ -40,15 +38,3 @@ new Trigger({
- Update the frequency (you can go as frequent as once per minute)
Be sure to check out more over on our [docs](https://docs.trigger.dev)
## 🚀 Deploy
We've made it really easy to deploy this repo to Render.com, if you don't already have a Node.js server to host your triggers.
[Render.com](https://render.com) is a super-fast way to deploy webapps and servers (think of it like a modern Heroku)
<a href="https://render.com/deploy?repo=https://github.com/triggerdotdev/scheduled-healthcheck">
<img width="144px" src="https://render.com/images/deploy-to-render-button.svg" alt="Deploy to Render">
</a>
> **Note** Make sure you use your "live" trigger.dev API Key when deploying to a server
+2
View File
@@ -26,6 +26,7 @@ export type WorkflowRunControllerOptions = {
organizationId: string;
isTest: boolean;
appOrigin: string;
attempt: number;
};
};
@@ -41,6 +42,7 @@ export class WorkflowRunController {
organizationId: string;
isTest: boolean;
appOrigin: string;
attempt: number;
};
#logger: Logger;
+26 -6
View File
@@ -283,9 +283,9 @@ export class TriggerServer {
},
INITIALIZE_HOST: async (data) => {
// Initialize workflow
const success = await this.#initializeWorkflow(data);
const response = await this.#initializeWorkflow(data);
if (success) {
if (response) {
return { type: "success" as const };
} else {
return {
@@ -294,6 +294,19 @@ export class TriggerServer {
};
}
},
INITIALIZE_HOST_V2: async (data) => {
// Initialize workflow
const response = await this.#initializeWorkflow(data);
if (response) {
return { type: "success" as const, data: response };
} else {
return {
type: "error" as const,
message: "Failed to connect to the Pulsar cluster",
};
}
},
},
});
@@ -337,12 +350,15 @@ export class TriggerServer {
}
);
this.#socket.close(4001, "Client not authenticated");
this.#socket.close(
4001,
"Could not authenticate to the server because the API key is invalid"
);
}
}
async #initializeWorkflow(
data: z.infer<(typeof ServerRPCSchema)["INITIALIZE_HOST"]["request"]>
data: z.infer<(typeof ServerRPCSchema)["INITIALIZE_HOST_V2"]["request"]>
) {
if (this.#isInitialized) {
throw new Error(
@@ -373,7 +389,7 @@ export class TriggerServer {
triggerTTL: data.triggerTTL,
});
this.#workflowId = response.id;
this.#workflowId = response.workflow.id;
this.#logger.debug("Initializing trigger subscriber...");
@@ -455,6 +471,10 @@ export class TriggerServer {
typeof properties["x-is-test"] === "string"
? properties["x-is-test"] === "true"
: false,
attempt:
typeof properties["x-attempt"] === "string"
? Number(properties["x-attempt"])
: 0,
},
});
@@ -487,7 +507,7 @@ export class TriggerServer {
this.#isInitialized = true;
return true;
return response;
} catch (error) {
if (error instanceof ZodError) {
this.#logger.error(
+152 -152
View File
@@ -15,7 +15,7 @@ const userCreatedEvent = z.object({
const trigger = new Trigger({
id: "my-workflow",
name: "My workflow",
apiKey: "trigger_dev_zC25mKNn6c0q",
// apiKey: "<enter your API key here>",
endpoint: "ws://localhost:8889/ws",
logLevel: "debug",
triggerTTL: 60 * 60 * 24,
@@ -44,159 +44,159 @@ const trigger = new Trigger({
trigger.listen();
new Trigger({
id: "smoke-test",
name: "Smoke Test",
apiKey: "trigger_dev_zC25mKNn6c0q",
endpoint: "ws://localhost:8889/ws",
logLevel: "debug",
on: customEvent({
name: "smoke.test",
schema: z.object({ baz: z.string() }),
}),
run: async (event, ctx) => {
await ctx.logger.info("Inside the smoke test workflow, received event", {
event,
myDate: new Date(),
});
},
}).listen();
// new Trigger({
// id: "smoke-test",
// name: "Smoke Test",
// apiKey: "trigger_dev_zC25mKNn6c0q",
// endpoint: "ws://localhost:8889/ws",
// logLevel: "debug",
// on: customEvent({
// name: "smoke.test",
// schema: z.object({ baz: z.string() }),
// }),
// run: async (event, ctx) => {
// await ctx.logger.info("Inside the smoke test workflow, received event", {
// event,
// myDate: new Date(),
// });
// },
// }).listen();
new Trigger({
id: "log-tests",
name: "My logs",
apiKey: "trigger_dev_zC25mKNn6c0q",
endpoint: "ws://localhost:8889/ws",
logLevel: "debug",
on: customEvent({ name: "user.created", schema: z.any() }),
run: async (event, ctx) => {
await ctx.logger.info("It's been 5 minutes since the last run!");
await ctx.logger.debug("This is a debug log");
await ctx.logger.warn("This is a warning");
await ctx.logger.error("This is an error");
},
}).listen();
// new Trigger({
// id: "log-tests",
// name: "My logs",
// apiKey: "trigger_dev_zC25mKNn6c0q",
// endpoint: "ws://localhost:8889/ws",
// logLevel: "debug",
// on: customEvent({ name: "user.created", schema: z.any() }),
// run: async (event, ctx) => {
// await ctx.logger.info("It's been 5 minutes since the last run!");
// await ctx.logger.debug("This is a debug log");
// await ctx.logger.warn("This is a warning");
// await ctx.logger.error("This is an error");
// },
// }).listen();
export const bookingPayloadSchema = z.object({
triggerEvent: z.string(),
createdAt: z.coerce.date(),
payload: z.object({
type: z.string(),
title: z.string(),
description: z.string(),
additionalNotes: z.string(),
customInputs: z.object({}),
startTime: z.coerce.date(),
endTime: z.coerce.date(),
organizer: z.object({
id: z.number(),
name: z.string(),
email: z.string(),
timeZone: z.string(),
language: z.object({ locale: z.string() }),
}),
attendees: z.array(
z.object({
email: z.string(),
name: z.string(),
timeZone: z.string(),
language: z.object({ locale: z.string() }),
})
),
location: z.string(),
destinationCalendar: z.object({
id: z.number(),
integration: z.string(),
externalId: z.string(),
userId: z.number(),
eventTypeId: z.null(),
credentialId: z.number(),
}),
hideCalendarNotes: z.boolean(),
requiresConfirmation: z.null(),
eventTypeId: z.number(),
seatsShowAttendees: z.boolean(),
uid: z.string(),
conferenceData: z.object({
createRequest: z.object({ requestId: z.string() }),
}),
videoCallData: z.object({
type: z.string(),
id: z.string(),
password: z.string(),
url: z.string(),
}),
appsStatus: z.array(
z.object({
appName: z.string(),
type: z.string(),
success: z.number(),
failures: z.number(),
errors: z.array(z.any()).optional(),
warnings: z.array(z.any()).optional(),
})
),
eventTitle: z.string(),
eventDescription: z.null(),
price: z.number(),
currency: z.string(),
length: z.number(),
bookingId: z.number(),
metadata: z.object({ videoCallUrl: z.string() }),
status: z.string(),
}),
});
// export const bookingPayloadSchema = z.object({
// triggerEvent: z.string(),
// createdAt: z.coerce.date(),
// payload: z.object({
// type: z.string(),
// title: z.string(),
// description: z.string(),
// additionalNotes: z.string(),
// customInputs: z.object({}),
// startTime: z.coerce.date(),
// endTime: z.coerce.date(),
// organizer: z.object({
// id: z.number(),
// name: z.string(),
// email: z.string(),
// timeZone: z.string(),
// language: z.object({ locale: z.string() }),
// }),
// attendees: z.array(
// z.object({
// email: z.string(),
// name: z.string(),
// timeZone: z.string(),
// language: z.object({ locale: z.string() }),
// })
// ),
// location: z.string(),
// destinationCalendar: z.object({
// id: z.number(),
// integration: z.string(),
// externalId: z.string(),
// userId: z.number(),
// eventTypeId: z.null(),
// credentialId: z.number(),
// }),
// hideCalendarNotes: z.boolean(),
// requiresConfirmation: z.null(),
// eventTypeId: z.number(),
// seatsShowAttendees: z.boolean(),
// uid: z.string(),
// conferenceData: z.object({
// createRequest: z.object({ requestId: z.string() }),
// }),
// videoCallData: z.object({
// type: z.string(),
// id: z.string(),
// password: z.string(),
// url: z.string(),
// }),
// appsStatus: z.array(
// z.object({
// appName: z.string(),
// type: z.string(),
// success: z.number(),
// failures: z.number(),
// errors: z.array(z.any()).optional(),
// warnings: z.array(z.any()).optional(),
// })
// ),
// eventTitle: z.string(),
// eventDescription: z.null(),
// price: z.number(),
// currency: z.string(),
// length: z.number(),
// bookingId: z.number(),
// metadata: z.object({ videoCallUrl: z.string() }),
// status: z.string(),
// }),
// });
new Trigger({
id: "calcom-booking-custom-event",
name: "Cal.com booking custom event",
apiKey: "trigger_dev_zC25mKNn6c0q",
endpoint: "ws://localhost:8889/ws",
logLevel: "debug",
triggerTTL: 60 * 60 * 24,
on: customEvent({ name: "calcom.booking", schema: bookingPayloadSchema }),
run: async (event, ctx) => {
return event;
},
}).listen();
// new Trigger({
// id: "calcom-booking-custom-event",
// name: "Cal.com booking custom event",
// apiKey: "trigger_dev_zC25mKNn6c0q",
// endpoint: "ws://localhost:8889/ws",
// logLevel: "debug",
// triggerTTL: 60 * 60 * 24,
// on: customEvent({ name: "calcom.booking", schema: bookingPayloadSchema }),
// run: async (event, ctx) => {
// return event;
// },
// }).listen();
new Trigger({
id: "testing-schedule-test-events",
name: "Testing scheduled test payloads",
apiKey: "trigger_dev_zC25mKNn6c0q",
endpoint: "ws://localhost:8889/ws",
logLevel: "debug",
triggerTTL: 60 * 60 * 24,
on: scheduleEvent({
rateOf: { hours: 1 },
}),
run: async (event, ctx) => {
return event;
},
}).listen();
// new Trigger({
// id: "testing-schedule-test-events",
// name: "Testing scheduled test payloads",
// apiKey: "trigger_dev_zC25mKNn6c0q",
// endpoint: "ws://localhost:8889/ws",
// logLevel: "debug",
// triggerTTL: 60 * 60 * 24,
// on: scheduleEvent({
// rateOf: { hours: 1 },
// }),
// run: async (event, ctx) => {
// return event;
// },
// }).listen();
new Trigger({
id: "smoke-test-webhook-schema-test",
name: "Smoke Test Webhook Schema Test",
apiKey: "trigger_dev_zC25mKNn6c0q",
endpoint: "ws://localhost:8889/ws",
logLevel: "debug",
on: webhookEvent({
service: "cal.com",
eventName: "BOOKING_CREATED",
filter: {
triggerEvent: ["BOOKING_CREATED"],
},
schema: bookingPayloadSchema,
verifyPayload: {
enabled: true,
header: "X-Cal-Signature-256",
},
}),
run: async (event, ctx) => {
await ctx.logger.info("Received a cal.com booking", {
event,
wallTime: new Date(),
});
},
}).listen();
// new Trigger({
// id: "smoke-test-webhook-schema-test",
// name: "Smoke Test Webhook Schema Test",
// apiKey: "trigger_dev_zC25mKNn6c0q",
// endpoint: "ws://localhost:8889/ws",
// logLevel: "debug",
// on: webhookEvent({
// service: "cal.com",
// eventName: "BOOKING_CREATED",
// filter: {
// triggerEvent: ["BOOKING_CREATED"],
// },
// schema: bookingPayloadSchema,
// verifyPayload: {
// enabled: true,
// header: "X-Cal-Signature-256",
// },
// }),
// run: async (event, ctx) => {
// await ctx.logger.info("Received a cal.com booking", {
// event,
// wallTime: new Date(),
// });
// },
// }).listen();
@@ -1,5 +1,52 @@
# @trigger.dev/airtable
## 0.1.24
### Patch Changes
- Updated dependencies [3a2cf0dd]
- @trigger.dev/sdk@0.2.17
## 0.1.23
### Patch Changes
- Updated dependencies [ee20f921]
- Updated dependencies [4f47d031]
- Updated dependencies [87a3bbee]
- Updated dependencies [51f9bc9d]
- Updated dependencies [0932ae7d]
- @trigger.dev/sdk@0.2.16
## 0.1.23-next.3
### Patch Changes
- Updated dependencies
- @trigger.dev/sdk@0.2.16-next.3
## 0.1.23-next.2
### Patch Changes
- Updated dependencies [87a3bbee]
- @trigger.dev/sdk@0.2.16-next.2
## 0.1.23-next.1
### Patch Changes
- Updated dependencies [0932ae7d]
- @trigger.dev/sdk@0.2.16-next.1
## 0.1.23-next.0
### Patch Changes
- Updated dependencies [ee20f921]
- Updated dependencies [51f9bc9d]
- @trigger.dev/sdk@0.2.16-next.0
## 0.1.22
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/airtable",
"version": "0.1.22",
"version": "0.1.24",
"description": "The official Airtable integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -1,5 +1,52 @@
# @trigger.dev/sendgrid
## 0.1.24
### Patch Changes
- Updated dependencies [3a2cf0dd]
- @trigger.dev/sdk@0.2.17
## 0.1.23
### Patch Changes
- Updated dependencies [ee20f921]
- Updated dependencies [4f47d031]
- Updated dependencies [87a3bbee]
- Updated dependencies [51f9bc9d]
- Updated dependencies [0932ae7d]
- @trigger.dev/sdk@0.2.16
## 0.1.23-next.3
### Patch Changes
- Updated dependencies
- @trigger.dev/sdk@0.2.16-next.3
## 0.1.23-next.2
### Patch Changes
- Updated dependencies [87a3bbee]
- @trigger.dev/sdk@0.2.16-next.2
## 0.1.23-next.1
### Patch Changes
- Updated dependencies [0932ae7d]
- @trigger.dev/sdk@0.2.16-next.1
## 0.1.23-next.0
### Patch Changes
- Updated dependencies [ee20f921]
- Updated dependencies [51f9bc9d]
- @trigger.dev/sdk@0.2.16-next.0
## 0.1.22
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/sendgrid",
"version": "0.1.22",
"version": "0.1.24",
"description": "The official SendGrid integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
+47
View File
@@ -1,5 +1,52 @@
# @trigger.dev/github
## 0.1.24
### Patch Changes
- Updated dependencies [3a2cf0dd]
- @trigger.dev/sdk@0.2.17
## 0.1.23
### Patch Changes
- Updated dependencies [ee20f921]
- Updated dependencies [4f47d031]
- Updated dependencies [87a3bbee]
- Updated dependencies [51f9bc9d]
- Updated dependencies [0932ae7d]
- @trigger.dev/sdk@0.2.16
## 0.1.23-next.3
### Patch Changes
- Updated dependencies
- @trigger.dev/sdk@0.2.16-next.3
## 0.1.23-next.2
### Patch Changes
- Updated dependencies [87a3bbee]
- @trigger.dev/sdk@0.2.16-next.2
## 0.1.23-next.1
### Patch Changes
- Updated dependencies [0932ae7d]
- @trigger.dev/sdk@0.2.16-next.1
## 0.1.23-next.0
### Patch Changes
- Updated dependencies [ee20f921]
- Updated dependencies [51f9bc9d]
- @trigger.dev/sdk@0.2.16-next.0
## 0.1.22
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/github",
"version": "0.1.22",
"version": "0.1.24",
"description": "The official GitHub integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
+47
View File
@@ -1,5 +1,52 @@
# @trigger.dev/resend
## 0.1.24
### Patch Changes
- Updated dependencies [3a2cf0dd]
- @trigger.dev/sdk@0.2.17
## 0.1.23
### Patch Changes
- Updated dependencies [ee20f921]
- Updated dependencies [4f47d031]
- Updated dependencies [87a3bbee]
- Updated dependencies [51f9bc9d]
- Updated dependencies [0932ae7d]
- @trigger.dev/sdk@0.2.16
## 0.1.23-next.3
### Patch Changes
- Updated dependencies
- @trigger.dev/sdk@0.2.16-next.3
## 0.1.23-next.2
### Patch Changes
- Updated dependencies [87a3bbee]
- @trigger.dev/sdk@0.2.16-next.2
## 0.1.23-next.1
### Patch Changes
- Updated dependencies [0932ae7d]
- @trigger.dev/sdk@0.2.16-next.1
## 0.1.23-next.0
### Patch Changes
- Updated dependencies [ee20f921]
- Updated dependencies [51f9bc9d]
- @trigger.dev/sdk@0.2.16-next.0
## 0.1.22
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/resend",
"version": "0.1.22",
"version": "0.1.24",
"description": "The official resend.com integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
+47
View File
@@ -1,5 +1,52 @@
# @trigger.dev/shopify
## 0.1.24
### Patch Changes
- Updated dependencies [3a2cf0dd]
- @trigger.dev/sdk@0.2.17
## 0.1.23
### Patch Changes
- Updated dependencies [ee20f921]
- Updated dependencies [4f47d031]
- Updated dependencies [87a3bbee]
- Updated dependencies [51f9bc9d]
- Updated dependencies [0932ae7d]
- @trigger.dev/sdk@0.2.16
## 0.1.23-next.3
### Patch Changes
- Updated dependencies
- @trigger.dev/sdk@0.2.16-next.3
## 0.1.23-next.2
### Patch Changes
- Updated dependencies [87a3bbee]
- @trigger.dev/sdk@0.2.16-next.2
## 0.1.23-next.1
### Patch Changes
- Updated dependencies [0932ae7d]
- @trigger.dev/sdk@0.2.16-next.1
## 0.1.23-next.0
### Patch Changes
- Updated dependencies [ee20f921]
- Updated dependencies [51f9bc9d]
- @trigger.dev/sdk@0.2.16-next.0
## 0.1.22
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/shopify",
"version": "0.1.22",
"version": "0.1.24",
"description": "The official Shopify integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
+47
View File
@@ -1,5 +1,52 @@
# @trigger.dev/slack
## 0.1.24
### Patch Changes
- Updated dependencies [3a2cf0dd]
- @trigger.dev/sdk@0.2.17
## 0.1.23
### Patch Changes
- Updated dependencies [ee20f921]
- Updated dependencies [4f47d031]
- Updated dependencies [87a3bbee]
- Updated dependencies [51f9bc9d]
- Updated dependencies [0932ae7d]
- @trigger.dev/sdk@0.2.16
## 0.1.23-next.3
### Patch Changes
- Updated dependencies
- @trigger.dev/sdk@0.2.16-next.3
## 0.1.23-next.2
### Patch Changes
- Updated dependencies [87a3bbee]
- @trigger.dev/sdk@0.2.16-next.2
## 0.1.23-next.1
### Patch Changes
- Updated dependencies [0932ae7d]
- @trigger.dev/sdk@0.2.16-next.1
## 0.1.23-next.0
### Patch Changes
- Updated dependencies [ee20f921]
- Updated dependencies [51f9bc9d]
- @trigger.dev/sdk@0.2.16-next.0
## 0.1.22
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/slack",
"version": "0.1.22",
"version": "0.1.24",
"description": "The official Slack integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
+47
View File
@@ -1,5 +1,52 @@
# @trigger.dev/whatsapp
## 0.1.22
### Patch Changes
- Updated dependencies [3a2cf0dd]
- @trigger.dev/sdk@0.2.17
## 0.1.21
### Patch Changes
- Updated dependencies [ee20f921]
- Updated dependencies [4f47d031]
- Updated dependencies [87a3bbee]
- Updated dependencies [51f9bc9d]
- Updated dependencies [0932ae7d]
- @trigger.dev/sdk@0.2.16
## 0.1.21-next.3
### Patch Changes
- Updated dependencies
- @trigger.dev/sdk@0.2.16-next.3
## 0.1.21-next.2
### Patch Changes
- Updated dependencies [87a3bbee]
- @trigger.dev/sdk@0.2.16-next.2
## 0.1.21-next.1
### Patch Changes
- Updated dependencies [0932ae7d]
- @trigger.dev/sdk@0.2.16-next.1
## 0.1.21-next.0
### Patch Changes
- Updated dependencies [ee20f921]
- Updated dependencies [51f9bc9d]
- @trigger.dev/sdk@0.2.16-next.0
## 0.1.20
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/whatsapp",
"version": "0.1.20",
"version": "0.1.22",
"description": "The official WhatsApp Business integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
+13
View File
@@ -0,0 +1,13 @@
# create-trigger
## 0.2.0
### Minor Changes
- 26e69cb6: Easily scaffold out standalone trigger.dev projects using create-trigger and our templates
## 0.2.0-next.0
### Minor Changes
- 26e69cb6: Easily scaffold out standalone trigger.dev projects using create-trigger and our templates
+77
View File
@@ -0,0 +1,77 @@
## ✨ Create Trigger - Get started writing Trigger.dev code quickly
Trigger.dev is an open source platform that makes it easy to create event-driven background tasks directly your codebase.
You can run these tasks (or "workflows" as we like to cal them) in your existing Node.js repo, but if you don't have one of those (👋 Next.js devs) or you just want to try us out without the setup, this `create-trigger` CLI will scaffold out a project for you in just a few seconds, either starting from scratch or using one of our many [templates](https://app.trigger.com/templates).
## 💻 Usage
To scaffold out a new project using `create-trigger`, run any of the following three commands and answer the prompts:
### npm
```sh
npm create trigger@latest
```
### yarn
```sh
yarn create trigger
```
### pnpm
```sh
pnpm create trigger@latest
```
You can also specify the [template](https://app.trigger.com/templates) you want to use by passing an argument to the command, like so:
```sh
npm create trigger@latest github-stars-to-slack
```
## Advanced Usage
| Option/Flag | Description |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `[template]` | The name of the template to use, e.g. basic-starter |
| `-p, --projectName` | The name of the project, as well as the name of the directory to create. Can be a path to a directory, e.g. ~/projects/my-project |
| `-k, --apiKey` | The development API key to use for the project. Visit https://app.trigger.dev to get yours |
| `--noGit` | Explicitly tell the CLI to not initialize a new git repo in the project |
| `--noInstall` | Explicitly tell the CLI to not run the package manager's install command |
## Folder structure
```
├── LICENSE
├── README.md
├── package.json
├── render.yaml
├── .env
├── .env.example
├── src
│   └── index.ts
└── tsconfig.json
```
### `src/index.ts`
All your Trigger.dev workflow code will be in here, and this is the part you can start customizing.
### `.env`
If provided, we'll save your development API Key here so running the project can connect to our servers.
### `render.yaml`
A [Render.com](https://render.com) Blueprint file that makes it easy to deploy your repo as a Background Worker.
### `README.md`
Contains useful instructions for getting started with the repo, including how to customize it, running it locally, testing it, and deploying it.
## Next steps
After you successfully scaffold out your project, take a look at the README. If you have any issues, please feel free to email us at hello@trigger.dev, or you can ask a question in our [Discord server](https://discord.gg/nkqV9xBYWy)
+68
View File
@@ -0,0 +1,68 @@
{
"name": "create-trigger",
"version": "0.2.0",
"description": "The Trigger.dev CLI to easily create and manage a Trigger.dev project",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/triggerdotdev/trigger.dev.git",
"directory": "packages/create-trigger"
},
"publishConfig": {
"access": "public"
},
"keywords": [
"typescript",
"trigger.dev",
"workflows",
"orchestration",
"events",
"webhooks",
"integrations",
"apis"
],
"files": [
"dist"
],
"type": "module",
"exports": "./dist/index.js",
"bin": {
"create-trigger": "./dist/index.js"
},
"devDependencies": {
"@types/fs-extra": "^11.0.1",
"@types/gradient-string": "^1.1.2",
"@types/inquirer": "^9.0.3",
"@types/node": "16",
"@types/node-fetch": "^2.6.2",
"rimraf": "^3.0.2",
"tsup": "^6.5.0",
"type-fest": "^3.6.0",
"typescript": "^4.9.5"
},
"scripts": {
"typecheck": "tsc",
"build": "tsup",
"dev": "tsup --watch",
"clean": "rimraf dist",
"start": "node dist/index.js"
},
"dependencies": {
"@types/degit": "^2.8.3",
"chalk": "^5.2.0",
"commander": "^9.4.1",
"degit": "^2.8.4",
"execa": "^7.0.0",
"fs-extra": "^11.1.0",
"gradient-string": "^2.0.2",
"inquirer": "^9.1.4",
"node-fetch": "^3.3.0",
"ora": "^6.1.2",
"terminal-link": "^3.0.0"
},
"engines": {
"node": ">=16"
}
}
+311
View File
@@ -0,0 +1,311 @@
import chalk from "chalk";
import { Command } from "commander";
import inquirer from "inquirer";
import terminalLink from "terminal-link";
import {
CREATE_TRIGGER,
DEFAULT_APP_NAME as DEFAULT_PROJECT_NAME,
} from "../consts.js";
import { getUserPkgManager } from "../utils/getUserPkgManager.js";
import { getVersion } from "../utils/getVersion.js";
import { logger } from "../utils/logger.js";
import { getTemplates } from "../utils/triggerApi.js";
export interface CliFlags {
noGit: boolean;
noInstall: boolean;
noTelemetry: boolean;
projectName: string;
apiKey?: string;
}
export interface CliResults {
templateName: string;
flags: CliFlags;
}
const defaultOptions: CliResults = {
templateName: "blank-starter",
flags: {
noGit: false,
noInstall: false,
noTelemetry: false,
projectName: DEFAULT_PROJECT_NAME,
},
};
export const runCli = async () => {
const cliResults = defaultOptions;
const program = new Command().name(CREATE_TRIGGER);
program
.description("A CLI for creating Trigger.dev projects")
.argument(
"[template-name]",
"The name of the template to use, e.g. basic-starter",
"blank-starter"
)
.option(
"-p, --projectName <project-name>",
"The name of the project, as well as the name of the directory to create. Can be a path to a directory, e.g. ~/projects/my-project",
false
)
.option(
"-k, --apiKey <api-key>",
"The development API key to use for the project. Visit https://app.trigger.dev to get yours",
false
)
.option(
"--noGit",
"Explicitly tell the CLI to not initialize a new git repo in the project",
false
)
.option(
"--noInstall",
"Explicitly tell the CLI to not run the package manager's install command",
false
)
.option(
"--noTelemetry",
"Explicitly tell the CLI to not send usage data to Trigger.dev",
false
)
.version(getVersion(), "-v, --version", "Display the version number")
.addHelpText(
"afterAll",
`\n The create-trigger CLI was inspired by ${chalk
.hex("#E8DCFF")
.bold("create-t3-stack")} \n`
)
.parse(process.argv);
const templateName = program.args[0];
if (templateName) {
cliResults.templateName = templateName;
}
cliResults.flags = program.opts();
try {
if (
process.env.SHELL?.toLowerCase().includes("git") &&
process.env.SHELL?.includes("bash")
) {
logger.warn(` WARNING: It looks like you are using Git Bash which is non-interactive. Please run create-t3-app with another
terminal such as Windows Terminal or PowerShell if you want to use the interactive CLI.`);
const error = new Error("Non-interactive environment");
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(error as any).isTTYError = true;
throw error;
}
if (!templateName) {
cliResults.templateName = await promptTemplateName(
cliResults.templateName
);
}
if (!cliResults.flags.projectName) {
cliResults.flags.projectName = await promptProjectName();
}
if (!cliResults.flags.apiKey) {
cliResults.flags.apiKey = await promptApiKey();
}
if (!cliResults.flags.noGit) {
cliResults.flags.noGit = !(await promptGit());
}
if (!cliResults.flags.noInstall) {
cliResults.flags.noInstall = !(await promptInstall());
}
} catch (err) {
// If the user is not calling create-trigger from an interactive terminal, inquirer will throw an error with isTTYError = true
// If this happens, we catch the error, tell the user what has happened, and then continue to run the program with a default trigger project
// Otherwise we have to do some fancy namespace extension logic on the Error type which feels overkill for one line
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (err instanceof Error && (err as any).isTTYError) {
logger.warn(
`${CREATE_TRIGGER} needs an interactive terminal to provide options`
);
const { shouldContinue } = await inquirer.prompt<{
shouldContinue: boolean;
}>({
name: "shouldContinue",
type: "confirm",
message: `Continue creating a trigger.dev project?`,
default: true,
});
if (!shouldContinue) {
logger.info("Exiting...");
process.exit(0);
}
logger.info(
`Bootstrapping the default Trigger.dev template in ./${cliResults.templateName}`
);
} else {
throw err;
}
}
return cliResults;
};
const promptTemplateName = async (
defaultTemplateName: string
): Promise<string> => {
const templates = await getTemplates();
if (templates.length === 0) {
return defaultTemplateName;
}
const defaultTemplate = templates.find(
(template) => template.slug === defaultTemplateName
);
const templateChoicesWithoutDefault = templates
.filter((template) => template.slug !== defaultTemplateName)
.map((template) => ({
name: `${template.shortTitle} - ${template.description} [${terminalLink(
"View more",
template.repositoryUrl
)}]`,
value: template.slug,
}));
const separator = new inquirer.Separator();
const choices = defaultTemplate
? [
{
name: `${defaultTemplate.shortTitle} - ${
defaultTemplate.description
} [${terminalLink("View more", defaultTemplate.repositoryUrl)}]`,
value: defaultTemplate.slug,
},
separator,
...templateChoicesWithoutDefault,
]
: templateChoicesWithoutDefault;
const { templateName } = await inquirer.prompt<{ templateName: string }>({
name: "templateName",
type: "list",
message: "What template would you like to use?",
choices,
default: defaultTemplateName,
});
logger.success(`Great! We're using the ${templateName} template`);
return templateName;
};
const promptProjectName = async (): Promise<string> => {
const { projectName } = await inquirer.prompt<{ projectName: string }>({
name: "projectName",
type: "input",
message: "What would you like to name your project?",
default: DEFAULT_PROJECT_NAME,
});
logger.success(`Great! We're creating your project at ${projectName}`);
return projectName;
};
const promptApiKey = async (): Promise<string | undefined> => {
// First prompt if they want to enter their API key now, and if they say Yes, then prompt for it and return it
const { apiKey } = await inquirer.prompt<{ apiKey: string | undefined }>({
type: "input",
name: "apiKey",
message: "Enter your development API key (optional)",
default: undefined,
validate: (input) => {
// Make sure they enter something like trigger_development_******** or trigger_dev_********
if (input) {
if (
!input.startsWith("trigger_development_") ||
!input.startsWith("trigger_dev_")
) {
return "Please enter a valid development API key or leave blank to skip";
}
}
return true;
},
});
if (apiKey) {
logger.success(
`Fantastic! We'll save the API key (${obfuscateApiKey(
apiKey
)}) in the .env file.`
);
}
return apiKey;
};
const promptGit = async (): Promise<boolean> => {
const { git } = await inquirer.prompt<{ git: boolean }>({
name: "git",
type: "confirm",
message: "Initialize a new git repository?",
default: true,
});
if (git) {
logger.success("Nice one! Initializing repository!");
} else {
logger.info("Sounds good! You can come back and run git init later.");
}
return git;
};
const promptInstall = async (): Promise<boolean> => {
const pkgManager = getUserPkgManager();
const { install } = await inquirer.prompt<{ install: boolean }>({
name: "install",
type: "confirm",
message:
`Would you like us to run '${pkgManager}` +
(pkgManager === "yarn" ? `'?` : ` install'?`),
default: true,
});
if (install) {
logger.success("Alright. We'll install the dependencies for you!");
} else {
if (pkgManager === "yarn") {
logger.info(
`No worries. You can run '${pkgManager}' later to install the dependencies.`
);
} else {
logger.info(
`No worries. You can run '${pkgManager} install' later to install the dependencies.`
);
}
}
return install;
};
export const obfuscateApiKey = (apiKey: string) => {
const [prefix, slug, secretPart] = apiKey.split("_") as [
string,
string,
string
];
return `${prefix}_${slug}_${"*".repeat(secretPart.length)}`;
};
+22
View File
@@ -0,0 +1,22 @@
import path from "path";
import { fileURLToPath } from "url";
// With the move to TSUP as a build tool, this keeps path routes in other files (installers, loaders, etc) in check more easily.
// Path is in relation to a single index.js file inside ./dist
const __filename = fileURLToPath(import.meta.url);
const distPath = path.dirname(__filename);
export const PKG_ROOT = path.join(distPath, "../");
export const TITLE_TEXT = `
_____ _ _
|_ _| ___ |_| ___ ___ ___ ___ _| | ___ _ _
| | | _|| || . || . || -_|| _| _ | . || -_|| | |
|_| |_| |_||_ ||_ ||___||_| |_||___||___| \\_/
|___||___|
`;
export const DEFAULT_APP_NAME = "my-triggers";
export const CREATE_TRIGGER = "create-trigger";
export const TEMPLATE_ORGANIZATION = "triggerdotdev";
export const TRIGGER_BASE_URL =
process.env.TRIGGER_BASE_URL ?? "https://app.trigger.dev";
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env node
import { runCli } from "./cli/index.js";
import { createProject } from "./utils/createProject.js";
import { logger } from "./utils/logger.js";
import { renderTitle } from "./utils/renderTitle.js";
import { createTemplateRef } from "./utils/templateRef.js";
import { installDependencies } from "./utils/installDependencies.js";
import { initializeGit } from "./utils/git.js";
import { parseNameAndPath } from "./utils/parseNameAndPath.js";
import { logNextSteps } from "./utils/logNextSteps.js";
import { createDotEnvFile } from "./utils/createDotEnvFile.js";
import { sendTelemetry } from "./utils/triggerApi.js";
import { createTelemetryEvent } from "./utils/createTelemetryEvent.js";
const main = async () => {
renderTitle();
const cli = await runCli();
const repositoryRef = createTemplateRef(cli.templateName);
const [scopedProjectName, projectDir] = parseNameAndPath(
cli.flags.projectName
);
const projectPath = await createProject(
repositoryRef,
projectDir,
scopedProjectName ?? cli.templateName
);
if (!projectPath) {
process.exit(1);
}
if (!cli.flags.noInstall) {
await installDependencies(projectPath);
}
if (!cli.flags.noGit) {
await initializeGit(projectPath);
}
await createDotEnvFile(projectPath, cli.flags.apiKey);
await logNextSteps({
projectName: projectDir,
noInstall: cli.flags.noInstall,
apiKey: cli.flags.apiKey,
});
if (!cli.flags.noTelemetry) {
await sendTelemetry(createTelemetryEvent(cli), cli.flags.apiKey);
}
process.exit(0);
};
main().catch((err) => {
logger.error("Aborting installation...");
if (err instanceof Error) {
logger.error(err);
} else {
logger.error(
"An unknown error has occurred. Please open an issue on github with the below:"
);
console.log(err);
}
process.exit(1);
});
@@ -0,0 +1,14 @@
import path from "path";
import fs from "fs-extra";
export async function createDotEnvFile(projectPath: string, apiKey?: string) {
const envPath = path.join(projectPath, ".env");
const envExists = await fs.pathExists(envPath);
if (envExists) {
return;
}
const envContents = apiKey
? `TRIGGER_API_KEY=${apiKey}`
: "TRIGGER_API_KEY=<enter your API key here>";
await fs.writeFile(envPath, envContents);
}
@@ -0,0 +1,74 @@
import path from "node:path";
import degit from "degit";
import ora from "ora";
import chalk from "chalk";
import fs from "fs-extra";
import { logger } from "./logger.js";
export async function createProject(
repositoryRef: string,
projectDir: string,
projectName: string
) {
const emitter = degit(repositoryRef);
emitter.on("info", (info) => {
console.log(info.message);
});
emitter.on("warn", (warning) => {
console.warn(warning.message);
});
const projectPath = path.resolve(process.cwd(), projectDir);
// If the project directory already exists, log an error and exit
if (fs.existsSync(projectPath)) {
logger.error(`A directory already exists at: ${projectPath}`);
return;
}
const spinner = ora(
`Copying ${repositoryRef} to: ${projectDir}...\n`
).start();
spinner.start();
await emitter.clone(projectPath);
// Rewrite the package.json file to use the new project name
updatePackageJson(projectName, projectPath);
// Rewrite the README.md file to use the new project name
updateReadme(projectName, projectPath);
// Remove package-lock.json
fs.removeSync(path.resolve(projectPath, "package-lock.json"));
// Remove .env.example
fs.removeSync(path.resolve(projectPath, ".env.example"));
spinner.succeed(
`${chalk.cyan.bold(projectName)} ${chalk.green("copied successfully!")}\n`
);
return projectDir;
}
function updatePackageJson(projectName: string, projectDir: string) {
const existingPackageJson = fs.readJSONSync(
path.resolve(projectDir, "package.json")
);
const newPackageJson = {
...existingPackageJson,
name: projectName,
};
fs.writeJSONSync(path.resolve(projectDir, "package.json"), newPackageJson, {
spaces: 2,
});
}
function updateReadme(projectName: string, projectDir: string) {
const existingReadme = fs.readFileSync(path.resolve(projectDir, "README.md"));
fs.writeFileSync(path.resolve(projectDir, "README.md"), existingReadme);
}
@@ -0,0 +1,21 @@
import { CliResults } from "../cli/index.js";
import { getVersion } from "./getVersion.js";
import { TelemetryEvent } from "./triggerApi.js";
import { randomUUID } from "crypto";
export function createTelemetryEvent(cli: CliResults): TelemetryEvent {
return {
id: `anon:${randomUUID()}`,
event: "scaffolded template",
properties: {
projectName: cli.flags.projectName,
templateName: cli.templateName,
noInstall: cli.flags.noInstall,
noGit: cli.flags.noGit,
arch: process.arch,
platform: process.platform,
nodeVersion: process.version,
packageVersion: getVersion(),
},
};
}
@@ -0,0 +1,19 @@
export type PackageManager = "npm" | "pnpm" | "yarn";
export const getUserPkgManager: () => PackageManager = () => {
// This environment variable is set by npm and yarn but pnpm seems less consistent
const userAgent = process.env.npm_config_user_agent;
if (userAgent) {
if (userAgent.startsWith("yarn")) {
return "yarn";
} else if (userAgent.startsWith("pnpm")) {
return "pnpm";
} else {
return "npm";
}
} else {
// If no user agent is set, assume npm
return "npm";
}
};
@@ -0,0 +1,12 @@
import { type PackageJson } from "type-fest";
import path from "path";
import fs from "fs-extra";
import { PKG_ROOT } from "../consts.js";
export const getVersion = () => {
const packageJsonPath = path.join(PKG_ROOT, "package.json");
const packageJsonContent = fs.readJSONSync(packageJsonPath) as PackageJson;
return packageJsonContent.version ?? "1.0.0";
};
+137
View File
@@ -0,0 +1,137 @@
import chalk from "chalk";
import { execSync } from "child_process";
import { execa } from "execa";
import fs from "fs-extra";
import inquirer from "inquirer";
import ora from "ora";
import path from "path";
import { logger } from "./logger.js";
const isGitInstalled = (dir: string): boolean => {
try {
execSync("git --version", { cwd: dir });
return true;
} catch (_e) {
return false;
}
};
/** @returns Whether or not the provided directory has a `.git` subdirectory in it. */
const isRootGitRepo = (dir: string): boolean => {
return fs.existsSync(path.join(dir, ".git"));
};
/** @returns Whether or not this directory or a parent directory has a `.git` directory. */
const isInsideGitRepo = async (dir: string): Promise<boolean> => {
try {
// If this command succeeds, we're inside a git repo
await execa("git", ["rev-parse", "--is-inside-work-tree"], {
cwd: dir,
stdout: "ignore",
});
return true;
} catch (_e) {
// Else, it will throw a git-error and we return false
return false;
}
};
const getGitVersion = () => {
const stdout = execSync("git --version").toString().trim();
const gitVersionTag = stdout.split(" ")[2];
const major = gitVersionTag?.split(".")[0];
const minor = gitVersionTag?.split(".")[1];
return { major: Number(major), minor: Number(minor) };
};
/** @returns The git config value of "init.defaultBranch". If it is not set, returns "main". */
const getDefaultBranch = () => {
const stdout = execSync("git config --global init.defaultBranch || echo main")
.toString()
.trim();
return stdout;
};
// This initializes the Git-repository for the project
export const initializeGit = async (projectDir: string) => {
logger.info("Initializing Git...");
if (!isGitInstalled(projectDir)) {
logger.warn("Git is not installed. Skipping Git initialization.");
return;
}
const spinner = ora("Creating a new git repo...\n").start();
const isRoot = isRootGitRepo(projectDir);
const isInside = await isInsideGitRepo(projectDir);
const dirName = path.parse(projectDir).name; // skip full path for logging
if (isInside && isRoot) {
// Dir is a root git repo
spinner.stop();
const { overwriteGit } = await inquirer.prompt<{
overwriteGit: boolean;
}>({
name: "overwriteGit",
type: "confirm",
message: `${chalk.redBright.bold(
"Warning:"
)} Git is already initialized in "${dirName}". Initializing a new git repository would delete the previous history. Would you like to continue anyways?`,
default: false,
});
if (!overwriteGit) {
spinner.info("Skipping Git initialization.");
return;
}
// Deleting the .git folder
fs.removeSync(path.join(projectDir, ".git"));
} else if (isInside && !isRoot) {
// Dir is inside a git worktree
spinner.stop();
const { initializeChildGitRepo } = await inquirer.prompt<{
initializeChildGitRepo: boolean;
}>({
name: "initializeChildGitRepo",
type: "confirm",
message: `${chalk.redBright.bold(
"Warning:"
)} "${dirName}" is already in a git worktree. Would you still like to initialize a new git repository in this directory?`,
default: false,
});
if (!initializeChildGitRepo) {
spinner.info("Skipping Git initialization.");
return;
}
}
// We're good to go, initializing the git repo
try {
const branchName = getDefaultBranch();
// --initial-branch flag was added in git v2.28.0
const { major, minor } = getGitVersion();
if (major < 2 || minor < 28) {
await execa("git", ["init"], { cwd: projectDir });
await execa("git", ["branch", "-m", branchName], { cwd: projectDir });
} else {
await execa("git", ["init", `--initial-branch=${branchName}`], {
cwd: projectDir,
});
}
await execa("git", ["add", "."], { cwd: projectDir });
spinner.succeed(
`${chalk.green("Successfully initialized and staged")} ${chalk.green.bold(
"git"
)}\n`
);
} catch (error) {
// Safeguard, should be unreachable
spinner.fail(
`${chalk.bold.red(
"Failed:"
)} could not initialize git. Update git to the latest version!\n`
);
}
};
@@ -0,0 +1,74 @@
import { getUserPkgManager, type PackageManager } from "./getUserPkgManager.js";
import { logger } from "./logger.js";
import ora, { type Ora } from "ora";
import chalk from "chalk";
import { execa } from "execa";
export async function installDependencies(projectDir: string) {
logger.info("Installing dependencies...");
const pkgManager = getUserPkgManager();
const installSpinner = await runInstallCommand(pkgManager, projectDir);
// If the spinner was used to show the progress, use succeed method on it
// If not, use the succeed on a new spinner
(installSpinner || ora()).succeed(
chalk.green("Successfully installed dependencies!\n")
);
}
async function runInstallCommand(
pkgManager: PackageManager,
projectDir: string
): Promise<Ora | null> {
switch (pkgManager) {
// When using npm, inherit the stderr stream so that the progress bar is shown
case "npm":
await execa(pkgManager, ["install"], {
cwd: projectDir,
stderr: "inherit",
});
return null;
// When using yarn or pnpm, use the stdout stream and ora spinner to show the progress
case "pnpm":
const pnpmSpinner = ora("Running pnpm install...").start();
const pnpmSubprocess = execa(pkgManager, ["install"], {
cwd: projectDir,
stdout: "pipe",
});
await new Promise<void>((res, rej) => {
pnpmSubprocess.stdout?.on("data", (data: Buffer) => {
const text = data.toString();
if (text.includes("Progress")) {
pnpmSpinner.text = text.includes("|")
? text.split(" | ")[1] ?? ""
: text;
}
});
pnpmSubprocess.on("error", (e) => rej(e));
pnpmSubprocess.on("close", () => res());
});
return pnpmSpinner;
case "yarn":
const yarnSpinner = ora("Running yarn...").start();
const yarnSubprocess = execa(pkgManager, [], {
cwd: projectDir,
stdout: "pipe",
});
await new Promise<void>((res, rej) => {
yarnSubprocess.stdout?.on("data", (data: Buffer) => {
yarnSpinner.text = data.toString();
});
yarnSubprocess.on("error", (e) => rej(e));
yarnSubprocess.on("close", () => res());
});
return yarnSpinner;
}
}
@@ -0,0 +1,46 @@
import { DEFAULT_APP_NAME, TRIGGER_BASE_URL } from "../consts.js";
import { getUserPkgManager } from "./getUserPkgManager.js";
import { logger } from "./logger.js";
import { whoami } from "./triggerApi.js";
// This logs the next steps that the user should take in order to advance the project
export async function logNextSteps({
projectName = DEFAULT_APP_NAME,
noInstall,
apiKey,
}: {
projectName: string;
noInstall: boolean;
apiKey?: string;
}) {
const pkgManager = getUserPkgManager();
logger.info("Next steps:");
projectName !== "." && logger.info(` cd ${projectName}`);
if (noInstall) {
// To reflect yarn's default behavior of installing packages when no additional args provided
if (pkgManager === "yarn") {
logger.info(` ${pkgManager}`);
} else {
logger.info(` ${pkgManager} install`);
}
}
if (!apiKey) {
logger.info(
` visit ${TRIGGER_BASE_URL} to get your development API key and update your .env file`
);
}
logger.info(` ${pkgManager === "npm" ? "npm run" : pkgManager} dev`);
if (apiKey) {
const org = await whoami(apiKey);
if (org) {
logger.info(
` visit ${TRIGGER_BASE_URL}/orgs/${org.organizationSlug} to see your triggers`
);
}
}
}
@@ -0,0 +1,16 @@
import chalk from "chalk";
export const logger = {
error(...args: unknown[]) {
console.log(chalk.red(...args));
},
warn(...args: unknown[]) {
console.log(chalk.yellow(...args));
},
info(...args: unknown[]) {
console.log(chalk.cyan(...args));
},
success(...args: unknown[]) {
console.log(chalk.green(...args));
},
};
@@ -0,0 +1,38 @@
import pathModule from "path";
/**
* Parses the projectName and its path from the user input.
*
* Returns a tuple of of `[projectName, path]`, where `projectName` is the name put in the "package.json"
* file and `path` is the path to the directory where the project will be created.
*
* If `projectName` is ".", the name of the directory will be used instead. Handles the case where the
* input includes a scoped package name in which case that is being parsed as the name, but not
* included as the path.
*
* For example:
*
* - dir/@mono/app => ["@mono/app", "dir/app"]
* - dir/app => ["app", "dir/app"]
*/
export const parseNameAndPath = (input: string) => {
const paths = input.split("/");
let projectName = paths[paths.length - 1];
// If the user ran `npx create-t3-app .` or similar, the projectName should be the current directory
if (projectName === ".") {
const parsedCwd = pathModule.resolve(process.cwd());
projectName = pathModule.basename(parsedCwd);
}
// If the first part is a @, it's a scoped package
const indexOfDelimiter = paths.findIndex((p) => p.startsWith("@"));
if (paths.findIndex((p) => p.startsWith("@")) !== -1) {
projectName = paths.slice(indexOfDelimiter).join("/");
}
const path = paths.filter((p) => !p.startsWith("@")).join("/");
return [projectName, path] as const;
};
@@ -0,0 +1,24 @@
import gradient from "gradient-string";
import { TITLE_TEXT } from "../consts.js";
import { getUserPkgManager } from "./getUserPkgManager.js";
// colors brought in from vscode poimandres theme
const poimandresTheme = {
blue: "#add7ff",
cyan: "#89ddff",
green: "#5de4c7",
magenta: "#fae4fc",
red: "#d0679d",
yellow: "#fffac2",
};
export const renderTitle = () => {
const triggerGradient = gradient(Object.values(poimandresTheme));
// resolves weird behavior where the ascii is offset
const pkgManager = getUserPkgManager();
if (pkgManager === "yarn" || pkgManager === "pnpm") {
console.log("");
}
console.log(triggerGradient.multiline(TITLE_TEXT));
};
@@ -0,0 +1,5 @@
import { TEMPLATE_ORGANIZATION } from "../consts.js";
export function createTemplateRef(templateName: string): string {
return `github:${TEMPLATE_ORGANIZATION}/${templateName}`;
}
@@ -0,0 +1,80 @@
import fetch from "node-fetch";
import { TRIGGER_BASE_URL } from "../consts.js";
export type WhoamiResponse = {
organizationId: number;
env: string;
organizationSlug: string;
};
export async function whoami(
apiKey: string
): Promise<WhoamiResponse | undefined> {
const response = await fetch(`${TRIGGER_BASE_URL}/api/v1/internal/whoami`, {
method: "GET",
headers: {
Accept: "application/json",
Authorization: `Bearer ${apiKey}`,
},
});
if (response.ok) {
return response.json() as Promise<WhoamiResponse>;
}
return;
}
export type TriggerTemplate = {
id: string;
slug: string;
title: string;
shortTitle: string;
description: string;
imageUrl: string;
repositoryUrl: string;
markdownDocs: string;
runLocalDocs: string;
priority: number;
services: string[];
workflowIds: string[];
createdAt: string;
updatedAt: string;
};
export async function getTemplates(): Promise<Array<TriggerTemplate>> {
const response = await fetch(`${TRIGGER_BASE_URL}/api/v1/templates`, {
method: "GET",
headers: {
Accept: "application/json",
},
});
if (response.ok) {
return response.json() as Promise<Array<TriggerTemplate>>;
}
return [];
}
export type TelemetryEvent = {
id: string;
event: string;
properties: Record<string | number, any>;
};
export async function sendTelemetry(event: TelemetryEvent, apiKey?: string) {
const headers: Record<string, string> = {
Accept: "application/json",
};
if (apiKey) {
headers.Authorization = `Bearer ${apiKey}`;
}
await fetch(`${TRIGGER_BASE_URL}/api/v1/internal/telemetry`, {
method: "POST",
headers,
body: JSON.stringify(event),
});
}
+49
View File
@@ -0,0 +1,49 @@
{
"include": ["src", "tsup.config.ts"],
"compilerOptions": {
/* LANGUAGE COMPILATION OPTIONS */
"target": "ES2020",
"lib": ["DOM", "DOM.Iterable", "ES2020"],
"module": "Node16",
"moduleResolution": "nodenext",
"resolveJsonModule": true,
"allowJs": true,
"checkJs": true,
/* EMIT RULES */
"outDir": "./dist",
"noEmit": true, // TSUP takes care of emitting js for us, in a MUCH faster way
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"removeComments": true,
/* TYPE CHECKING RULES */
"strict": true,
// "noImplicitAny": true, // Included in "Strict"
// "noImplicitThis": true, // Included in "Strict"
// "strictBindCallApply": true, // Included in "Strict"
// "strictFunctionTypes": true, // Included in "Strict"
// "strictNullChecks": true, // Included in "Strict"
// "strictPropertyInitialization": true, // Included in "Strict"
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"useUnknownInCatchVariables": true,
"noUncheckedIndexedAccess": true, // TLDR - Checking an indexed value (array[0]) now forces type <T | undefined> as there is no confirmation that index exists
// THE BELOW ARE EXTRA STRICT OPTIONS THAT SHOULD ONLY BY CONSIDERED IN VERY SAFE PROJECTS
// "exactOptionalPropertyTypes": true, // TLDR - Setting to undefined is not the same as a property not being defined at all
// "noPropertyAccessFromIndexSignature": true, // TLDR - Use dot notation for objects if youre sure it exists, use ['index'] notaion if unsure
/* OTHER OPTIONS */
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
// "emitDecoratorMetadata": true,
// "experimentalDecorators": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"useDefineForClassFields": true
}
}
+16
View File
@@ -0,0 +1,16 @@
import { defineConfig } from "tsup";
const isDev = process.env.npm_lifecycle_event === "dev";
export default defineConfig({
clean: true,
dts: true,
entry: ["src/index.ts"],
format: ["esm"],
minify: !isDev,
metafile: !isDev,
sourcemap: true,
target: "esnext",
outDir: "dist",
onSuccess: isDev ? "node dist/index.js" : undefined,
});
+7 -1
View File
@@ -2,8 +2,8 @@
const logLevels = [
"disabled",
"log",
"error",
"log",
"warn",
"info",
"debug",
@@ -38,6 +38,12 @@ export class Logger {
console.log(`${this.#formatName()} `, ...[...args, ...this.#formatTags()]);
}
logClean(...args: any[]) {
if (this.#level < 1) return;
console.log(`${this.#formatName()} `, ...args);
}
error(...args: any[]) {
if (this.#level < 2) return;
@@ -20,6 +20,7 @@ export const HostRPCSchema = {
apiKey: z.string(),
isTest: z.boolean().default(false),
appOrigin: z.string(),
attempt: z.number().optional(),
}),
}),
response: z.boolean(),
@@ -86,6 +86,44 @@ export const ServerRPCSchema = {
])
.nullable(),
},
INITIALIZE_HOST_V2: {
request: z.object({
apiKey: z.string(),
workflowId: z.string(),
workflowName: z.string(),
trigger: TriggerMetadataSchema,
packageVersion: z.string(),
packageName: z.string(),
triggerTTL: z.number().optional(),
}),
response: z
.discriminatedUnion("type", [
z.object({
type: z.literal("success"),
data: z.object({
workflow: z.object({
id: z.string(),
slug: z.string(),
}),
environment: z.object({
id: z.string(),
slug: z.string(),
}),
organization: z.object({
id: z.string(),
slug: z.string(),
}),
isNew: z.boolean(),
url: z.string(),
}),
}),
z.object({
type: z.literal("error"),
message: z.string(),
}),
])
.nullable(),
},
START_WORKFLOW_RUN: {
request: z.object({
runId: z.string(),
+21 -3
View File
@@ -6,11 +6,13 @@ import { Logger } from "../logger";
export class InternalApiClient {
#apiKey: string;
#baseUrl: string;
#v2BaseUrl: string;
#logger: Logger;
constructor(apiKey: string, baseUrl: string) {
this.#apiKey = apiKey;
this.#baseUrl = `${baseUrl}/api/v1/internal`;
this.#v2BaseUrl = `${baseUrl}/api/v2/internal`;
this.#logger = new Logger("trigger.dev [internal-api]");
}
@@ -18,6 +20,7 @@ export class InternalApiClient {
const ResponseSchema = z.object({
organizationId: z.string(),
env: z.string(),
organizationSlug: z.string(),
});
const Response401Schema = z.object({
@@ -61,14 +64,26 @@ export class InternalApiClient {
async registerWorkflow(workflow: WorkflowMetadata) {
const responseSchema = z.object({
id: z.string(),
workflow: z.object({
id: z.string(),
slug: z.string(),
}),
environment: z.object({
id: z.string(),
slug: z.string(),
}),
organization: z.object({
id: z.string(),
slug: z.string(),
}),
url: z.string(),
});
const validationResponseSchema = z.object({
error: z.string(),
});
const response = await fetch(this.#apiUrl(`/workflows/${workflow.id}`), {
const response = await fetch(this.#v2ApiUrl(`/workflows/${workflow.id}`), {
method: "PUT",
headers: this.#headers({ "Content-Type": "application/json" }),
body: JSON.stringify(workflow),
@@ -77,7 +92,9 @@ export class InternalApiClient {
if (response.ok) {
const rawBody = await response.json();
return responseSchema.parse(rawBody);
const body = responseSchema.parse(rawBody);
return { ...body, isNew: response.status === 201 };
}
if (response.status === 400) {
@@ -123,6 +140,7 @@ export class InternalApiClient {
}
#apiUrl = (path: string) => `${this.#baseUrl}${path}`;
#v2ApiUrl = (path: string) => `${this.#v2BaseUrl}${path}`;
#headers = (additionalHeaders?: Record<string, string>) => ({
Accept: "application/json",
Authorization: `Bearer ${this.#apiKey}`,
@@ -9,6 +9,7 @@ const Catalog = {
"x-ttl": z.coerce.number().optional(),
"x-is-test": z.string().default("false"),
"x-app-origin": z.string().default("https://app.trigger.dev"),
"x-attempt": z.string().optional(),
}),
},
};
+41
View File
@@ -1,5 +1,46 @@
# @trigger.dev/sdk
## 0.2.17
### Patch Changes
- 3a2cf0dd: Fixed the missing error message when logging invalid API key and improved the error message
## 0.2.16
### Patch Changes
- ee20f921: Make the schema an optional param for customEvent and webhookEvent
- 4f47d031: Give a better error message when the API key is invalid
- 87a3bbee: Added a more helpful error message when missing an API key
- 51f9bc9d: Added handly links to the dashboard in log feedback
- 0932ae7d: Log out when a run first starts as well
## 0.2.16-next.3
### Patch Changes
- Give a better error message when the API key is invalid
## 0.2.16-next.2
### Patch Changes
- 87a3bbee: Added a more helpful error message when missing an API key
## 0.2.16-next.1
### Patch Changes
- 0932ae7d: Log out when a run first starts as well
## 0.2.16-next.0
### Patch Changes
- ee20f921: Make the schema an optional param for customEvent and webhookEvent
- 51f9bc9d: Added handly links to the dashboard in log feedback
## 0.2.15
### Patch Changes
+4 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/sdk",
"version": "0.2.15",
"version": "0.2.17",
"description": "trigger.dev Node.JS SDK",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -37,10 +37,13 @@
"build:tsup": "tsup --dts-resolve"
},
"dependencies": {
"chalk": "^5.2.0",
"debug": "^4.3.4",
"evt": "^2.4.13",
"get-caller-file": "^2.0.5",
"node-fetch": "2.6.x",
"slug": "^6.0.0",
"terminal-link": "^3.0.0",
"ulid": "^2.3.0",
"uuid": "^9.0.0",
"ws": "^8.11.0",
+72 -8
View File
@@ -15,6 +15,8 @@ import { ContextLogger } from "./logger";
import { Trigger, TriggerOptions } from "./trigger";
import { TriggerContext, TriggerFetch } from "./types";
import { generateErrorMessage, ErrorMessageOptions } from "zod-error";
import terminalLink from "terminal-link";
import chalk from "chalk";
const zodErrorMessageOptions: ErrorMessageOptions = {
delimiter: {
@@ -39,6 +41,23 @@ export class TriggerClient<TSchema extends z.ZodTypeAny> {
#logger: Logger;
#closedByUser = false;
#registerResponse?: {
workflow: {
id: string;
slug: string;
};
environment: {
id: string;
slug: string;
};
organization: {
id: string;
slug: string;
};
isNew: boolean;
url: string;
};
#responseCompleteCallbacks = new Map<
string,
{
@@ -100,7 +119,25 @@ export class TriggerClient<TSchema extends z.ZodTypeAny> {
this.#initializeRPC();
await this.#initializeHost();
this.#logger.log(`✨ Connected and listening for events`);
if (this.#registerResponse?.isNew) {
this.#logger.logClean(
`🎉 Successfully registered "${
this.#trigger.name
}" to trigger.dev 👉 ${terminalLink(
"View on dashboard",
this.#registerResponse.url,
{ fallback: (text, url) => `${text}: (${url})` }
)}. Listening for events...`
);
} else {
this.#logger.log(
`✨ Connected and listening for events 👉 ${terminalLink(
"View on dashboard",
this.#registerResponse!.url,
{ fallback: (text, url) => `${text}: (${url})` }
)}`
);
}
} catch (error) {
this.#logger.log(`🚩 Could not connect to trigger.dev`);
@@ -144,11 +181,11 @@ export class TriggerClient<TSchema extends z.ZodTypeAny> {
return;
}
this.#logger.error(`🚩 Could not connect to trigger.dev (code ${code})`);
if (reason) {
this.#logger.error("Reason:", reason);
}
this.#logger.error(
`${chalk.red("error")} Could not connect to trigger.dev${
reason ? `: ${reason}` : `(code ${code})`
}`
);
// If #isConnected is already false, that means we are already trying to reconnect
if (!this.#isConnected) return;
@@ -590,6 +627,19 @@ export class TriggerClient<TSchema extends z.ZodTypeAny> {
() => {
this.#logger.debug("Running trigger...");
if (
typeof data.meta.attempt === "number" &&
data.meta.attempt === 0
) {
this.#logger.log(
`Run ${data.id} started 👉 ${terminalLink(
"View on dashboard",
`${this.#registerResponse!.url}/runs/${data.id}`,
{ fallback: (text, url) => `${text}: (${url})` }
)}`
);
}
serverRPC
.send("START_WORKFLOW_RUN", {
runId: data.id,
@@ -599,7 +649,13 @@ export class TriggerClient<TSchema extends z.ZodTypeAny> {
return this.#trigger.options
.run(eventData, ctx)
.then((output) => {
this.#logger.log(`Run ${data.id} complete 🏃`);
this.#logger.log(
`Run ${data.id} complete 👉 ${terminalLink(
"View on dashboard",
`${this.#registerResponse!.url}/runs/${data.id}`,
{ fallback: (text, url) => `${text}: (${url})` }
)}`
);
return serverRPC.send("COMPLETE_WORKFLOW_RUN", {
runId: data.id,
@@ -676,7 +732,7 @@ export class TriggerClient<TSchema extends z.ZodTypeAny> {
throw new Error("Cannot initialize host without an RPC connection");
}
const response = await this.#send("INITIALIZE_HOST", {
const response = await this.#send("INITIALIZE_HOST_V2", {
apiKey: this.#apiKey,
workflowId: this.#trigger.id,
workflowName: this.#trigger.name,
@@ -686,10 +742,18 @@ export class TriggerClient<TSchema extends z.ZodTypeAny> {
triggerTTL: this.#options.triggerTTL,
});
console.log(response);
if (!response) {
throw new Error("Could not initialize workflow with server");
}
if (response?.type === "error") {
throw new Error(response.message);
}
this.#registerResponse = response.data;
this.#logger.debug("Successfully initialized workflow with server");
}
+57
View File
@@ -1,9 +1,11 @@
import { TriggerClient } from "../client";
import { LogLevel } from "internal-bridge";
import { TriggerEvent } from "../events";
import chalk from "chalk";
import type { TriggerContext } from "../types";
import { z } from "zod";
import terminalLink from "terminal-link";
export type TriggerOptions<TSchema extends z.ZodTypeAny> = {
id: string;
@@ -31,6 +33,44 @@ export class Trigger<TSchema extends z.ZodTypeAny> {
}
async listen() {
const apiKey = this.#getApiKey();
if (apiKey.status === "invalid") {
console.log(
`${chalk.red("Trigger.dev error")}: ${chalk.bold(
this.id
)} is has an invalid API key ("${chalk.italic(
apiKey.apiKey
)}"), please set the TRIGGER_API_KEY environment variable or pass the apiKey option to a valid value. ${terminalLink(
"Get your API key here",
"https://app.trigger.dev",
{
fallback(text, url) {
return `${text} 👉 ${url}`;
},
}
)}`
);
return;
} else if (apiKey.status === "missing") {
console.log(
`${chalk.red("Trigger.dev error")}: ${chalk.bold(
this.id
)} is missing an API key, please set the TRIGGER_API_KEY environment variable or pass the apiKey option to the Trigger constructor. ${terminalLink(
"Get your API key here",
"https://app.trigger.dev",
{
fallback(text, url) {
return `${text} 👉 ${url}`;
},
}
)}`
);
return;
}
if (!this.#client) {
this.#client = new TriggerClient(this, this.options);
}
@@ -53,4 +93,21 @@ export class Trigger<TSchema extends z.ZodTypeAny> {
get on() {
return this.options.on;
}
#getApiKey() {
const apiKey = this.options.apiKey ?? process.env.TRIGGER_API_KEY;
if (!apiKey) {
return { status: "missing" as const };
}
// Validate the api_key format (should be trigger_{env}_XXXXX)
const isValid = apiKey.match(/^trigger_[a-z]+_[a-zA-Z0-9]+$/);
if (!isValid) {
return { status: "invalid" as const, apiKey };
}
return { status: "valid" as const, apiKey };
}
}
+371 -51
View File
@@ -92,13 +92,13 @@ importers:
'@types/json-pointer': 1.0.31
'@types/morgan': 1.9.4
'@types/node': 18.14.0
'@typescript-eslint/eslint-plugin': 5.53.0_bzepuo66bcyj4mepwnxofjvdli
'@typescript-eslint/eslint-plugin': 5.53.0_7kw3g6rralp5ps6mg3uyzz6azm
concurrently: 7.6.0
dotenv: 16.0.3
eslint: 8.34.0
eslint-config-prettier: 8.6.0_eslint@8.34.0
eslint-config-standard-with-typescript: 34.0.0_e4cqfx33t3lusso5bte4gguj3u
eslint-plugin-import: 2.27.5_zycheyzypw6s5ouujsf5akzhsy
eslint-plugin-import: 2.27.5_eslint@8.34.0
eslint-plugin-n: 15.6.1_eslint@8.34.0
eslint-plugin-promise: 6.1.1_eslint@8.34.0
nock: 13.3.0
@@ -287,7 +287,7 @@ importers:
'@aws-sdk/client-s3': 3.245.0
'@aws-sdk/s3-request-presigner': 3.245.0
'@cfworker/json-schema': 1.12.5
'@codemirror/autocomplete': 6.4.0_eo6pz6bvsllvatnnwfprpuflde
'@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu
'@codemirror/commands': 6.1.3
'@codemirror/lang-javascript': 6.1.2
'@codemirror/lang-json': 6.0.1
@@ -325,7 +325,7 @@ importers:
'@trigger.dev/slack': link:../../integrations/slack
'@trigger.dev/whatsapp': link:../../integrations/whatsapp
'@typeform/embed-react': 2.14.1_react@18.2.0
'@uiw/react-codemirror': 4.19.5_aguurb4bmecpxzejz52amioxne
'@uiw/react-codemirror': 4.19.5_k4ec5g7vuuzzonc3d6xbjnmmle
bcryptjs: 2.4.3
classnames: 2.3.2
clsx: 1.2.1
@@ -844,7 +844,7 @@ importers:
tsup: ^6.5.0
zod: ^3.20.2
dependencies:
'@react-email/render': 0.0.3_react@18.2.0
'@react-email/render': 0.0.3
debug: 4.3.4
zod: 3.20.2
devDependencies:
@@ -955,6 +955,51 @@ importers:
'@types/node': 16.18.11
typescript: 4.9.4
packages/create-trigger:
specifiers:
'@types/degit': ^2.8.3
'@types/fs-extra': ^11.0.1
'@types/gradient-string': ^1.1.2
'@types/inquirer': ^9.0.3
'@types/node': '16'
'@types/node-fetch': ^2.6.2
chalk: ^5.2.0
commander: ^9.4.1
degit: ^2.8.4
execa: ^7.0.0
fs-extra: ^11.1.0
gradient-string: ^2.0.2
inquirer: ^9.1.4
node-fetch: ^3.3.0
ora: ^6.1.2
rimraf: ^3.0.2
terminal-link: ^3.0.0
tsup: ^6.5.0
type-fest: ^3.6.0
typescript: ^4.9.5
dependencies:
'@types/degit': 2.8.3
chalk: 5.2.0
commander: 9.5.0
degit: 2.8.4
execa: 7.0.0
fs-extra: 11.1.0
gradient-string: 2.0.2
inquirer: 9.1.4
node-fetch: 3.3.0
ora: 6.1.2
terminal-link: 3.0.0
devDependencies:
'@types/fs-extra': 11.0.1
'@types/gradient-string': 1.1.2
'@types/inquirer': 9.0.3
'@types/node': 16.18.11
'@types/node-fetch': 2.6.2
rimraf: 3.0.2
tsup: 6.6.3_typescript@4.9.5
type-fest: 3.6.0
typescript: 4.9.5
packages/emails:
specifiers:
'@react-email/button': ^0.0.4
@@ -1142,12 +1187,15 @@ importers:
'@types/slug': ^5.0.3
'@types/uuid': ^9.0.0
'@types/ws': ^8.5.3
chalk: ^5.2.0
debug: ^4.3.4
evt: ^2.4.13
get-caller-file: ^2.0.5
internal-bridge: workspace:*
node-fetch: 2.6.x
rimraf: ^3.0.2
slug: ^6.0.0
terminal-link: ^3.0.0
tsup: ^6.5.0
tsx: ^3.12.1
ulid: ^2.3.0
@@ -1157,10 +1205,13 @@ importers:
zod-error: ^1.1.0
zod-to-json-schema: ^3.20.2
dependencies:
chalk: 5.2.0
debug: 4.3.4
evt: 2.4.13
get-caller-file: 2.0.5
node-fetch: 2.6.7
slug: 6.1.0
terminal-link: 3.0.0
ulid: 2.3.0
uuid: 9.0.0
ws: 8.12.0
@@ -3784,13 +3835,12 @@ packages:
prettier: 2.8.2
dev: false
/@codemirror/autocomplete/6.4.0_eo6pz6bvsllvatnnwfprpuflde:
/@codemirror/autocomplete/6.4.0_czcfkg2f66rxeiodoti7r2gulu:
resolution: {integrity: sha512-HLF2PnZAm1s4kGs30EiqKMgD7XsYaQ0XJnMR0rofEWQ5t5D60SfqpDIkIh1ze5tiEbyUWm8+VJ6W1/erVvBMIA==}
peerDependencies:
'@codemirror/language': ^6.0.0
'@codemirror/state': ^6.0.0
'@codemirror/view': ^6.0.0
'@lezer/common': ^1.0.0
dependencies:
'@codemirror/language': 6.3.2
'@codemirror/state': 6.2.0
@@ -3810,7 +3860,7 @@ packages:
/@codemirror/lang-javascript/6.1.2:
resolution: {integrity: sha512-OcwLfZXdQ1OHrLiIcKCn7MqZ7nx205CMKlhe+vL88pe2ymhT9+2P+QhwkYGxMICj8TDHyp8HFKVwpiisUT7iEQ==}
dependencies:
'@codemirror/autocomplete': 6.4.0_eo6pz6bvsllvatnnwfprpuflde
'@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu
'@codemirror/language': 6.3.2
'@codemirror/lint': 6.1.0
'@codemirror/state': 6.2.0
@@ -5523,6 +5573,16 @@ packages:
react-dom: 18.2.0_react@18.2.0
dev: false
/@react-email/render/0.0.3:
resolution: {integrity: sha512-+4eOrLGdTCJjoJU3PunekErjo3PFnhSDFjVINBHrfJT+1wlVdhWfDo7hFdzXJx/JOOYqmnZTilU/umGLdRumKQ==}
engines: {node: '>=18.0.0'}
dependencies:
pretty: 2.0.0
react-dom: 18.2.0
transitivePeerDependencies:
- react
dev: false
/@react-email/render/0.0.3_react@18.2.0:
resolution: {integrity: sha512-+4eOrLGdTCJjoJU3PunekErjo3PFnhSDFjVINBHrfJT+1wlVdhWfDo7hFdzXJx/JOOYqmnZTilU/umGLdRumKQ==}
engines: {node: '>=18.0.0'}
@@ -5638,7 +5698,7 @@ packages:
eslint: 8.31.0
eslint-import-resolver-node: 0.3.6
eslint-import-resolver-typescript: 3.5.3_vz4tyq5r7fh66imfi352lmrvhq
eslint-plugin-import: 2.27.5_qdjeohovcytra7xto5vgmxssaq
eslint-plugin-import: 2.27.5_2ac3tknkazjoq5fxmuugu665ny
eslint-plugin-jest: 26.9.0_y6565ziejixavcuubgd3r7fqr4
eslint-plugin-jest-dom: 4.0.3_eslint@8.31.0
eslint-plugin-jsx-a11y: 6.7.1_eslint@8.31.0
@@ -6354,6 +6414,10 @@ packages:
'@types/ms': 0.7.31
dev: true
/@types/degit/2.8.3:
resolution: {integrity: sha512-CL7y71j2zaDmtPLD5Xq5S1Gv2dFoHl0/GBZm6s39Mj/ls28L3NzAOqf7H4H0/2TNVMgMjMVf9CAFYSjmXhi3bw==}
dev: false
/@types/eslint/8.4.10:
resolution: {integrity: sha512-Sl/HOqN8NKPmhWo2VBEPm0nvHnu2LL3v9vKo8MEq0EtbJ4eVzGPl41VNPvn5E1i5poMk4/XD8UriLHpJvEP/Nw==}
dependencies:
@@ -6400,12 +6464,25 @@ packages:
'@types/node': 18.14.0
dev: true
/@types/fs-extra/11.0.1:
resolution: {integrity: sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA==}
dependencies:
'@types/jsonfile': 6.1.1
'@types/node': 18.14.0
dev: true
/@types/glob/7.2.0:
resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==}
dependencies:
'@types/minimatch': 5.1.2
'@types/node': 18.14.0
/@types/gradient-string/1.1.2:
resolution: {integrity: sha512-zIet2KvHr2dkOCPI5ggQQ+WJVyfBSFaqK9sNelhgDjlE2K3Fu2muuPJwu5aKM3xoWuc3WXudVEMUwI1QWhykEQ==}
dependencies:
'@types/tinycolor2': 1.4.3
dev: true
/@types/hast/2.3.4:
resolution: {integrity: sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==}
dependencies:
@@ -6420,6 +6497,13 @@ packages:
resolution: {integrity: sha512-K3e+NZlpCKd6Bd/EIdqjFJRFHbrq5TzPPLwREk5Iv/YoIjQrs6ljdAUCo+Lb2xFlGNOjGSE0dqsVD19cZL137w==}
dev: true
/@types/inquirer/9.0.3:
resolution: {integrity: sha512-CzNkWqQftcmk2jaCWdBTf9Sm7xSw4rkI1zpU/Udw3HX5//adEZUIm9STtoRP1qgWj0CWQtJ9UTvqmO2NNjhMJw==}
dependencies:
'@types/through': 0.0.30
rxjs: 7.8.0
dev: true
/@types/is-ci/3.0.0:
resolution: {integrity: sha512-Q0Op0hdWbYd1iahB+IFNQcWXFq4O0Q5MwQP7uN0souuQ4rPg1vEYcnIOfr1gY+M+6rc8FGoRaBO1mOOvL29sEQ==}
dependencies:
@@ -6468,6 +6552,12 @@ packages:
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
dev: true
/@types/jsonfile/6.1.1:
resolution: {integrity: sha512-GSgiRCVeapDN+3pqA35IkQwasaCh/0YFH5dEF6S88iDvEn901DjOeH3/QPY+XYP1DFzDZPvIvfeEgk+7br5png==}
dependencies:
'@types/node': 18.14.0
dev: true
/@types/jsonwebtoken/9.0.1:
resolution: {integrity: sha512-c5ltxazpWabia/4UzhIoaDcIza4KViOQhdbjRlfcIGVnsE3c3brkz9Z+F/EeJIECOQP7W7US2hNE930cWWkPiw==}
dependencies:
@@ -6671,6 +6761,15 @@ packages:
'@types/jest': 29.2.5
dev: true
/@types/through/0.0.30:
resolution: {integrity: sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg==}
dependencies:
'@types/node': 18.14.0
dev: true
/@types/tinycolor2/1.4.3:
resolution: {integrity: sha512-Kf1w9NE5HEgGxCRyIcRXR/ZYtDv0V8FVPtYHwLxl0O+maGX0erE77pQlD0gpP+/KByMZ87mOA79SjifhSB3PjQ==}
/@types/unist/2.0.6:
resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==}
dev: true
@@ -6731,7 +6830,7 @@ packages:
- supports-color
dev: true
/@typescript-eslint/eslint-plugin/5.53.0_bzepuo66bcyj4mepwnxofjvdli:
/@typescript-eslint/eslint-plugin/5.53.0_7kw3g6rralp5ps6mg3uyzz6azm:
resolution: {integrity: sha512-alFpFWNucPLdUOySmXCJpzr6HKC3bu7XooShWM+3w/EL6J2HIoB2PFxpLnq4JauWVk6DiVeNKzQlFEaE+X9sGw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
@@ -6742,7 +6841,6 @@ packages:
typescript:
optional: true
dependencies:
'@typescript-eslint/parser': 5.48.1_7kw3g6rralp5ps6mg3uyzz6azm
'@typescript-eslint/scope-manager': 5.53.0
'@typescript-eslint/type-utils': 5.53.0_7kw3g6rralp5ps6mg3uyzz6azm
'@typescript-eslint/utils': 5.53.0_7kw3g6rralp5ps6mg3uyzz6azm
@@ -7005,18 +7103,17 @@ packages:
eslint-visitor-keys: 3.3.0
dev: true
/@uiw/codemirror-extensions-basic-setup/4.19.5_tbeldtdcrf45b35pezgkzq2u4e:
/@uiw/codemirror-extensions-basic-setup/4.19.5_wd2tsis3in55bkaiwnc2c46tom:
resolution: {integrity: sha512-1zt7ZPJ01xKkSW/KDy0FZNga0bngN1fC594wCVG7FBi60ehfcAucpooQ+JSPScKXopxcb+ugPKZvVLzr9/OfzA==}
peerDependencies:
'@codemirror/autocomplete': '>=6.0.0'
'@codemirror/commands': '>=6.0.0'
'@codemirror/language': '>=6.0.0'
'@codemirror/lint': '>=6.0.0'
'@codemirror/search': '>=6.0.0'
'@codemirror/state': '>=6.0.0'
'@codemirror/view': '>=6.0.0'
dependencies:
'@codemirror/autocomplete': 6.4.0_eo6pz6bvsllvatnnwfprpuflde
'@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu
'@codemirror/commands': 6.1.3
'@codemirror/language': 6.3.2
'@codemirror/lint': 6.1.0
@@ -7025,14 +7122,11 @@ packages:
'@codemirror/view': 6.7.2
dev: false
/@uiw/react-codemirror/4.19.5_aguurb4bmecpxzejz52amioxne:
/@uiw/react-codemirror/4.19.5_k4ec5g7vuuzzonc3d6xbjnmmle:
resolution: {integrity: sha512-ZCHh8d7beXbF8/t7F1+yHht6A9Y6CdKeOkZq4A09lxJEnyTQrj1FMf2zvfaqc7K23KNjkTCtSlbqKKbVDgrWaw==}
peerDependencies:
'@babel/runtime': '>=7.11.0'
'@codemirror/state': '>=6.0.0'
'@codemirror/theme-one-dark': '>=6.0.0'
'@codemirror/view': '>=6.0.0'
codemirror: '>=6.0.0'
react: '>=16.8.0'
react-dom: '>=16.8.0'
dependencies:
@@ -7041,14 +7135,13 @@ packages:
'@codemirror/state': 6.2.0
'@codemirror/theme-one-dark': 6.1.0
'@codemirror/view': 6.7.2
'@uiw/codemirror-extensions-basic-setup': 4.19.5_tbeldtdcrf45b35pezgkzq2u4e
codemirror: 6.0.1_@lezer+common@1.0.2
'@uiw/codemirror-extensions-basic-setup': 4.19.5_wd2tsis3in55bkaiwnc2c46tom
codemirror: 6.0.1
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
transitivePeerDependencies:
- '@codemirror/autocomplete'
- '@codemirror/language'
- '@codemirror/lint'
- '@codemirror/search'
dev: false
@@ -7261,6 +7354,20 @@ packages:
type-fest: 0.21.3
dev: true
/ansi-escapes/5.0.0:
resolution: {integrity: sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==}
engines: {node: '>=12'}
dependencies:
type-fest: 1.4.0
dev: false
/ansi-escapes/6.0.0:
resolution: {integrity: sha512-IG23inYII3dWlU2EyiAiGj6Bwal5GzsgPMwjYGvc1HPE2dgbj4ZB5ToWBKSquKw74nB3TIuOwaI6/jSULzfgrw==}
engines: {node: '>=14.16'}
dependencies:
type-fest: 3.6.0
dev: false
/ansi-regex/2.1.1:
resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==}
engines: {node: '>=0.10.0'}
@@ -7273,7 +7380,6 @@ packages:
/ansi-regex/6.0.1:
resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==}
engines: {node: '>=12'}
dev: true
/ansi-styles/3.2.1:
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
@@ -7295,7 +7401,6 @@ packages:
/ansi-styles/6.2.1:
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
engines: {node: '>=12'}
dev: true
/ansicolors/0.3.2:
resolution: {integrity: sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==}
@@ -7751,6 +7856,14 @@ packages:
inherits: 2.0.4
readable-stream: 3.6.0
/bl/5.1.0:
resolution: {integrity: sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==}
dependencies:
buffer: 6.0.3
inherits: 2.0.4
readable-stream: 3.6.0
dev: false
/blob-util/2.0.2:
resolution: {integrity: sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==}
dev: true
@@ -7879,6 +7992,13 @@ packages:
base64-js: 1.5.1
ieee754: 1.2.1
/buffer/6.0.3:
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
dev: false
/buffers/0.1.1:
resolution: {integrity: sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==}
engines: {node: '>=0.2.0'}
@@ -8135,6 +8255,11 @@ packages:
ansi-styles: 4.3.0
supports-color: 7.2.0
/chalk/5.2.0:
resolution: {integrity: sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==}
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
dev: false
/character-entities-html4/2.1.0:
resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
dev: true
@@ -8234,6 +8359,13 @@ packages:
dependencies:
restore-cursor: 3.1.0
/cli-cursor/4.0.0:
resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
restore-cursor: 4.0.0
dev: false
/cli-progress/3.11.2:
resolution: {integrity: sha512-lCPoS6ncgX4+rJu5bS3F/iCz17kZ9MPZ6dpuTtI0KXKABkhyXIdYB3Inby1OpaGti3YlI3EeEkM9AuWpelJrVA==}
engines: {node: '>=4'}
@@ -8307,6 +8439,11 @@ packages:
engines: {node: '>= 10'}
dev: true
/cli-width/4.0.0:
resolution: {integrity: sha512-ZksGS2xpa/bYkNzN3BAw1wEjsLV/ZKOf/CCrJ/QOBsxx6fOARIkwTutxp1XIOIohi6HKmOFjMoK/XaqDVUpEEw==}
engines: {node: '>= 12'}
dev: false
/client-only/0.0.1:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
dev: false
@@ -8378,18 +8515,16 @@ packages:
engines: {node: '>=0.10.0'}
dev: false
/codemirror/6.0.1_@lezer+common@1.0.2:
/codemirror/6.0.1:
resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==}
dependencies:
'@codemirror/autocomplete': 6.4.0_eo6pz6bvsllvatnnwfprpuflde
'@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu
'@codemirror/commands': 6.1.3
'@codemirror/language': 6.3.2
'@codemirror/lint': 6.1.0
'@codemirror/search': 6.2.3
'@codemirror/state': 6.2.0
'@codemirror/view': 6.7.2
transitivePeerDependencies:
- '@lezer/common'
dev: false
/collection-visit/1.0.0:
@@ -8454,7 +8589,6 @@ packages:
/commander/9.5.0:
resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
engines: {node: ^12.20.0 || >=14}
dev: true
/common-tags/1.8.2:
resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==}
@@ -9016,6 +9150,12 @@ packages:
vm2: 3.9.13
dev: true
/degit/2.8.4:
resolution: {integrity: sha512-vqYuzmSA5I50J882jd+AbAhQtgK6bdKUJIex1JNfEUPENCgYsxugzKVZlFyMwV4i06MmnV47/Iqi5Io86zf3Ng==}
engines: {node: '>=8.0.0'}
hasBin: true
dev: false
/delayed-stream/1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
@@ -9180,7 +9320,6 @@ packages:
/eastasianwidth/0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
dev: true
/ecc-jsbn/0.1.2:
resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==}
@@ -9224,7 +9363,6 @@ packages:
/emoji-regex/9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
dev: true
/emojis-list/3.0.0:
resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==}
@@ -9699,6 +9837,11 @@ packages:
engines: {node: '>=10'}
dev: true
/escape-string-regexp/5.0.0:
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
engines: {node: '>=12'}
dev: false
/escodegen/1.14.3:
resolution: {integrity: sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==}
engines: {node: '>=4.0'}
@@ -9740,11 +9883,11 @@ packages:
eslint-plugin-promise: ^6.0.0
typescript: '*'
dependencies:
'@typescript-eslint/eslint-plugin': 5.53.0_bzepuo66bcyj4mepwnxofjvdli
'@typescript-eslint/eslint-plugin': 5.53.0_7kw3g6rralp5ps6mg3uyzz6azm
'@typescript-eslint/parser': 5.48.1_7kw3g6rralp5ps6mg3uyzz6azm
eslint: 8.34.0
eslint-config-standard: 17.0.0_rwq7hzy2vtlwiajbw6pmw3rkzy
eslint-plugin-import: 2.27.5_zycheyzypw6s5ouujsf5akzhsy
eslint-plugin-import: 2.27.5_eslint@8.34.0
eslint-plugin-n: 15.6.1_eslint@8.34.0
eslint-plugin-promise: 6.1.1_eslint@8.34.0
typescript: 4.9.5
@@ -9761,7 +9904,7 @@ packages:
eslint-plugin-promise: ^6.0.0
dependencies:
eslint: 8.34.0
eslint-plugin-import: 2.27.5_zycheyzypw6s5ouujsf5akzhsy
eslint-plugin-import: 2.27.5_eslint@8.34.0
eslint-plugin-n: 15.6.1_eslint@8.34.0
eslint-plugin-promise: 6.1.1_eslint@8.34.0
dev: true
@@ -9804,7 +9947,7 @@ packages:
debug: 4.3.4
enhanced-resolve: 5.12.0
eslint: 8.31.0
eslint-plugin-import: 2.27.5_qdjeohovcytra7xto5vgmxssaq
eslint-plugin-import: 2.27.5_2ac3tknkazjoq5fxmuugu665ny
get-tsconfig: 4.3.0
globby: 13.1.3
is-core-module: 2.11.0
@@ -9814,7 +9957,7 @@ packages:
- supports-color
dev: true
/eslint-module-utils/2.7.4_fqawsowff3yxll7f25e3byprdq:
/eslint-module-utils/2.7.4_eyqnu5kib2hfrvsonwfdq4ojse:
resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==}
engines: {node: '>=4'}
peerDependencies:
@@ -9835,7 +9978,6 @@ packages:
eslint-import-resolver-webpack:
optional: true
dependencies:
'@typescript-eslint/parser': 5.48.1_7kw3g6rralp5ps6mg3uyzz6azm
debug: 3.2.7
eslint: 8.34.0
eslint-import-resolver-node: 0.3.7
@@ -9843,7 +9985,7 @@ packages:
- supports-color
dev: true
/eslint-module-utils/2.7.4_sqt5xxn4ciiurbqrzlaarm6ama:
/eslint-module-utils/2.7.4_v73lhamtbyinynmwa5fn7kpmfq:
resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==}
engines: {node: '>=4'}
peerDependencies:
@@ -9868,6 +10010,7 @@ packages:
debug: 3.2.7
eslint: 8.31.0
eslint-import-resolver-node: 0.3.7
eslint-import-resolver-typescript: 3.5.3_vz4tyq5r7fh66imfi352lmrvhq
transitivePeerDependencies:
- supports-color
dev: true
@@ -9903,7 +10046,7 @@ packages:
regexpp: 3.2.0
dev: true
/eslint-plugin-import/2.27.5_qdjeohovcytra7xto5vgmxssaq:
/eslint-plugin-import/2.27.5_2ac3tknkazjoq5fxmuugu665ny:
resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==}
engines: {node: '>=4'}
peerDependencies:
@@ -9921,7 +10064,7 @@ packages:
doctrine: 2.1.0
eslint: 8.31.0
eslint-import-resolver-node: 0.3.7
eslint-module-utils: 2.7.4_sqt5xxn4ciiurbqrzlaarm6ama
eslint-module-utils: 2.7.4_v73lhamtbyinynmwa5fn7kpmfq
has: 1.0.3
is-core-module: 2.11.0
is-glob: 4.0.3
@@ -9936,7 +10079,7 @@ packages:
- supports-color
dev: true
/eslint-plugin-import/2.27.5_zycheyzypw6s5ouujsf5akzhsy:
/eslint-plugin-import/2.27.5_eslint@8.34.0:
resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==}
engines: {node: '>=4'}
peerDependencies:
@@ -9946,7 +10089,6 @@ packages:
'@typescript-eslint/parser':
optional: true
dependencies:
'@typescript-eslint/parser': 5.48.1_7kw3g6rralp5ps6mg3uyzz6azm
array-includes: 3.1.6
array.prototype.flat: 1.3.1
array.prototype.flatmap: 1.3.1
@@ -9954,7 +10096,7 @@ packages:
doctrine: 2.1.0
eslint: 8.34.0
eslint-import-resolver-node: 0.3.7
eslint-module-utils: 2.7.4_fqawsowff3yxll7f25e3byprdq
eslint-module-utils: 2.7.4_eyqnu5kib2hfrvsonwfdq4ojse
has: 1.0.3
is-core-module: 2.11.0
is-glob: 4.0.3
@@ -10464,6 +10606,21 @@ packages:
signal-exit: 3.0.7
strip-final-newline: 2.0.0
/execa/7.0.0:
resolution: {integrity: sha512-tQbH0pH/8LHTnwTrsKWideqi6rFB/QNUawEwrn+WHyz7PX1Tuz2u7wfTvbaNBdP5JD5LVWxNo8/A8CHNZ3bV6g==}
engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0}
dependencies:
cross-spawn: 7.0.3
get-stream: 6.0.1
human-signals: 4.3.0
is-stream: 3.0.0
merge-stream: 2.0.0
npm-run-path: 5.1.0
onetime: 6.0.0
signal-exit: 3.0.7
strip-final-newline: 3.0.0
dev: false
/executable/4.1.1:
resolution: {integrity: sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==}
engines: {node: '>=4'}
@@ -10727,6 +10884,14 @@ packages:
escape-string-regexp: 1.0.5
dev: true
/figures/5.0.0:
resolution: {integrity: sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==}
engines: {node: '>=14'}
dependencies:
escape-string-regexp: 5.0.0
is-unicode-supported: 1.3.0
dev: false
/file-entry-cache/6.0.1:
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
engines: {node: ^10.12.0 || >=12.0.0}
@@ -10968,6 +11133,15 @@ packages:
universalify: 2.0.0
dev: true
/fs-extra/11.1.0:
resolution: {integrity: sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw==}
engines: {node: '>=14.14'}
dependencies:
graceful-fs: 4.2.10
jsonfile: 6.1.0
universalify: 2.0.0
dev: false
/fs-extra/7.0.1:
resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==}
engines: {node: '>=6 <7 || >=8'}
@@ -11379,6 +11553,14 @@ packages:
/graceful-fs/4.2.10:
resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==}
/gradient-string/2.0.2:
resolution: {integrity: sha512-rEDCuqUQ4tbD78TpzsMtt5OIf0cBCSDWSJtUDaF6JsAh+k0v9r++NzxNEG87oDZx9ZwGhD8DaezR2L/yrw0Jdw==}
engines: {node: '>=10'}
dependencies:
chalk: 4.1.2
tinygradient: 1.1.5
dev: false
/grapheme-splitter/1.0.4:
resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==}
@@ -11639,6 +11821,11 @@ packages:
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
engines: {node: '>=10.17.0'}
/human-signals/4.3.0:
resolution: {integrity: sha512-zyzVyMjpGBX2+6cDVZeFPCdtOtdsxOeseRhB9tkQ6xXmGUNrcnBzdEKPy3VPNYz+4gy1oukVOXcrJCunSyc6QQ==}
engines: {node: '>=14.18.0'}
dev: false
/humanize-duration/3.27.3:
resolution: {integrity: sha512-iimHkHPfIAQ8zCDQLgn08pRqSVioyWvnGfaQ8gond2wf7Jq2jJ+24ykmnRyiz3fIldcn4oUuQXpjqKLhSVR7lw==}
dev: false
@@ -11743,6 +11930,27 @@ packages:
wrap-ansi: 7.0.0
dev: true
/inquirer/9.1.4:
resolution: {integrity: sha512-9hiJxE5gkK/cM2d1mTEnuurGTAoHebbkX0BYl3h7iEg7FYfuNIom+nDfBCSWtvSnoSrWCeBxqqBZu26xdlJlXA==}
engines: {node: '>=12.0.0'}
dependencies:
ansi-escapes: 6.0.0
chalk: 5.2.0
cli-cursor: 4.0.0
cli-width: 4.0.0
external-editor: 3.1.0
figures: 5.0.0
lodash: 4.17.21
mute-stream: 0.0.8
ora: 6.1.2
run-async: 2.4.1
rxjs: 7.8.0
string-width: 5.1.2
strip-ansi: 7.0.1
through: 2.3.8
wrap-ansi: 8.1.0
dev: false
/internal-slot/1.0.4:
resolution: {integrity: sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ==}
engines: {node: '>= 0.4'}
@@ -12004,6 +12212,11 @@ packages:
resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==}
engines: {node: '>=8'}
/is-interactive/2.0.0:
resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==}
engines: {node: '>=12'}
dev: false
/is-invalid-path/0.1.0:
resolution: {integrity: sha512-aZMG0T3F34mTg4eTdszcGXx54oiZ4NtHSft3hWNJMGJXUUqdIj3cOZuHcU0nCWWcY3jd7yRe/3AEm3vSNTpBGQ==}
engines: {node: '>=0.10.0'}
@@ -12119,6 +12332,11 @@ packages:
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
engines: {node: '>=8'}
/is-stream/3.0.0:
resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dev: false
/is-string/1.0.7:
resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==}
engines: {node: '>= 0.4'}
@@ -12156,6 +12374,11 @@ packages:
resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
engines: {node: '>=10'}
/is-unicode-supported/1.3.0:
resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==}
engines: {node: '>=12'}
dev: false
/is-valid-path/0.1.1:
resolution: {integrity: sha512-+kwPrVDu9Ms03L90Qaml+79+6DZHqHyRoANI6IsZJ/g8frhnfchDOBCa0RbQ6/kdHt5CS5OeIEyrYznNuVN+8A==}
engines: {node: '>=0.10.0'}
@@ -12557,7 +12780,6 @@ packages:
universalify: 2.0.0
optionalDependencies:
graceful-fs: 4.2.10
dev: true
/jsonpath-plus/5.1.0:
resolution: {integrity: sha512-890w2Pjtj0iswAxalRlt2kHthi6HKrXEfZcn+ZNZptv7F3rUGIeDuZo+C+h4vXBHLEsVjJrHeCm35nYeZLzSBQ==}
@@ -12850,6 +13072,14 @@ packages:
chalk: 4.1.2
is-unicode-supported: 0.1.0
/log-symbols/5.1.0:
resolution: {integrity: sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==}
engines: {node: '>=12'}
dependencies:
chalk: 5.2.0
is-unicode-supported: 1.3.0
dev: false
/log-update/4.0.0:
resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==}
engines: {node: '>=10'}
@@ -13532,6 +13762,11 @@ packages:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
/mimic-fn/4.0.0:
resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
engines: {node: '>=12'}
dev: false
/mimic-response/1.0.1:
resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==}
engines: {node: '>=4'}
@@ -13725,7 +13960,6 @@ packages:
/mute-stream/0.0.8:
resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==}
dev: true
/mz/2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
@@ -13935,6 +14169,13 @@ packages:
dependencies:
path-key: 3.1.1
/npm-run-path/5.1.0:
resolution: {integrity: sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
path-key: 4.0.0
dev: false
/npmlog/4.1.2:
resolution: {integrity: sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==}
dependencies:
@@ -14117,6 +14358,13 @@ packages:
dependencies:
mimic-fn: 2.1.0
/onetime/6.0.0:
resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
engines: {node: '>=12'}
dependencies:
mimic-fn: 4.0.0
dev: false
/ono/4.0.11:
resolution: {integrity: sha512-jQ31cORBFE6td25deYeD80wxKBMj+zBmHTrVxnc6CKhx8gho6ipmWM5zj/oeoqioZ99yqBls9Z/9Nss7J26G2g==}
dependencies:
@@ -14174,6 +14422,21 @@ packages:
strip-ansi: 6.0.1
wcwidth: 1.0.1
/ora/6.1.2:
resolution: {integrity: sha512-EJQ3NiP5Xo94wJXIzAyOtSb0QEIAUu7m8t6UZ9krbz0vAJqr92JpcK/lEXg91q6B9pEGqrykkd2EQplnifDSBw==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
bl: 5.1.0
chalk: 5.2.0
cli-cursor: 4.0.0
cli-spinners: 2.7.0
is-interactive: 2.0.0
is-unicode-supported: 1.3.0
log-symbols: 5.1.0
strip-ansi: 7.0.1
wcwidth: 1.0.1
dev: false
/os-tmpdir/1.0.2:
resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==}
engines: {node: '>=0.10.0'}
@@ -14429,6 +14692,11 @@ packages:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
/path-key/4.0.0:
resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==}
engines: {node: '>=12'}
dev: false
/path-parse/1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
@@ -14968,6 +15236,15 @@ packages:
shallow-equal: 1.2.1
dev: false
/react-dom/18.2.0:
resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
peerDependencies:
react: ^18.2.0
dependencies:
loose-envify: 1.4.0
scheduler: 0.23.0
dev: false
/react-dom/18.2.0_react@18.2.0:
resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
peerDependencies:
@@ -15550,6 +15827,14 @@ packages:
onetime: 5.1.2
signal-exit: 3.0.7
/restore-cursor/4.0.0:
resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
onetime: 5.1.2
signal-exit: 3.0.7
dev: false
/ret/0.1.15:
resolution: {integrity: sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==}
engines: {node: '>=0.12'}
@@ -15643,7 +15928,6 @@ packages:
/run-async/2.4.1:
resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==}
engines: {node: '>=0.12.0'}
dev: true
/run-exclusive/2.2.18:
resolution: {integrity: sha512-TXr1Gkl1iEAOCCpBTRm/2m0+1KGjORcWpZZ+VGGTe7dYX8E4y8/fMvrHk0zf+kclec2R//tpvdBxgG0bDgaJfw==}
@@ -15660,7 +15944,6 @@ packages:
resolution: {integrity: sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==}
dependencies:
tslib: 2.4.1
dev: true
/sade/1.8.1:
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
@@ -16201,7 +16484,6 @@ packages:
eastasianwidth: 0.2.0
emoji-regex: 9.2.2
strip-ansi: 7.0.1
dev: true
/string.prototype.matchall/4.0.8:
resolution: {integrity: sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==}
@@ -16278,7 +16560,6 @@ packages:
engines: {node: '>=12'}
dependencies:
ansi-regex: 6.0.1
dev: true
/strip-bom/3.0.0:
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
@@ -16288,6 +16569,11 @@ packages:
resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
engines: {node: '>=6'}
/strip-final-newline/3.0.0:
resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==}
engines: {node: '>=12'}
dev: false
/strip-indent/3.0.0:
resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
engines: {node: '>=8'}
@@ -16371,7 +16657,6 @@ packages:
dependencies:
has-flag: 4.0.0
supports-color: 7.2.0
dev: true
/supports-preserve-symlinks-flag/1.0.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
@@ -16518,6 +16803,14 @@ packages:
engines: {node: '>=8'}
dev: false
/terminal-link/3.0.0:
resolution: {integrity: sha512-flFL3m4wuixmf6IfhFJd1YPiLiMuxEc8uHRM1buzIeZPm22Au2pDqBJQgdo7n1WfPU1ONFGv7YDwpFBmHGF6lg==}
engines: {node: '>=12'}
dependencies:
ansi-escapes: 5.0.0
supports-hyperlinks: 2.3.0
dev: false
/test-exclude/6.0.0:
resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}
engines: {node: '>=8'}
@@ -16565,7 +16858,6 @@ packages:
/through/2.3.8:
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
dev: true
/through2/2.0.5:
resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==}
@@ -16595,6 +16887,17 @@ packages:
resolution: {integrity: sha512-hGYWYBMPr7p4g5IarQE7XhlyWveh1EKhy4wUBS1LrHXCKYgvz+4/jCqgmJqZxxldesn05vccrtME2RLLZNW7iA==}
dev: true
/tinycolor2/1.6.0:
resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==}
dev: false
/tinygradient/1.1.5:
resolution: {integrity: sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw==}
dependencies:
'@types/tinycolor2': 1.4.3
tinycolor2: 1.6.0
dev: false
/tinypool/0.3.0:
resolution: {integrity: sha512-NX5KeqHOBZU6Bc0xj9Vr5Szbb1j8tUHIeD18s41aDJaPeC5QTdEhK0SpdpUrZlj2nv5cctNcSjaKNanXlfcVEQ==}
engines: {node: '>=14.0.0'}
@@ -17102,10 +17405,19 @@ packages:
engines: {node: '>=8'}
dev: false
/type-fest/1.4.0:
resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==}
engines: {node: '>=10'}
dev: false
/type-fest/2.19.0:
resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==}
engines: {node: '>=12.20'}
/type-fest/3.6.0:
resolution: {integrity: sha512-RqTRtKTzvPpNdDUp1dVkKQRunlPITk4mXeqFlAZoJsS+fLRn8AdPK0TcQDumGayhU7fjlBfiBjsq3pe3rIfXZQ==}
engines: {node: '>=14.16'}
/type-is/1.6.18:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'}
@@ -17299,7 +17611,6 @@ packages:
/universalify/2.0.0:
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
engines: {node: '>= 10.0.0'}
dev: true
/unload/2.2.0:
resolution: {integrity: sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==}
@@ -17997,6 +18308,15 @@ packages:
string-width: 4.2.3
strip-ansi: 6.0.1
/wrap-ansi/8.1.0:
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
engines: {node: '>=12'}
dependencies:
ansi-styles: 6.2.1
string-width: 5.1.2
strip-ansi: 7.0.1
dev: false
/wrappy/1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}