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..4716db1f99c 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,9 +136,21 @@ 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) { + // 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: [ @@ -165,6 +195,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);