v3: misc CLI improvements (#1173)
* prevent downgrades during update check * detect bun and use npm instead * detect missing tsconfig during init and print helpful error * add changeset * add links to dev worker started message * allow users to add custom pkg manager args during init * update changeset * fix links in unsupported terminals * deprecate terminalLink * update changeset
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
- Prevent downgrades during update check and advise to upgrade CLI
|
||||
- Detect bun and use npm instead
|
||||
- During init, fail early and advise if not a TypeScript project
|
||||
- During init, allow specifying custom package manager args
|
||||
- Add links to dev worker started message
|
||||
- Fix links in unsupported terminals
|
||||
@@ -84,7 +84,9 @@ export function createPkApiKeyForEnv(envType: RuntimeEnvironment["type"]) {
|
||||
return `pk_${envSlug(envType)}_${apiKeyId(20)}`;
|
||||
}
|
||||
|
||||
export function envSlug(environmentType: RuntimeEnvironment["type"]) {
|
||||
export type EnvSlug = "dev" | "stg" | "prod" | "prev";
|
||||
|
||||
export function envSlug(environmentType: RuntimeEnvironment["type"]): EnvSlug {
|
||||
switch (environmentType) {
|
||||
case "DEVELOPMENT": {
|
||||
return "dev";
|
||||
@@ -100,3 +102,7 @@ export function envSlug(environmentType: RuntimeEnvironment["type"]) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isEnvSlug(maybeSlug: string): maybeSlug is EnvSlug {
|
||||
return ["dev", "stg", "prod", "prev"].includes(maybeSlug);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { LoaderFunctionArgs, redirect } from "@remix-run/server-runtime";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import { EnvSlug, isEnvSlug } from "~/models/api-key.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
projectRef: z.string(),
|
||||
});
|
||||
|
||||
export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
const { projectRef } = ParamsSchema.parse(params);
|
||||
|
||||
const project = await prisma.project.findFirst({
|
||||
where: {
|
||||
externalRef: projectRef,
|
||||
organization: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
organization: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
return new Response("Project not found", { status: 404 });
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
const envSlug = url.searchParams.get("envSlug");
|
||||
|
||||
// Get the environment from the slug
|
||||
if (envSlug && isEnvSlug(envSlug)) {
|
||||
const env = await getEnvFromSlug(project.id, userId, envSlug);
|
||||
|
||||
if (env) {
|
||||
url.searchParams.set("environments", env.id);
|
||||
}
|
||||
|
||||
url.searchParams.delete("envSlug");
|
||||
}
|
||||
|
||||
return redirect(
|
||||
`/orgs/${project.organization.slug}/projects/v3/${project.slug}/runs${url.search}`
|
||||
);
|
||||
}
|
||||
|
||||
async function getEnvFromSlug(projectId: string, userId: string, envSlug: EnvSlug) {
|
||||
if (envSlug === "dev") {
|
||||
return await prisma.runtimeEnvironment.findFirst({
|
||||
where: {
|
||||
projectId,
|
||||
slug: envSlug,
|
||||
orgMember: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return await prisma.runtimeEnvironment.findFirst({
|
||||
where: {
|
||||
projectId,
|
||||
slug: envSlug,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { LoaderFunctionArgs, redirect } from "@remix-run/server-runtime";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
projectRef: z.string(),
|
||||
});
|
||||
|
||||
export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
const validatedParams = ParamsSchema.parse(params);
|
||||
|
||||
const project = await prisma.project.findFirst({
|
||||
where: {
|
||||
externalRef: validatedParams.projectRef,
|
||||
organization: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
organization: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
return new Response("Not found", { status: 404 });
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
|
||||
return redirect(
|
||||
`/orgs/${project.organization.slug}/projects/v3/${project.slug}/test${url.search}`
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
import { log } from "@clack/prompts";
|
||||
import { Metafile } from "esbuild";
|
||||
import { join } from "node:path";
|
||||
import terminalLink from "terminal-link";
|
||||
|
||||
import { SkipLoggingError } from "../src/cli/common.js";
|
||||
import {
|
||||
@@ -16,6 +15,7 @@ import { writeJSONFile } from "../src/utilities/fileSystem.js";
|
||||
import { PackageManager } from "../src/utilities/getUserPackageManager.js";
|
||||
import { JavascriptProject } from "../src/utilities/javascriptProject.js";
|
||||
import { logger } from "../src/utilities/logger.js";
|
||||
import { cliLink } from "../src/utilities/cliOutput.js";
|
||||
|
||||
type HandleDependenciesOptions = {
|
||||
entryPointMetaOutput: Metafile["outputs"]["out/stdin.js"];
|
||||
@@ -81,7 +81,7 @@ export async function handleDependencies(options: HandleDependenciesOptions) {
|
||||
log.warn(
|
||||
`No additionalFiles matches for:\n\n${copyResult.noMatches
|
||||
.map((glob) => `- "${glob}"`)
|
||||
.join("\n")}\n\nIf this is unexpected you should check your ${terminalLink(
|
||||
.join("\n")}\n\nIf this is unexpected you should check your ${cliLink(
|
||||
"glob patterns",
|
||||
"https://github.com/isaacs/node-glob?tab=readme-ov-file#glob-primer"
|
||||
)} are valid.`
|
||||
|
||||
@@ -16,7 +16,6 @@ import { readFileSync } from "node:fs";
|
||||
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, join, posix, relative, resolve } from "node:path";
|
||||
import { setTimeout } from "node:timers/promises";
|
||||
import terminalLink from "terminal-link";
|
||||
import invariant from "tiny-invariant";
|
||||
import { z } from "zod";
|
||||
import * as packageJson from "../../package.json";
|
||||
@@ -50,7 +49,7 @@ import {
|
||||
mockServerOnlyPlugin,
|
||||
workerSetupImportConfigPlugin,
|
||||
} from "../utilities/build";
|
||||
import { chalkError, chalkPurple, chalkWarning } from "../utilities/cliOutput";
|
||||
import { chalkError, chalkPurple, chalkWarning, cliLink } from "../utilities/cliOutput";
|
||||
import {
|
||||
logESMRequireError,
|
||||
logTaskMetadataParseError,
|
||||
@@ -437,7 +436,7 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) {
|
||||
throw new SkipLoggingError(`Deployment failed to complete: ${finishedDeployment}`);
|
||||
}
|
||||
|
||||
const deploymentLink = terminalLink(
|
||||
const deploymentLink = cliLink(
|
||||
"View deployment",
|
||||
`${authorization.dashboardUrl}/projects/v3/${resolvedConfig.config.project}/deployments/${finishedDeployment.shortCode}`
|
||||
);
|
||||
@@ -576,7 +575,7 @@ function checkLogsForWarnings(logs: string): WarningsCheckReturn {
|
||||
const warnings: LogParserOptions = [
|
||||
{
|
||||
regex: /prisma:warn We could not find your Prisma schema/,
|
||||
message: `Prisma generate failed to find the default schema. Did you include it in config.additionalFiles? ${terminalLink(
|
||||
message: `Prisma generate failed to find the default schema. Did you include it in config.additionalFiles? ${cliLink(
|
||||
"Config docs",
|
||||
docs.config.prisma
|
||||
)}\nCustom schema paths require a postinstall script like this: \`prisma generate --schema=./custom/path/to/schema.prisma\``,
|
||||
@@ -626,17 +625,17 @@ function checkLogsForErrors(logs: string) {
|
||||
const errors: LogParserOptions = [
|
||||
{
|
||||
regex: /Error: Provided --schema at (?<schema>.*) doesn't exist/,
|
||||
message: `Prisma generate failed to find the specified schema at "$schema".\nDid you include it in config.additionalFiles? ${terminalLink(
|
||||
message: `Prisma generate failed to find the specified schema at "$schema".\nDid you include it in config.additionalFiles? ${cliLink(
|
||||
"Config docs",
|
||||
docs.config.prisma
|
||||
)}`,
|
||||
},
|
||||
{
|
||||
regex: /sh: 1: (?<packageOrBinary>.*): not found/,
|
||||
message: `$packageOrBinary not found\n\nIf it's a package: Include it in ${terminalLink(
|
||||
message: `$packageOrBinary not found\n\nIf it's a package: Include it in ${cliLink(
|
||||
"config.additionalPackages",
|
||||
docs.config.prisma
|
||||
)}\nIf it's a binary: Please ${terminalLink(
|
||||
)}\nIf it's a binary: Please ${cliLink(
|
||||
"get in touch",
|
||||
getInTouch
|
||||
)} and we'll see what we can do!`,
|
||||
@@ -1341,7 +1340,7 @@ async function compileProject(
|
||||
log.warn(
|
||||
`No additionalFiles matches for:\n\n${copyResult.noMatches
|
||||
.map((glob) => `- "${glob}"`)
|
||||
.join("\n")}\n\nIf this is unexpected you should check your ${terminalLink(
|
||||
.join("\n")}\n\nIf this is unexpected you should check your ${cliLink(
|
||||
"glob patterns",
|
||||
"https://github.com/isaacs/node-glob?tab=readme-ov-file#glob-primer"
|
||||
)} are valid.`
|
||||
|
||||
@@ -29,7 +29,15 @@ import {
|
||||
mockServerOnlyPlugin,
|
||||
workerSetupImportConfigPlugin,
|
||||
} from "../utilities/build";
|
||||
import { chalkError, chalkGrey, chalkPurple, chalkTask, chalkWorker } from "../utilities/cliOutput";
|
||||
import {
|
||||
chalkError,
|
||||
chalkGrey,
|
||||
chalkLink,
|
||||
chalkPurple,
|
||||
chalkTask,
|
||||
chalkWorker,
|
||||
cliLink,
|
||||
} from "../utilities/cliOutput";
|
||||
import { readConfig } from "../utilities/configFiles";
|
||||
import { readJSONFile } from "../utilities/fileSystem";
|
||||
import { printDevBanner, printStandloneInitialBanner } from "../utilities/initialBanner.js";
|
||||
@@ -624,13 +632,23 @@ function useDev({
|
||||
}
|
||||
|
||||
backgroundWorker.metadata = backgroundWorkerRecord.data;
|
||||
backgroundWorker;
|
||||
|
||||
const testUrl = `${dashboardUrl}/projects/v3/${config.project}/test?environment=dev`;
|
||||
const runsUrl = `${dashboardUrl}/projects/v3/${config.project}/runs?envSlug=dev`;
|
||||
|
||||
const pipe = chalkGrey("|");
|
||||
const bullet = chalkGrey("○");
|
||||
const arrow = chalkGrey("->");
|
||||
|
||||
const testLink = chalkLink(cliLink("Test tasks", testUrl));
|
||||
const runsLink = chalkLink(cliLink("View runs", runsUrl));
|
||||
|
||||
const workerStarted = chalkGrey("Background worker started");
|
||||
const workerVersion = chalkWorker(backgroundWorkerRecord.data.version);
|
||||
|
||||
logger.log(
|
||||
`${chalkGrey(
|
||||
`○ Background worker started -> ${chalkWorker(
|
||||
backgroundWorkerRecord.data.version
|
||||
)}`
|
||||
)}`
|
||||
`${bullet} ${workerStarted} ${arrow} ${workerVersion} ${pipe} ${testLink} ${pipe} ${runsLink}`
|
||||
);
|
||||
|
||||
firstBuild = false;
|
||||
|
||||
@@ -4,11 +4,10 @@ import { GetProjectResponseBody, flattenAttributes } from "@trigger.dev/core/v3"
|
||||
import { recordSpanException } from "@trigger.dev/core/v3/workers";
|
||||
import chalk from "chalk";
|
||||
import { Command } from "commander";
|
||||
import { execa } from "execa";
|
||||
import { ExecaError, Options as ExecaOptions, ResultPromise as ExecaResult, execa } from "execa";
|
||||
import { applyEdits, modify, findNodeAtLocation, parseTree, getNodeValue } from "jsonc-parser";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join, relative, resolve } from "node:path";
|
||||
import terminalLink from "terminal-link";
|
||||
import { z } from "zod";
|
||||
import { CliApiClient } from "../apiClient";
|
||||
import {
|
||||
@@ -24,7 +23,7 @@ import {
|
||||
import { readConfig } from "../utilities/configFiles.js";
|
||||
import { createFileFromTemplate } from "../utilities/createFileFromTemplate";
|
||||
import { createFile, pathExists, readFile } from "../utilities/fileSystem";
|
||||
import { getUserPackageManager } from "../utilities/getUserPackageManager";
|
||||
import { PackageManager, getUserPackageManager } from "../utilities/getUserPackageManager";
|
||||
import { printStandloneInitialBanner } from "../utilities/initialBanner.js";
|
||||
import { logger } from "../utilities/logger";
|
||||
import { cliRootPath } from "../utilities/resolveInternalFilePath";
|
||||
@@ -32,12 +31,14 @@ import { login } from "./login";
|
||||
import { spinner } from "../utilities/windows";
|
||||
import { CLOUD_API_URL } from "../consts";
|
||||
import * as packageJson from "../../package.json";
|
||||
import { cliLink, prettyError } from "../utilities/cliOutput";
|
||||
|
||||
const InitCommandOptions = CommonCommandOptions.extend({
|
||||
projectRef: z.string().optional(),
|
||||
overrideConfig: z.boolean().default(false),
|
||||
tag: z.string().default("beta"),
|
||||
skipPackageInstall: z.boolean().default(false),
|
||||
pkgArgs: z.string().optional(),
|
||||
});
|
||||
|
||||
type InitCommandOptions = z.infer<typeof InitCommandOptions>;
|
||||
@@ -59,6 +60,10 @@ export function configureInitCommand(program: Command) {
|
||||
)
|
||||
.option("--skip-package-install", "Skip installing the @trigger.dev/sdk package")
|
||||
.option("--override-config", "Override the existing config file if it exists")
|
||||
.option(
|
||||
"--pkg-args <args>",
|
||||
"Additional arguments to pass to the package manager, accepts CSV for multiple args"
|
||||
)
|
||||
).action(async (path, options) => {
|
||||
await handleTelemetry(async () => {
|
||||
await printStandloneInitialBanner(true);
|
||||
@@ -78,6 +83,9 @@ async function _initCommand(dir: string, options: InitCommandOptions) {
|
||||
|
||||
intro("Initializing project");
|
||||
|
||||
// Detect tsconfig.json and exit if not found
|
||||
await detectTsConfig(dir, options);
|
||||
|
||||
const authorization = await login({
|
||||
embedded: true,
|
||||
defaultApiUrl: options.apiUrl,
|
||||
@@ -153,7 +161,7 @@ async function _initCommand(dir: string, options: InitCommandOptions) {
|
||||
// Ignore .trigger dir
|
||||
await gitIgnoreDotTriggerDir(dir, options);
|
||||
|
||||
const projectDashboard = terminalLink(
|
||||
const projectDashboard = cliLink(
|
||||
"project dashboard",
|
||||
`${authorization.dashboardUrl}/projects/v3/${selectedProject.externalRef}`
|
||||
);
|
||||
@@ -169,13 +177,10 @@ async function _initCommand(dir: string, options: InitCommandOptions) {
|
||||
);
|
||||
log.info(` 2. Visit your ${projectDashboard} to view your newly created tasks.`);
|
||||
log.info(
|
||||
` 3. Head over to our ${terminalLink(
|
||||
"v3 docs",
|
||||
"https://trigger.dev/docs/v3"
|
||||
)} to learn more.`
|
||||
` 3. Head over to our ${cliLink("v3 docs", "https://trigger.dev/docs/v3")} to learn more.`
|
||||
);
|
||||
log.info(
|
||||
` 4. Need help? Join our ${terminalLink(
|
||||
` 4. Need help? Join our ${cliLink(
|
||||
"Discord community",
|
||||
"https://trigger.dev/discord"
|
||||
)} or email us at ${chalk.cyan("help@trigger.dev")}`
|
||||
@@ -321,8 +326,46 @@ async function gitIgnoreDotTriggerDir(dir: string, options: InitCommandOptions)
|
||||
});
|
||||
}
|
||||
|
||||
async function detectTsConfig(dir: string, options: InitCommandOptions) {
|
||||
return await tracer.startActiveSpan("detectTsConfig", async (span) => {
|
||||
try {
|
||||
const projectDir = resolve(process.cwd(), dir);
|
||||
const tsconfigPath = join(projectDir, "tsconfig.json");
|
||||
|
||||
span.setAttributes({
|
||||
"cli.projectDir": projectDir,
|
||||
"cli.tsconfigPath": tsconfigPath,
|
||||
});
|
||||
|
||||
const tsconfigExists = await pathExists(tsconfigPath);
|
||||
|
||||
if (!tsconfigExists) {
|
||||
prettyError(
|
||||
"No tsconfig.json found",
|
||||
`The init command needs to be run in a TypeScript project. You can create one like this:`,
|
||||
`npm install typescript --save-dev\nnpx tsc --init\n`
|
||||
);
|
||||
|
||||
throw new Error("TypeScript required");
|
||||
}
|
||||
|
||||
logger.debug("tsconfig.json exists", { tsconfigPath });
|
||||
|
||||
span.end();
|
||||
} catch (e) {
|
||||
if (!(e instanceof SkipCommandError)) {
|
||||
recordSpanException(span, e);
|
||||
}
|
||||
|
||||
span.end();
|
||||
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function addConfigFileToTsConfig(dir: string, options: InitCommandOptions) {
|
||||
return await tracer.startActiveSpan("createTriggerDir", async (span) => {
|
||||
return await tracer.startActiveSpan("addConfigFileToTsConfig", async (span) => {
|
||||
try {
|
||||
const projectDir = resolve(process.cwd(), dir);
|
||||
const tsconfigPath = join(projectDir, "tsconfig.json");
|
||||
@@ -391,9 +434,12 @@ async function installPackages(dir: string, options: InitCommandOptions) {
|
||||
return await tracer.startActiveSpan("installPackages", async (span) => {
|
||||
const installSpinner = spinner();
|
||||
|
||||
let pkgManager: PackageManager | undefined;
|
||||
|
||||
try {
|
||||
const projectDir = resolve(process.cwd(), dir);
|
||||
const pkgManager = await getUserPackageManager(projectDir);
|
||||
|
||||
pkgManager = await getUserPackageManager(projectDir);
|
||||
|
||||
span.setAttributes({
|
||||
"cli.projectDir": projectDir,
|
||||
@@ -401,54 +447,63 @@ async function installPackages(dir: string, options: InitCommandOptions) {
|
||||
"cli.tag": options.tag,
|
||||
});
|
||||
|
||||
const userArgs = options.pkgArgs?.split(",") ?? [];
|
||||
const execaOptions = { cwd: projectDir } satisfies ExecaOptions;
|
||||
|
||||
let installProcess: ExecaResult<typeof execaOptions>;
|
||||
let args: string[];
|
||||
|
||||
switch (pkgManager) {
|
||||
case "npm": {
|
||||
installSpinner.start(`Running npm install @trigger.dev/sdk@${options.tag}`);
|
||||
|
||||
// --save-exact: pin version, e.g. 3.0.0-beta.20 instead of ^3.0.0-beta.20
|
||||
await execa("npm", ["install", "--save-exact", `@trigger.dev/sdk@${options.tag}`], {
|
||||
cwd: projectDir,
|
||||
stdio: options.logLevel === "debug" ? "inherit" : "ignore",
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
case "pnpm": {
|
||||
installSpinner.start(`Running pnpm add @trigger.dev/sdk@${options.tag}`);
|
||||
|
||||
// pins version by default
|
||||
await execa("pnpm", ["add", `@trigger.dev/sdk@${options.tag}`], {
|
||||
cwd: projectDir,
|
||||
stdio: options.logLevel === "debug" ? "inherit" : "ignore",
|
||||
});
|
||||
args = ["install", "--save-exact", ...userArgs, `@trigger.dev/sdk@${options.tag}`];
|
||||
|
||||
break;
|
||||
}
|
||||
case "pnpm":
|
||||
case "yarn": {
|
||||
installSpinner.start(`Running yarn add @trigger.dev/sdk@${options.tag}`);
|
||||
|
||||
// pins version by default
|
||||
await execa("yarn", ["add", `@trigger.dev/sdk@${options.tag}`], {
|
||||
cwd: projectDir,
|
||||
stdio: options.logLevel === "debug" ? "inherit" : "ignore",
|
||||
});
|
||||
args = ["add", ...userArgs, `@trigger.dev/sdk@${options.tag}`];
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
installSpinner.start(`Running ${pkgManager} ${args.join(" ")}`);
|
||||
|
||||
installProcess = execa(pkgManager, args, execaOptions);
|
||||
|
||||
const handleProcessOutput = (data: Buffer) => {
|
||||
logger.debug(data.toString());
|
||||
};
|
||||
|
||||
installProcess.stderr?.on("data", handleProcessOutput);
|
||||
installProcess.stdout?.on("data", handleProcessOutput);
|
||||
|
||||
await installProcess;
|
||||
|
||||
installSpinner.stop(`@trigger.dev/sdk@${options.tag} installed`);
|
||||
|
||||
span.end();
|
||||
} catch (e) {
|
||||
installSpinner.stop(
|
||||
`Failed to install @trigger.dev/sdk@${options.tag}. Rerun command with --log-level debug for more details.`
|
||||
);
|
||||
if (options.logLevel === "debug") {
|
||||
installSpinner.stop(`Failed to install @trigger.dev/sdk@${options.tag}.`);
|
||||
} else {
|
||||
installSpinner.stop(
|
||||
`Failed to install @trigger.dev/sdk@${options.tag}. Rerun command with --log-level debug for more details.`
|
||||
);
|
||||
}
|
||||
|
||||
if (!(e instanceof SkipCommandError)) {
|
||||
recordSpanException(span, e);
|
||||
}
|
||||
|
||||
if (e instanceof ExecaError) {
|
||||
if (pkgManager) {
|
||||
e.message += ` \n\nNote: You can pass additional args to ${pkgManager} by using --pkg-args. For example: trigger.dev init --pkg-args="--workspace-root"`;
|
||||
}
|
||||
}
|
||||
|
||||
span.end();
|
||||
|
||||
throw e;
|
||||
@@ -546,7 +601,7 @@ async function selectProject(apiClient: CliApiClient, dashboardUrl: string, proj
|
||||
}
|
||||
|
||||
if (projectsResponse.data.length === 0) {
|
||||
const newProjectLink = terminalLink(
|
||||
const newProjectLink = cliLink(
|
||||
"Create new project",
|
||||
`${dashboardUrl}/projects/new?version=v3`
|
||||
);
|
||||
|
||||
@@ -11,7 +11,7 @@ import { join, resolve } from "path";
|
||||
import { JavascriptProject } from "../utilities/javascriptProject.js";
|
||||
import { PackageManager } from "../utilities/getUserPackageManager.js";
|
||||
import { getVersion } from "../utilities/getVersion.js";
|
||||
import { chalkError, prettyWarning } from "../utilities/cliOutput.js";
|
||||
import { chalkError, prettyError, prettyWarning } from "../utilities/cliOutput.js";
|
||||
|
||||
export const UpdateCommandOptions = CommonCommandOptions.pick({
|
||||
logLevel: true,
|
||||
@@ -81,7 +81,13 @@ export async function updateTriggerPackages(
|
||||
|
||||
const triggerDependencies = getTriggerDependencies(packageJson);
|
||||
|
||||
function getVersionMismatches(deps: Dependency[], targetVersion: string): Dependency[] {
|
||||
function getVersionMismatches(
|
||||
deps: Dependency[],
|
||||
targetVersion: string
|
||||
): {
|
||||
mismatches: Dependency[];
|
||||
isDowngrade: boolean;
|
||||
} {
|
||||
const mismatches: Dependency[] = [];
|
||||
|
||||
for (const dep of deps) {
|
||||
@@ -92,12 +98,35 @@ export async function updateTriggerPackages(
|
||||
mismatches.push(dep);
|
||||
}
|
||||
|
||||
return mismatches;
|
||||
const extractRelease = (version: string) => {
|
||||
const release = Number(version.split("3.0.0-beta.")[1]);
|
||||
return release || undefined;
|
||||
};
|
||||
|
||||
let isDowngrade = false;
|
||||
const targetRelease = extractRelease(targetVersion);
|
||||
|
||||
if (targetRelease) {
|
||||
isDowngrade = mismatches.some((dep) => {
|
||||
const depRelease = extractRelease(dep.version);
|
||||
|
||||
if (!depRelease) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return depRelease > targetRelease;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
mismatches,
|
||||
isDowngrade,
|
||||
};
|
||||
}
|
||||
|
||||
const versionMismatches = getVersionMismatches(triggerDependencies, cliVersion);
|
||||
const { mismatches, isDowngrade } = getVersionMismatches(triggerDependencies, cliVersion);
|
||||
|
||||
if (versionMismatches.length === 0) {
|
||||
if (mismatches.length === 0) {
|
||||
if (!embedded) {
|
||||
outro(`Nothing to do${newCliVersion ? " ..but you should really update your CLI!" : ""}`);
|
||||
return hasOutput;
|
||||
@@ -105,10 +134,14 @@ export async function updateTriggerPackages(
|
||||
return hasOutput;
|
||||
}
|
||||
|
||||
prettyWarning(
|
||||
"Mismatch between your CLI version and installed packages",
|
||||
"We recommend pinned versions for guaranteed compatibility"
|
||||
);
|
||||
if (isDowngrade) {
|
||||
prettyError("Some of the installed @trigger.dev packages are newer than your CLI version");
|
||||
} else {
|
||||
prettyWarning(
|
||||
"Mismatch between your CLI version and installed packages",
|
||||
"We recommend pinned versions for guaranteed compatibility"
|
||||
);
|
||||
}
|
||||
|
||||
if (!process.stdout.isTTY) {
|
||||
// Running in CI with version mismatch detected
|
||||
@@ -124,7 +157,22 @@ export async function updateTriggerPackages(
|
||||
CLI version: ${cliVersion}
|
||||
|
||||
Current package versions that don't match the CLI:
|
||||
${versionMismatches.map((dep) => `- ${dep.name}@${dep.version}`).join("\n")}\n`
|
||||
${mismatches.map((dep) => `- ${dep.name}@${dep.version}`).join("\n")}\n`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// WARNING: We can only start accepting user input once we know this is a TTY, otherwise, the process will exit with an error in CI
|
||||
|
||||
if (isDowngrade) {
|
||||
printUpdateTable("Versions", mismatches, cliVersion, "installed", "CLI");
|
||||
|
||||
outro("CLI update required!");
|
||||
|
||||
logger.log(
|
||||
`${chalkError(
|
||||
"X Error:"
|
||||
)} Please update your CLI. Alternatively, use \`--skip-update-check\` at your own risk.\n`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -132,7 +180,7 @@ export async function updateTriggerPackages(
|
||||
log.message(""); // spacing
|
||||
|
||||
// Always require user confirmation
|
||||
const userWantsToUpdate = await updateConfirmation(versionMismatches, cliVersion);
|
||||
const userWantsToUpdate = await updateConfirmation(mismatches, cliVersion);
|
||||
|
||||
if (isCancel(userWantsToUpdate)) {
|
||||
throw new OutroCommandError();
|
||||
@@ -175,7 +223,7 @@ export async function updateTriggerPackages(
|
||||
process.prependOnceListener("exit", exitHandler);
|
||||
|
||||
// Update package.json
|
||||
mutatePackageJsonWithUpdatedPackages(packageJson, versionMismatches, cliVersion);
|
||||
mutatePackageJsonWithUpdatedPackages(packageJson, mismatches, cliVersion);
|
||||
await writeJSONFile(packageJsonPath, packageJson, true);
|
||||
|
||||
async function revertPackageJsonChanges() {
|
||||
@@ -274,20 +322,26 @@ function mutatePackageJsonWithUpdatedPackages(
|
||||
}
|
||||
}
|
||||
|
||||
function printUpdateTable(depsToUpdate: Dependency[], targetVersion: string): void {
|
||||
log.message("Suggested updates");
|
||||
function printUpdateTable(
|
||||
heading: string,
|
||||
depsToUpdate: Dependency[],
|
||||
targetVersion: string,
|
||||
oldColumn = "old",
|
||||
newColumn = "new"
|
||||
): void {
|
||||
log.message(heading);
|
||||
|
||||
const tableData = depsToUpdate.map((dep) => ({
|
||||
package: dep.name,
|
||||
old: dep.version,
|
||||
new: targetVersion,
|
||||
[oldColumn]: dep.version,
|
||||
[newColumn]: targetVersion,
|
||||
}));
|
||||
|
||||
logger.table(tableData);
|
||||
}
|
||||
|
||||
async function updateConfirmation(depsToUpdate: Dependency[], targetVersion: string) {
|
||||
printUpdateTable(depsToUpdate, targetVersion);
|
||||
printUpdateTable("Suggested updates", depsToUpdate, targetVersion);
|
||||
|
||||
let confirmMessage = "Would you like to apply those updates?";
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { log } from "@clack/prompts";
|
||||
import chalk from "chalk";
|
||||
import terminalLink, { Options as TerminalLinkOptions } from "terminal-link";
|
||||
|
||||
export const green = "#4FFF54";
|
||||
export const purple = "#735BF3";
|
||||
@@ -110,3 +111,10 @@ export function prettyWarning(header: string, body?: string, footer?: string) {
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
export function cliLink(text: string, url: string, options?: TerminalLinkOptions) {
|
||||
return terminalLink(text, url, {
|
||||
fallback: (text, url) => `${text} ${url}`,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import chalk from "chalk";
|
||||
import { relative } from "node:path";
|
||||
import { chalkError, chalkPurple, chalkGrey, chalkGreen, chalkWarning } from "./cliOutput";
|
||||
import { chalkError, chalkPurple, chalkGrey, chalkGreen, chalkWarning, cliLink } from "./cliOutput";
|
||||
import { logger } from "./logger";
|
||||
import { ReadConfigResult } from "./configFiles";
|
||||
import { z } from "zod";
|
||||
import { groupTaskMetadataIssuesByTask } from "@trigger.dev/core/v3";
|
||||
import terminalLink from "terminal-link";
|
||||
import { docs } from "./links";
|
||||
|
||||
export type ESMRequireError = {
|
||||
@@ -114,7 +113,7 @@ export function logESMRequireError(parsedError: ESMRequireError, resolvedConfig:
|
||||
}
|
||||
|
||||
logger.log(
|
||||
`${chalkGrey("○")} For more info see the ${terminalLink("relevant docs", docs.config.esm)}.\n`
|
||||
`${chalkGrey("○")} For more info see the ${cliLink("relevant docs", docs.config.esm)}.\n`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ export const LOCKFILES = {
|
||||
npmShrinkwrap: "npm-shrinkwrap.json",
|
||||
pnpm: "pnpm-lock.yaml",
|
||||
yarn: "yarn.lock",
|
||||
bun: "bun.lockb",
|
||||
};
|
||||
|
||||
export async function getUserPackageManager(path: string): Promise<PackageManager> {
|
||||
@@ -53,11 +54,16 @@ export async function detectPackageManagerFromArtifacts(path: string): Promise<P
|
||||
|
||||
switch (basename(foundPath)) {
|
||||
case LOCKFILES.yarn:
|
||||
logger.debug("Found yarn artifact", { foundPath });
|
||||
return "yarn";
|
||||
case LOCKFILES.pnpm:
|
||||
logger.debug("Found pnpm artifact", { foundPath });
|
||||
return "pnpm";
|
||||
case LOCKFILES.npm:
|
||||
case LOCKFILES.npmShrinkwrap:
|
||||
logger.debug("Found npm artifact", { foundPath });
|
||||
case LOCKFILES.bun:
|
||||
logger.debug("Found bun artifact", { foundPath });
|
||||
return "npm";
|
||||
default:
|
||||
throw new Error(`Unhandled package manager detection path: ${foundPath}`);
|
||||
|
||||
@@ -23,7 +23,6 @@ import dotenv from "dotenv";
|
||||
import { Evt } from "evt";
|
||||
import { ChildProcess, fork } from "node:child_process";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import terminalLink from "terminal-link";
|
||||
import {
|
||||
chalkError,
|
||||
chalkGrey,
|
||||
@@ -33,6 +32,7 @@ import {
|
||||
chalkTask,
|
||||
chalkWarning,
|
||||
chalkWorker,
|
||||
cliLink,
|
||||
prettyPrintDate,
|
||||
} from "../../utilities/cliOutput.js";
|
||||
import { safeDeleteFileSync } from "../../utilities/fileSystem.js";
|
||||
@@ -606,7 +606,7 @@ export class BackgroundWorker {
|
||||
|
||||
const pipe = chalkGrey("|");
|
||||
const bullet = chalkGrey("○");
|
||||
const link = chalkLink(terminalLink("View logs", logsUrl));
|
||||
const link = chalkLink(cliLink("View logs", logsUrl));
|
||||
let timestampPrefix = chalkGrey(prettyPrintDate(payload.execution.attempt.startedAt));
|
||||
const workerPrefix = chalkWorker(this.metadata.version);
|
||||
const taskPrefix = chalkTask(execution.task.id);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// See: https://www.totaltypescript.com/tsconfig-cheat-sheet
|
||||
{
|
||||
"include": ["./src/**/*.ts", "./src/**/*.tsx", "./e2e/**/*.ts"],
|
||||
"include": ["./src/**/*.ts", "./src/**/*.tsx", "./e2e/**/*.ts", "./types.d.ts"],
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
declare module "terminal-link" {
|
||||
export interface Options {
|
||||
fallback?: ((text: string, url: string) => string) | boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated The default fallback is broken in some terminals. Please use `cliLink` instead.
|
||||
*/
|
||||
export default function terminalLink(text: string, url: string, options?: Options): string;
|
||||
}
|
||||
Reference in New Issue
Block a user