Adding the @trigger.dev/init CLI package

This commit is contained in:
Eric Allam
2023-06-23 11:40:26 +01:00
parent 3c2f1709cc
commit 28914b878c
29 changed files with 751 additions and 683 deletions
+11
View File
@@ -0,0 +1,11 @@
---
"@trigger.dev/integration-kit": patch
"@trigger.dev/sdk": patch
"@trigger.dev/github": patch
"@trigger.dev/openai": patch
"@trigger.dev/slack": patch
"@trigger.dev/nextjs": patch
"@trigger.dev/init": patch
---
Creating the init CLI package
@@ -36,6 +36,11 @@ export async function action({ request }: ActionArgs) {
return json({ error: "Invalid request body" }, { status: 400 });
}
logger.info("Creating endpoint", {
url: request.url,
endpoint: body.data,
});
const service = new CreateEndpointService();
try {
-77
View File
@@ -1,77 +0,0 @@
## ✨ 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
npx 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
npx 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)
-311
View File
@@ -1,311 +0,0 @@
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)}`;
};
-71
View File
@@ -1,71 +0,0 @@
#!/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);
});
@@ -1,46 +0,0 @@
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`
);
}
}
}
@@ -1,38 +0,0 @@
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;
};
@@ -1,80 +0,0 @@
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),
});
}
+25
View File
@@ -0,0 +1,25 @@
## ✨ @trigger.dev/init - Initialize your Next.js project to start using Trigger.dev
Trigger.dev is an open source platform that makes it easy to create event-driven background tasks directly your Next.js project.
## 💻 Usage
To initialize your Next.js project using `@trigger.dev/init`, run any of the following three commands and answer the prompts:
### npm
```sh
npx @trigger.dev/init@latest
```
### yarn
```sh
yarn @trigger.dev/init@latest
```
### pnpm
```sh
pnpm dlx @trigger.dev/init@latest
```
@@ -1,14 +1,14 @@
{
"name": "create-trigger",
"name": "@trigger.dev/init",
"version": "0.2.0",
"description": "The Trigger.dev CLI to easily create and manage a Trigger.dev project",
"description": "The CLI to easily initialize Trigger.dev in your Next.js 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"
"directory": "packages/init-trigger"
},
"publishConfig": {
"access": "public"
@@ -21,7 +21,10 @@
"events",
"webhooks",
"integrations",
"apis"
"apis",
"jobs",
"background-jobs",
"nextjs"
],
"files": [
"dist"
@@ -29,7 +32,7 @@
"type": "module",
"exports": "./dist/index.js",
"bin": {
"create-trigger": "./dist/index.js"
"init": "./dist/index.js"
},
"devDependencies": {
"@types/fs-extra": "^11.0.1",
@@ -60,9 +63,10 @@
"inquirer": "^9.1.4",
"node-fetch": "^3.3.0",
"ora": "^6.1.2",
"simple-git": "^3.19.0",
"terminal-link": "^3.0.0"
},
"engines": {
"node": ">=16"
"node": ">=18"
}
}
+157
View File
@@ -0,0 +1,157 @@
import { Command } from "commander";
import inquirer from "inquirer";
import { COMMAND_NAME } from "../consts.js";
import { getVersion } from "../utils/getVersion.js";
import { logger } from "../utils/logger.js";
export interface CliFlags {
projectPath: string;
triggerUrl: string;
endpointUrl: string;
endpointSlug: string;
apiKey?: string;
}
export interface CliResults {
flags: CliFlags;
}
const defaultOptions: CliResults = {
flags: {
projectPath: ".",
triggerUrl: "https://cloud.trigger.dev",
endpointUrl: "http://localhost:3000",
endpointSlug: "my-nextjs-project",
},
};
export const parseCliOptions = async () => {
const cliResults = defaultOptions;
const program = new Command().name(COMMAND_NAME);
program
.description("A CLI for initializing Trigger.dev in your Next.js")
.option(
"-p, --project-path <project-path>",
"The path to the Next.js project",
"."
)
.option(
"-k, --api-key <api-key>",
"The development API key to use for the project.",
false
)
.option(
"-e, --endpoint-slug <endpoint-slug>",
"The unique slug for the endpoint to use for this project. (e.g. my-nextjs-project)",
false
)
.option(
"-u, --endpoint-url <endpoint-url>",
"The URL of your local Next.js project. (e.g. http://localhost:3000). NOTE: Must be a publicly accessible URL if you are using a deployed Trigger.dev instance",
false
)
.option(
"-t, --trigger-url <trigger-url>",
"The URL of the Trigger.dev instance to use. (e.g. https://cloud.trigger.dev)",
false
)
.version(getVersion(), "-v, --version", "Display the version number")
.parse(process.argv);
cliResults.flags = program.opts();
return cliResults;
};
export const runCliPrompts = async (cliResults: CliResults) => {
try {
if (!cliResults.flags.apiKey) {
cliResults.flags.apiKey = await promptApiKey();
}
if (!cliResults.flags.endpointSlug) {
cliResults.flags.endpointSlug = await promptEndpointSlug();
}
} catch (err) {
// If the user is not calling the command 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(
`${COMMAND_NAME} needs an interactive terminal to provide options`
);
const { shouldContinue } = await inquirer.prompt<{
shouldContinue: boolean;
}>({
name: "shouldContinue",
type: "confirm",
message: `Continue initializing your trigger.dev project?`,
default: true,
});
if (!shouldContinue) {
logger.info("Exiting...");
process.exit(0);
}
} else {
throw err;
}
}
return cliResults;
};
const promptApiKey = async (): Promise<string> => {
// 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 }>({
type: "input",
name: "apiKey",
message: "Enter your development API key (required)",
validate: (input) => {
// Make sure they enter something like tr_dev_********
if (!input) {
return "Please enter your development API key";
}
if (!input.startsWith("tr_dev_")) {
return "Please enter a valid development API key or leave blank to skip (should start with tr_dev_)";
}
return true;
},
});
return apiKey;
};
const promptEndpointSlug = async (): Promise<string> => {
const { endpointSlug } = await inquirer.prompt<{
endpointSlug: string;
}>({
type: "input",
name: "endpointSlug",
message: "Enter a unique slug for your endpoint (required)",
validate: (input) => {
if (!input) {
return "Please enter a unique slug for your endpoint";
}
return true;
},
});
return endpointSlug;
};
export const obfuscateApiKey = (apiKey: string) => {
const [prefix, slug, secretPart] = apiKey.split("_") as [
string,
string,
string
];
return `${prefix}_${slug}_${"*".repeat(secretPart.length)}`;
};
@@ -16,7 +16,7 @@ export const TITLE_TEXT = `
`;
export const DEFAULT_APP_NAME = "my-triggers";
export const CREATE_TRIGGER = "create-trigger";
export const COMMAND_NAME = "@trigger.dev/init";
export const TEMPLATE_ORGANIZATION = "triggerdotdev";
export const TRIGGER_BASE_URL =
process.env.TRIGGER_BASE_URL ?? "https://app.trigger.dev";
process.env.TRIGGER_BASE_URL ?? "https://cloud.trigger.dev";
+332
View File
@@ -0,0 +1,332 @@
#!/usr/bin/env node
import { CliResults, parseCliOptions, runCliPrompts } from "./cli/index.js";
import { addDependencies } from "./utils/addDependencies.js";
import { logger } from "./utils/logger.js";
import { resolvePath } from "./utils/parseNameAndPath.js";
import { renderTitle } from "./utils/renderTitle.js";
import fs from "fs/promises";
import pathModule from "path";
import { simpleGit } from "simple-git";
import { TriggerApi } from "./utils/triggerApi.js";
const main = async () => {
renderTitle();
const cliOptions = await parseCliOptions();
const resolvedPath = resolvePath(cliOptions.flags.projectPath);
// Detect if are are in a Next.js project
const isNextJsProject = await detectNextJsProject(resolvedPath);
if (!isNextJsProject) {
logger.error("You must run this command in a Next.js project.");
process.exit(1);
} else {
logger.success("✅ Detected Next.js project");
}
const hasGitChanges = await detectGitChanges(resolvedPath);
if (hasGitChanges) {
// Warn the user that they have git changes
logger.warn(
"⚠️ You have uncommitted git changes, you may want to commit them before continuing."
);
}
const isTypescriptProject = await detectTypescriptProject(resolvedPath);
if (!isTypescriptProject) {
// Exit with an error message
logger.error(
"You must be using TypeScript in your Next.js project to use Trigger.dev."
);
process.exit(1);
}
const cliResults = await runCliPrompts(cliOptions);
const apiKey = cliResults.flags.apiKey;
if (!apiKey) {
logger.error("You must provide an API key to continue.");
process.exit(1);
}
await addDependencies(resolvedPath, [
{ name: "@trigger.dev/sdk", version: "next" },
{ name: "@trigger.dev/nextjs", version: "next" },
]);
// Setup environment variables
const addedEnvVars = await setupEnvironmentVariables(
resolvedPath,
cliResults
);
logger.success(`✅ Setup environment variables ${addedEnvVars.join(", ")}`);
const nextJsDir = await detectPagesOrAppDir(resolvedPath);
if (nextJsDir === "pages") {
await createTriggerPageRoute(resolvedPath, cliResults);
} else {
await createTriggerAppRoute(resolvedPath, cliResults);
}
const api = new TriggerApi(apiKey, cliResults.flags.triggerUrl);
const endpoint = await api.createEndpoint({
id: cliResults.flags.endpointSlug,
url: `${cliResults.flags.endpointUrl}${
cliResults.flags.endpointUrl.endsWith("/") ? "" : "/"
}api/trigger`,
});
if (!endpoint) {
logger.error(
"Unable to create endpoint, please contact eric@trigger.dev for assistance."
);
process.exit(1);
}
logger.success(`✅ Successfully initialized Trigger.dev!`);
logger.info(
`🔗 Visit your Trigger.dev dashboard at ${cliResults.flags.triggerUrl}`
);
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);
});
// Detects if the project is a Next.js project at path
async function detectNextJsProject(path: string): Promise<boolean> {
// Checks for the presence of a next.config.js file
try {
// Check if next.config.js file exists in the given path
await fs.access(pathModule.join(path, "next.config.js"));
return true;
} catch (error) {
// If next.config.js file doesn't exist, it's not a Next.js project
return false;
}
}
// Detects if there are any uncommitted git changes at path
async function detectGitChanges(path: string): Promise<boolean> {
const git = simpleGit(path);
const status = await git.status();
return status.files.length > 0;
}
async function detectTypescriptProject(path: string): Promise<boolean> {
// Checks for the presence of a tsconfig.json file
try {
await fs.access(pathModule.join(path, "tsconfig.json"));
return true;
} catch (error) {
return false;
}
}
// Detect the use of pages or app dir in the Next.js project
// Import the next.config.js file and check for experimental: { appDir: true }
async function detectPagesOrAppDir(path: string): Promise<"pages" | "app"> {
const nextConfigPath = pathModule.join(path, "next.config.js");
const importedConfig = await import(nextConfigPath);
if (importedConfig?.default?.experimental?.appDir) {
return "app";
} else {
return "pages";
}
}
async function createTriggerPageRoute(path: string, cliResults: CliResults) {
const routeContent = `
import { Job, TriggerClient, eventTrigger } from "@trigger.dev/sdk";
import { createPagesRoute } from "@trigger.dev/nextjs";
const { handler, config } = createPagesRoute(client, { path: "/api/trigger" });
export { config };
const client = new TriggerClient({
id: "${cliResults.flags.endpointSlug}",
url: process.env.VERCEL_URL,
apiKey: process.env.TRIGGER_API_KEY,
apiUrl: process.env.TRIGGER_API_URL,
});
new Job(client, {
id: "example-job",
name: "Example Job",
version: "0.0.1",
trigger: eventTrigger({
name: "example.event",
}),
run: async (payload, io, ctx) => {
await io.logger.info("Hello world!", { payload });
return {
message: "Hello world!",
};
},
});
export default handler;
`;
const directories = pathModule.join(path, "pages", "api");
await fs.mkdir(directories, { recursive: true });
// Don't overwrite the file if it already exists
const exists = await pathExists(pathModule.join(directories, "trigger.ts"));
if (exists) {
logger.info("Skipping creation of pages route because it already exists");
return;
}
await fs.writeFile(pathModule.join(directories, "trigger.ts"), routeContent);
logger.success("✅ Create pages route at /pages/api/trigger.ts");
}
async function createTriggerAppRoute(path: string, cliResults: CliResults) {
const routeContent = `
import { Job, TriggerClient, eventTrigger } from "@trigger.dev/sdk";
import { createAppRoute } from "@trigger.dev/nextjs";
const client = new TriggerClient({
id: "${cliResults.flags.endpointSlug}",
url: process.env.VERCEL_URL,
apiKey: process.env.TRIGGER_API_KEY,
apiUrl: process.env.TRIGGER_API_URL,
});
new Job(client, {
id: "example-job",
name: "Example Job",
version: "0.0.1",
trigger: eventTrigger({
name: "example.event",
}),
run: async (payload, io, ctx) => {
await io.logger.info("Hello world!", { payload });
return {
message: "Hello world!",
};
},
});
export const { POST, dynamic } = createAppRoute(client, {
path: "/api/trigger",
});
`;
const directories = pathModule.join(path, "app", "api", "trigger");
await fs.mkdir(directories, { recursive: true });
const fileExists = await pathExists(pathModule.join(directories, "route.ts"));
if (fileExists) {
logger.info("Skipping creation of app route because it already exists");
return;
}
await fs.writeFile(pathModule.join(directories, "route.ts"), routeContent);
logger.success("✅ Create app route at /app/api/trigger/route.ts");
}
type EnvironmentVariable = "TRIGGER_API_KEY" | "TRIGGER_API_URL" | "VERCEL_URL";
async function setupEnvironmentVariables(
path: string,
cliResults: CliResults
): Promise<Array<EnvironmentVariable>> {
let results: Array<EnvironmentVariable> = [];
const envFilePath = pathModule.join(path, ".env.local");
const envFileExists = await pathExists(envFilePath);
if (envFileExists) {
const envFileContent = await fs.readFile(envFilePath, "utf-8");
if (envFileContent.includes("TRIGGER_API_KEY")) {
// Update the existing value
const updatedEnvFileContent = envFileContent.replace(
/TRIGGER_API_KEY=.*/g,
`TRIGGER_API_KEY=${cliResults.flags.apiKey}`
);
await fs.writeFile(envFilePath, updatedEnvFileContent);
results.push("TRIGGER_API_KEY");
} else {
await fs.appendFile(
envFilePath,
`TRIGGER_API_KEY=${cliResults.flags.apiKey}\n`
);
results.push("TRIGGER_API_KEY");
}
if (envFileContent.includes("TRIGGER_API_URL")) {
// Update existing value
const updatedEnvFileContent = envFileContent.replace(
/TRIGGER_API_URL=.*/g,
`TRIGGER_API_URL=${cliResults.flags.triggerUrl}`
);
await fs.writeFile(envFilePath, updatedEnvFileContent);
results.push("TRIGGER_API_URL");
} else {
await fs.appendFile(
envFilePath,
`TRIGGER_API_URL=${cliResults.flags.triggerUrl}\n`
);
results.push("TRIGGER_API_URL");
}
if (!envFileContent.includes("VERCEL_URL")) {
await fs.appendFile(
envFilePath,
`VERCEL_URL=${cliResults.flags.endpointUrl}\n`
);
results.push("VERCEL_URL");
}
} else {
const envFileContent = `
TRIGGER_API_KEY=${cliResults.flags.apiKey}
TRIGGER_API_URL=${cliResults.flags.triggerUrl}
VERCEL_URL=${cliResults.flags.endpointUrl}
`;
await fs.writeFile(envFilePath, envFileContent);
results = ["TRIGGER_API_KEY", "TRIGGER_API_URL", "VERCEL_URL"];
}
return results;
}
async function pathExists(path: string): Promise<boolean> {
try {
await fs.access(path);
return true;
} catch (error) {
return false;
}
}
@@ -1,23 +1,54 @@
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";
import ora, { type Ora } from "ora";
import pathModule from "path";
import { getUserPkgManager, type PackageManager } from "./getUserPkgManager.js";
import fs from "fs/promises";
export async function installDependencies(projectDir: string) {
logger.info("Installing dependencies...");
export type InstallPackage = {
name: string;
version: string;
};
export async function addDependencies(
projectDir: string,
packages: Array<InstallPackage>
) {
const pkgManager = getUserPkgManager();
await addDependenciesToPackageJson(projectDir, packages);
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")
chalk.green(
`Successfully installed ${packages
.map((pkg) => `${pkg.name}@${pkg.version}`)
.join(", ")}}`
)
);
}
async function addDependenciesToPackageJson(
projectDir: string,
packages: Array<InstallPackage>
) {
const pkgJsonPath = pathModule.join(projectDir, "package.json");
const pkgBuffer = await fs.readFile(pkgJsonPath);
const pkgJson = JSON.parse(pkgBuffer.toString());
// Add the dependencies to the package.json file
pkgJson.dependencies = {
...pkgJson.dependencies,
...Object.fromEntries(packages.map((pkg) => [pkg.name, pkg.version])),
};
// Write the updated package.json file
await fs.writeFile(pkgJsonPath, JSON.stringify(pkgJson, null, 2));
}
async function runInstallCommand(
pkgManager: PackageManager,
projectDir: string
@@ -0,0 +1,6 @@
import pathModule from "path";
// Takes a relative path (like .) and resolves it to a full path (like /Users/username/Projects/my-triggers)
export const resolvePath = (input: string) => {
return pathModule.resolve(process.cwd(), input);
};
@@ -0,0 +1,41 @@
import fetch from "node-fetch";
export type CreateEndpointOptions = {
id: string;
url: string;
};
export type EndpointResponse = {
id: string;
slug: string;
url: string;
environmentId: string;
organizationId: string;
projectId: string;
createdAt: string;
updatedAt: string;
indexingHookIdentifier: string;
};
export class TriggerApi {
constructor(private apiKey: string, private baseUrl: string) {}
async createEndpoint(
options: CreateEndpointOptions
): Promise<EndpointResponse | undefined> {
const response = await fetch(`${this.baseUrl}/api/v1/endpoints`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.apiKey}`,
},
body: JSON.stringify(options),
});
if (response.ok) {
return response.json() as Promise<EndpointResponse>;
}
return;
}
}
+125 -46
View File
@@ -533,6 +533,9 @@ importers:
'@radix-ui/react-popover': ^1.0.5
'@radix-ui/react-slot': ^1.0.2
'@trigger.dev/companyicons': ^1.5.9
'@trigger.dev/init': workspace:*
'@trigger.dev/nextjs': next
'@trigger.dev/sdk': next
'@types/node': 20.3.1
'@types/react': ^18.0.21
'@types/react-dom': ^18.0.6
@@ -557,6 +560,8 @@ importers:
'@radix-ui/react-popover': 1.0.5_ib3m5ricvtkl2cll7qpr2f6lvq
'@radix-ui/react-slot': 1.0.2_kzbn2opkn2327fwg5yzwzya5o4
'@trigger.dev/companyicons': 1.5.9_biqbaboplfbrettd7655fr4n2y
'@trigger.dev/nextjs': 1.0.0-next.0_ro477i2ikctu7zbnxdidq5gjqe
'@trigger.dev/sdk': 2.0.0-next.1
'@types/node': 20.3.1
'@types/react': 18.0.26
'@types/react-dom': 18.0.10
@@ -575,6 +580,8 @@ importers:
tailwindcss: 3.3.2
tailwindcss-animate: 1.0.5_tailwindcss@3.3.2
typescript: 5.1.3
devDependencies:
'@trigger.dev/init': link:../../packages/init-trigger
integrations/github:
specifiers:
@@ -647,51 +654,6 @@ importers:
rimraf: 3.0.2
tsup: 6.6.3
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/database:
specifiers:
'@prisma/client': ^4.16.0
@@ -750,6 +712,53 @@ importers:
'@types/react': 18.0.26
typescript: 4.9.5
packages/init-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
simple-git: ^3.19.0
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
simple-git: 3.19.0
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/integration-kit:
specifiers:
'@trigger.dev/tsconfig': workspace:*
@@ -6903,6 +6912,18 @@ packages:
- supports-color
dev: false
/@kwsites/file-exists/1.1.1:
resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==}
dependencies:
debug: 4.3.4
transitivePeerDependencies:
- supports-color
dev: false
/@kwsites/promise-deferred/1.1.1:
resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==}
dev: false
/@lezer/common/1.0.2:
resolution: {integrity: sha512-SVgiGtMnMnW3ActR8SXgsDhw7a0w0ChHSYAyAUxxrOiJ1OqYWEKk/xJd84tTSPo1mo6DXLObAJALNnd0Hrv7Ng==}
dev: false
@@ -10558,6 +10579,46 @@ packages:
react-dom: 18.2.0_react@18.2.0
dev: false
/@trigger.dev/nextjs/1.0.0-next.0_ro477i2ikctu7zbnxdidq5gjqe:
resolution: {integrity: sha512-mIOuGNK2os0tqAPzmixayhBDZZ15xWfr60rP2jIHs2ARIdLVARvqQS1NvTs+WhfzgevFEKDuwXliv1LVi0JDEQ==}
engines: {node: '>=18'}
peerDependencies:
'@trigger.dev/sdk': ^2.0.0-next.0
next: 13.3.1
dependencies:
'@trigger.dev/sdk': 2.0.0-next.1
debug: 4.3.4
next: 13.3.1_biqbaboplfbrettd7655fr4n2y
transitivePeerDependencies:
- supports-color
dev: false
/@trigger.dev/sdk/2.0.0-next.1:
resolution: {integrity: sha512-MA7Z582x1fDaFpcinZic9Rh6V74Xv6R4yHwiH4u9VrLmsiaQfwnwK9LWXoR/ZhcwEwsCt+VG0Tce4pDmGUN8HA==}
engines: {node: '>=18'}
dependencies:
chalk: 5.2.0
debug: 4.3.4
evt: 2.4.13
get-caller-file: 2.0.5
git-remote-origin-url: 4.0.0
git-repo-info: 2.1.1
node-fetch: 2.6.11
slug: 6.1.0
terminal-link: 3.0.0
ulid: 2.3.0
uuid: 9.0.0
ws: 8.12.0
zod: 3.21.4
zod-error: 1.1.0
zod-to-json-schema: 3.20.2_zod@3.21.4
transitivePeerDependencies:
- bufferutil
- encoding
- supports-color
- utf-8-validate
dev: false
/@tsconfig/node10/1.0.9:
resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==}
@@ -10954,7 +11015,7 @@ packages:
/@types/node-fetch/2.6.2:
resolution: {integrity: sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==}
dependencies:
'@types/node': 18.15.13
'@types/node': 20.3.1
form-data: 3.0.1
dev: true
@@ -22411,6 +22472,16 @@ packages:
/signal-exit/3.0.7:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
/simple-git/3.19.0:
resolution: {integrity: sha512-hyH2p9Ptxjf/xPuL7HfXbpYt9gKhC1yWDh3KYIAYJJePAKV7AEjLN4xhp7lozOdNiaJ9jlVvAbBymVlcS2jRiA==}
dependencies:
'@kwsites/file-exists': 1.1.1
'@kwsites/promise-deferred': 1.1.1
debug: 4.3.4
transitivePeerDependencies:
- supports-color
dev: false
/simple-oauth2/5.0.0:
resolution: {integrity: sha512-8291lo/z5ZdpmiOFzOs1kF3cxn22bMj5FFH+DNUppLJrpoIlM1QnFiE7KpshHu3J3i21TVcx4yW+gXYjdCKDLQ==}
dependencies:
@@ -25280,6 +25351,14 @@ packages:
zod: 3.20.2
dev: false
/zod-to-json-schema/3.20.2_zod@3.21.4:
resolution: {integrity: sha512-qka3UAXmVXD8z5SHkRU89UyHp8JHJW7zc8RQCpt7QloJOn6uijwkjUm8o+M/cF1IysmKc5gxac/QeDikaQMdzQ==}
peerDependencies:
zod: ^3.20.0
dependencies:
zod: 3.21.4
dev: false
/zod/3.20.2:
resolution: {integrity: sha512-1MzNQdAvO+54H+EaK5YpyEy0T+Ejo/7YLHS93G3RnYWh5gaotGHwGeN/ZO687qEDU2y4CdStQYXVHIgrUl5UVQ==}
dev: false