From 4bafa7ee08730a6124cf46e607f8d71d696a8615 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 30 Apr 2026 23:33:36 +0100 Subject: [PATCH 1/2] feat(cli-v3): add --no-browser flag and Examples to init/login --help Adds an obvious bypass for the browser auto-open during auth, the most common friction point for agentic and other non-interactive uses. Also: error loudly when init runs under non-TTY stdin without --yes (previously default-and-exited silently, leaving the project half-initialized) and surface common invocations via an Examples block in --help output. --- .changeset/cli-no-browser-headless-init.md | 5 ++++ packages/cli-v3/src/commands/init.ts | 30 +++++++++++++++++++++- packages/cli-v3/src/commands/login.ts | 29 ++++++++++++++++++--- 3 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 .changeset/cli-no-browser-headless-init.md diff --git a/.changeset/cli-no-browser-headless-init.md b/.changeset/cli-no-browser-headless-init.md new file mode 100644 index 00000000000..e78b74cd6a3 --- /dev/null +++ b/.changeset/cli-no-browser-headless-init.md @@ -0,0 +1,5 @@ +--- +"trigger.dev": patch +--- + +Add `--no-browser` flag to `init` and `login` to skip auto-opening the browser during authentication. Also error loudly when `init` is run without `--yes` under non-TTY stdin (previously default-and-exited silently, leaving the project half-initialized). Both commands now show an `Examples` section in `--help`. diff --git a/packages/cli-v3/src/commands/init.ts b/packages/cli-v3/src/commands/init.ts index 82c4d0e4e09..763304163e0 100644 --- a/packages/cli-v3/src/commands/init.ts +++ b/packages/cli-v3/src/commands/init.ts @@ -57,6 +57,7 @@ const InitCommandOptions = CommonCommandOptions.extend({ gitRef: z.string().default("main"), javascript: z.boolean().default(false), yes: z.boolean().default(false), + browser: z.boolean().default(true), }); type InitCommandOptions = z.infer; @@ -65,7 +66,23 @@ export function configureInitCommand(program: Command) { return commonOptions( program .command("init") - .description("Initialize your existing project for development with Trigger.dev") + .summary("Initialize your existing project for development with Trigger.dev") + .description( + `Initialize your existing project for development with Trigger.dev. + +Examples: + # Interactive setup + $ trigger.dev init + + # Non-interactive (CI / scripts) + $ trigger.dev init --yes --project-ref proj_abc123 + + # Headless / agent (no browser) + $ trigger.dev init --yes --project-ref proj_abc123 --no-browser + + # Use a named profile + $ trigger.dev init --profile staging` + ) .argument("[path]", "The path to the project", ".") .option( "-p, --project-ref ", @@ -89,6 +106,7 @@ export function configureInitCommand(program: Command) { "Additional arguments to pass to the package manager, accepts CSV for multiple args" ) .option("-y, --yes", "Skip all prompts and use defaults (requires --project-ref)") + .option("--no-browser", "Don't automatically open the browser during login; print the URL only") ) .addOption( new CommandOption( @@ -118,6 +136,15 @@ async function _initCommand(dir: string, options: InitCommandOptions) { throw new Error("--project-ref is required when using --yes flag"); } + // Refuse to run interactively when stdin isn't a TTY (CI, agent harness, etc). + // Previously this silently default-and-exited at the first prompt, leaving the + // project half-initialized. + if (!options.yes && !process.stdin.isTTY) { + throw new Error( + "Interactive prompts cannot be used in non-TTY environments. Pass --yes (and --project-ref) to run non-interactively." + ); + } + const hasSeenMCPInstallPrompt = readConfigHasSeenMCPInstallPrompt(); if (!hasSeenMCPInstallPrompt) { @@ -165,6 +192,7 @@ async function _initCommand(dir: string, options: InitCommandOptions) { embedded: true, defaultApiUrl: options.apiUrl, profile: options.profile, + browser: options.browser, }); if (!authorization.ok) { diff --git a/packages/cli-v3/src/commands/login.ts b/packages/cli-v3/src/commands/login.ts index 59af7ee895a..3561183b36a 100644 --- a/packages/cli-v3/src/commands/login.ts +++ b/packages/cli-v3/src/commands/login.ts @@ -41,6 +41,7 @@ import { links } from "@trigger.dev/core/v3"; export const LoginCommandOptions = CommonCommandOptions.extend({ apiUrl: z.string(), + browser: z.boolean().default(true), }); export type LoginCommandOptions = z.infer; @@ -49,7 +50,21 @@ export function configureLoginCommand(program: Command) { return commonOptions( program .command("login") - .description("Login with Trigger.dev so you can perform authenticated actions") + .summary("Login with Trigger.dev so you can perform authenticated actions") + .description( + `Login with Trigger.dev so you can perform authenticated actions. + +Examples: + # Interactive login (opens browser) + $ trigger.dev login + + # Headless / agent (print URL only) + $ trigger.dev login --no-browser + + # Login to a named profile + $ trigger.dev login --profile staging` + ) + .option("--no-browser", "Don't automatically open the browser; print the URL only") ) .version(VERSION, "-v, --version", "Display the version number") .action(async (options) => { @@ -67,7 +82,12 @@ export async function loginCommand(options: unknown) { } async function _loginCommand(options: LoginCommandOptions) { - return login({ defaultApiUrl: options.apiUrl, embedded: false, profile: options.profile }); + return login({ + defaultApiUrl: options.apiUrl, + embedded: false, + profile: options.profile, + browser: options.browser, + }); } export type LoginOptions = { @@ -75,6 +95,7 @@ export type LoginOptions = { embedded?: boolean; profile?: string; silent?: boolean; + browser?: boolean; }; export async function login(options?: LoginOptions): Promise { @@ -259,7 +280,9 @@ export async function login(options?: LoginOptions): Promise { `Please visit the following URL to login:\n${chalkLink(authorizationCodeResult.url)}` ); - if (await isLinuxServer()) { + if (opts.browser === false) { + log.message("Browser auto-open disabled. Visit the URL above to login."); + } else if (await isLinuxServer()) { log.message("Please install `xdg-utils` to automatically open the login URL."); } else { await open(authorizationCodeResult.url); From 8adb94feaee3409e341369bba2dde982a38278d8 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 30 Apr 2026 23:53:49 +0100 Subject: [PATCH 2/2] fix(cli-v3): skip MCP install prompt when --yes is set On a fresh machine where hasSeenMCPInstallPrompt is false, the prompt at init.ts:150 fired unconditionally and would hang the --yes flow that the non-TTY guard above is meant to support. Default to CLI (skip MCP) when --yes is passed. --- packages/cli-v3/src/commands/init.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/cli-v3/src/commands/init.ts b/packages/cli-v3/src/commands/init.ts index 763304163e0..4716db1f99c 100644 --- a/packages/cli-v3/src/commands/init.ts +++ b/packages/cli-v3/src/commands/init.ts @@ -147,7 +147,10 @@ async function _initCommand(dir: string, options: InitCommandOptions) { const hasSeenMCPInstallPrompt = readConfigHasSeenMCPInstallPrompt(); - if (!hasSeenMCPInstallPrompt) { + // Skip the MCP-vs-CLI prompt when --yes is set: the user explicitly chose CLI + // by running `trigger.dev init` non-interactively, and the prompt would + // otherwise hang on a fresh machine where `hasSeenMCPInstallPrompt` is false. + if (!hasSeenMCPInstallPrompt && !options.yes) { const installChoice = await select({ message: "Choose how you want to initialize your project:", options: [