From 2c9ce9b632b12e61ad0a89e336527e140ceb0aa8 Mon Sep 17 00:00:00 2001 From: Jon Samp Date: Sat, 11 Jul 2026 18:39:23 -0500 Subject: [PATCH 1/5] [eas-cli] Add `eas browse` command Add `eas browse [PAGE]` to open the project page on expo.dev, with an optional whitelisted page argument (e.g. `build`, `hosting`, `cicd`) for project subpages and a `--no-browser` flag to print the URL instead. `eas open` becomes a thin alias that opens the project dashboard. --- CHANGELOG.md | 1 + packages/eas-cli/README.md | 26 ++++- .../src/build/utils/__tests__/url.test.ts | 35 ++++++ packages/eas-cli/src/build/utils/url.ts | 16 ++- .../src/commands/__tests__/browse.test.ts | 105 ++++++++++++++++++ packages/eas-cli/src/commands/browse.ts | 91 +++++++++++++++ packages/eas-cli/src/commands/open.ts | 42 +------ 7 files changed, 271 insertions(+), 45 deletions(-) create mode 100644 packages/eas-cli/src/build/utils/__tests__/url.test.ts create mode 100644 packages/eas-cli/src/commands/__tests__/browse.test.ts create mode 100644 packages/eas-cli/src/commands/browse.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f45468fc40..2af92e769c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ This is the log of notable changes to EAS CLI and related packages. ### ๐ŸŽ‰ New features +- [eas-cli] Add `eas browse` to open the project page on expo.dev in a web browser, with an optional page argument (e.g. `eas browse build`, `eas browse hosting`) for project subpages and a `--no-browser` flag to print the URL. `eas open` is now an alias of `eas browse`. ([#3999](https://github.com/expo/eas-cli/pull/3999) by [@jonsamp](https://github.com/jonsamp)) - [eas-cli] Improve `eas workflow:create`: add a `--template` flag, generate a placeholder workflow when a file name is passed, use shorter default file names (`build.yml`, `update.yml`, `deploy.yml`), configure EAS Build and EAS Update automatically when the chosen template requires them, set default app identifiers without prompting for the development build and deploy templates, install `expo-dev-client` during development build setup, and tighten the generated comments and next steps. ([#3943](https://github.com/expo/eas-cli/pull/3943) by [@jonsamp](https://github.com/jonsamp)) - [eas-cli] `eas integrations:posthog:dashboard` now opens PostHog via a signed-in link, skipping the login prompt. ([#3975](https://github.com/expo/eas-cli/pull/3975) by [@gwdp](https://github.com/gwdp)) - [build-tools] Add `eas/posthog_capture_event` workflow function to send PostHog events from workflow runs. ([#3934](https://github.com/expo/eas-cli/pull/3934) by [@gwdp](https://github.com/gwdp)) diff --git a/packages/eas-cli/README.md b/packages/eas-cli/README.md index 371e1d2979..47b3ef19d1 100644 --- a/packages/eas-cli/README.md +++ b/packages/eas-cli/README.md @@ -67,6 +67,7 @@ eas --help COMMAND * [`eas branch:list`](#eas-branchlist) * [`eas branch:rename`](#eas-branchrename) * [`eas branch:view [NAME]`](#eas-branchview-name) +* [`eas browse [PAGE]`](#eas-browse-page) * [`eas build`](#eas-build) * [`eas build:cancel [BUILD_ID]`](#eas-buildcancel-build_id) * [`eas build:configure`](#eas-buildconfigure) @@ -433,6 +434,27 @@ DESCRIPTION _See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v21.0.0/packages/eas-cli/src/commands/branch/view.ts)_ +## `eas browse [PAGE]` + +open the project page in a web browser + +``` +USAGE + $ eas browse [PAGE] [-n] + +ARGUMENTS + [PAGE] (build|builds|submit|submissions|update|updates|workflow|workflows|cicd|hosting|deployments|credentials|env|in + sights|settings) Project subpage to open. Defaults to the project dashboard. + +FLAGS + -n, --no-browser Print the URL instead of opening it in a web browser + +DESCRIPTION + open the project page in a web browser +``` + +_See code: [packages/eas-cli/src/commands/browse.ts](https://github.com/expo/eas-cli/blob/v20.5.1/packages/eas-cli/src/commands/browse.ts)_ + ## `eas build` start a build @@ -2194,14 +2216,14 @@ _See code: [packages/eas-cli/src/commands/observe/versions.ts](https://github.co ## `eas open` -open the project page in a web browser +open the project page in a web browser (alias of `eas browse`) ``` USAGE $ eas open DESCRIPTION - open the project page in a web browser + open the project page in a web browser (alias of `eas browse`) ``` _See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v21.0.0/packages/eas-cli/src/commands/open.ts)_ diff --git a/packages/eas-cli/src/build/utils/__tests__/url.test.ts b/packages/eas-cli/src/build/utils/__tests__/url.test.ts new file mode 100644 index 0000000000..fdc4488d4e --- /dev/null +++ b/packages/eas-cli/src/build/utils/__tests__/url.test.ts @@ -0,0 +1,35 @@ +import { getProjectDashboardUrl, getProjectPageUrl } from '../url'; + +describe(getProjectPageUrl, () => { + it('builds the project dashboard URL when no page is provided', () => { + expect(getProjectPageUrl('testuser', 'testapp')).toBe( + 'https://expo.dev/accounts/testuser/projects/testapp' + ); + }); + + it('appends the page path when provided', () => { + expect(getProjectPageUrl('testuser', 'testapp', 'builds')).toBe( + 'https://expo.dev/accounts/testuser/projects/testapp/builds' + ); + }); + + it('preserves nested page paths without encoding the separator', () => { + expect(getProjectPageUrl('testuser', 'testapp', 'hosting/deployments')).toBe( + 'https://expo.dev/accounts/testuser/projects/testapp/hosting/deployments' + ); + }); + + it('encodes account and project names', () => { + expect(getProjectPageUrl('my org', 'my app', 'updates')).toBe( + 'https://expo.dev/accounts/my%20org/projects/my%20app/updates' + ); + }); +}); + +describe(getProjectDashboardUrl, () => { + it('matches the page URL with no page', () => { + expect(getProjectDashboardUrl('testuser', 'testapp')).toBe( + getProjectPageUrl('testuser', 'testapp') + ); + }); +}); diff --git a/packages/eas-cli/src/build/utils/url.ts b/packages/eas-cli/src/build/utils/url.ts index 41a151f14a..81efce6236 100644 --- a/packages/eas-cli/src/build/utils/url.ts +++ b/packages/eas-cli/src/build/utils/url.ts @@ -4,10 +4,18 @@ import { getExpoApiBaseUrl, getExpoWebsiteBaseUrl } from '../../api'; import { AppPlatform, BuildFragment } from '../../graphql/generated'; export function getProjectDashboardUrl(accountName: string, projectName: string): string { - return new URL( - `/accounts/${accountName}/projects/${projectName}`, - getExpoWebsiteBaseUrl() - ).toString(); + return getProjectPageUrl(accountName, projectName); +} + +export function getProjectPageUrl( + accountName: string, + projectName: string, + page?: string | null +): string { + const projectPath = `/accounts/${encodeURIComponent(accountName)}/projects/${encodeURIComponent( + projectName + )}`; + return new URL(page ? `${projectPath}/${page}` : projectPath, getExpoWebsiteBaseUrl()).toString(); } export function getBuildLogsUrl(build: BuildFragment, hash?: string): string { diff --git a/packages/eas-cli/src/commands/__tests__/browse.test.ts b/packages/eas-cli/src/commands/__tests__/browse.test.ts new file mode 100644 index 0000000000..9cfa09a464 --- /dev/null +++ b/packages/eas-cli/src/commands/__tests__/browse.test.ts @@ -0,0 +1,105 @@ +import openBrowserAsync from 'better-opn'; + +import { getMockOclifConfig } from '../../__tests__/commands/utils'; +import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient'; +import { testProjectId } from '../../credentials/__tests__/fixtures-constants'; +import { AccountFragment } from '../../graphql/generated'; +import Log from '../../log'; +import { ora } from '../../ora'; +import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils'; +import Browse from '../browse'; + +jest.mock('better-opn'); +jest.mock('../../log'); +jest.mock('../../project/projectUtils'); +jest.mock('../../ora', () => ({ + ora: jest.fn(() => ({ + start: jest.fn().mockReturnThis(), + succeed: jest.fn().mockReturnThis(), + fail: jest.fn().mockReturnThis(), + })), +})); + +describe(Browse, () => { + const graphqlClient = {} as ExpoGraphqlClient; + const mockConfig = getMockOclifConfig(); + + function createCommand(argv: string[]): Browse { + const command = new Browse(argv, mockConfig); + jest.spyOn(command as any, 'getContextAsync').mockReturnValue({ + privateProjectConfig: { + projectId: testProjectId, + exp: { slug: 'testapp' }, + }, + loggedIn: { graphqlClient }, + } as never); + return command; + } + + beforeEach(() => { + jest.resetAllMocks(); + jest + .mocked(getOwnerAccountForProjectIdAsync) + .mockResolvedValue({ name: 'testuser' } as AccountFragment); + jest.mocked(openBrowserAsync).mockResolvedValue({} as never); + jest.mocked(ora).mockReturnValue({ + start: jest.fn().mockReturnThis(), + succeed: jest.fn().mockReturnThis(), + fail: jest.fn().mockReturnThis(), + } as any); + }); + + it('opens the project dashboard when no page is provided', async () => { + await createCommand([]).runAsync(); + + expect(openBrowserAsync).toHaveBeenCalledWith( + 'https://expo.dev/accounts/testuser/projects/testapp' + ); + }); + + it('opens a subpage by its EAS command name', async () => { + await createCommand(['build']).runAsync(); + + expect(openBrowserAsync).toHaveBeenCalledWith( + 'https://expo.dev/accounts/testuser/projects/testapp/builds' + ); + }); + + it('maps the website wording to the same subpage', async () => { + await createCommand(['submissions']).runAsync(); + + expect(openBrowserAsync).toHaveBeenCalledWith( + 'https://expo.dev/accounts/testuser/projects/testapp/submissions' + ); + }); + + it('prints the URL without opening a browser when --no-browser is passed', async () => { + await createCommand(['hosting', '--no-browser']).runAsync(); + + expect(openBrowserAsync).not.toHaveBeenCalled(); + expect(Log.log).toHaveBeenCalledWith( + 'https://expo.dev/accounts/testuser/projects/testapp/hosting' + ); + }); + + it('rejects an unknown page', async () => { + await expect(createCommand(['nope']).runAsync()).rejects.toThrow(); + expect(openBrowserAsync).not.toHaveBeenCalled(); + }); + + it('fails the spinner when the browser cannot be opened', async () => { + jest.mocked(openBrowserAsync).mockResolvedValue(false); + const spinner = { + start: jest.fn().mockReturnThis(), + succeed: jest.fn().mockReturnThis(), + fail: jest.fn().mockReturnThis(), + }; + jest.mocked(ora).mockReturnValue(spinner as any); + + await createCommand([]).runAsync(); + + expect(spinner.fail).toHaveBeenCalledWith( + expect.stringContaining('Unable to open a web browser') + ); + }); +}); diff --git a/packages/eas-cli/src/commands/browse.ts b/packages/eas-cli/src/commands/browse.ts new file mode 100644 index 0000000000..9018b91de9 --- /dev/null +++ b/packages/eas-cli/src/commands/browse.ts @@ -0,0 +1,91 @@ +import { Args, Flags } from '@oclif/core'; +import openBrowserAsync from 'better-opn'; + +import { getProjectPageUrl } from '../build/utils/url'; +import EasCommand from '../commandUtils/EasCommand'; +import Log from '../log'; +import { ora } from '../ora'; +import { getOwnerAccountForProjectIdAsync } from '../project/projectUtils'; + +// Maps the page argument to its path on the project dashboard. Keys are the accepted +// argument values; multiple keys can point at the same page so both the EAS command name +// (e.g. `build`) and the website's wording (e.g. `builds`) work. +const PROJECT_PAGES: Record = { + build: 'builds', + builds: 'builds', + submit: 'submissions', + submissions: 'submissions', + update: 'updates', + updates: 'updates', + workflow: 'workflows', + workflows: 'workflows', + cicd: 'workflows', + hosting: 'hosting', + deployments: 'hosting/deployments', + credentials: 'credentials', + env: 'environment-variables', + insights: 'insights', + settings: 'settings', +}; + +export default class Browse extends EasCommand { + static override description = 'open the project page in a web browser'; + + static override args = { + page: Args.string({ + description: 'Project subpage to open. Defaults to the project dashboard.', + required: false, + options: Object.keys(PROJECT_PAGES), + }), + }; + + static override flags = { + 'no-browser': Flags.boolean({ + char: 'n', + description: 'Print the URL instead of opening it in a web browser', + default: false, + }), + }; + + static override contextDefinition = { + ...this.ContextOptions.ProjectConfig, + ...this.ContextOptions.LoggedIn, + }; + + async runAsync(): Promise { + const { args, flags } = await this.parse(Browse); + const page = args.page ? PROJECT_PAGES[args.page] : null; + + // this command is interactive by nature (only really run by humans in a terminal) + const { + privateProjectConfig: { projectId, exp }, + loggedIn: { graphqlClient }, + } = await this.getContextAsync(Browse, { + nonInteractive: false, + withServerSideEnvironment: null, + }); + + const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId); + const url = getProjectPageUrl(account.name, exp.slug, page); + + if (flags['no-browser']) { + Log.log(url); + return; + } + + const failedMessage = `Unable to open a web browser. Project page is available at: ${url}`; + const spinner = ora(`Opening ${url}`).start(); + try { + const opened = await openBrowserAsync(url); + + if (opened) { + spinner.succeed(`Opened ${url}`); + } else { + spinner.fail(failedMessage); + } + } catch (error) { + spinner.fail(failedMessage); + throw error; + } + } +} diff --git a/packages/eas-cli/src/commands/open.ts b/packages/eas-cli/src/commands/open.ts index 1fdef3704b..ae3ca53216 100644 --- a/packages/eas-cli/src/commands/open.ts +++ b/packages/eas-cli/src/commands/open.ts @@ -1,46 +1,10 @@ -import openBrowserAsync from 'better-opn'; - -import { getProjectDashboardUrl } from '../build/utils/url'; import EasCommand from '../commandUtils/EasCommand'; -import { ora } from '../ora'; -import { getOwnerAccountForProjectIdAsync } from '../project/projectUtils'; +import Browse from './browse'; export default class Open extends EasCommand { - static override description = 'open the project page in a web browser'; - - static override contextDefinition = { - ...this.ContextOptions.ProjectConfig, - ...this.ContextOptions.LoggedIn, - }; + static override description = 'open the project page in a web browser (alias of `eas browse`)'; async runAsync(): Promise { - // this command is interactive by nature (only really run by humans in a terminal) - const { - privateProjectConfig: { projectId, exp }, - loggedIn: { graphqlClient }, - } = await this.getContextAsync(Open, { - nonInteractive: false, - withServerSideEnvironment: null, - }); - - const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId); - - const projectName = exp.slug; - - const projectDashboardUrl = getProjectDashboardUrl(account.name, projectName); - const failedMessage = `Unable to open a web browser. Project page is available at: ${projectDashboardUrl}`; - const spinner = ora(`Opening ${projectDashboardUrl}`).start(); - try { - const opened = await openBrowserAsync(projectDashboardUrl); - - if (opened) { - spinner.succeed(`Opened ${projectDashboardUrl}`); - } else { - spinner.fail(failedMessage); - } - } catch (error) { - spinner.fail(failedMessage); - throw error; - } + await Browse.run([]); } } From de92ff3594e69df485df2e12c8e371ac682ecccb Mon Sep 17 00:00:00 2001 From: Jon Samp Date: Sat, 11 Jul 2026 18:39:55 -0500 Subject: [PATCH 2/5] [eas-cli] Update changelog PR link --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2af92e769c..53a0d73a19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ This is the log of notable changes to EAS CLI and related packages. ### ๐ŸŽ‰ New features -- [eas-cli] Add `eas browse` to open the project page on expo.dev in a web browser, with an optional page argument (e.g. `eas browse build`, `eas browse hosting`) for project subpages and a `--no-browser` flag to print the URL. `eas open` is now an alias of `eas browse`. ([#3999](https://github.com/expo/eas-cli/pull/3999) by [@jonsamp](https://github.com/jonsamp)) +- [eas-cli] Add `eas browse` to open the project page on expo.dev in a web browser, with an optional page argument (e.g. `eas browse build`, `eas browse hosting`) for project subpages and a `--no-browser` flag to print the URL. `eas open` is now an alias of `eas browse`. ([#4003](https://github.com/expo/eas-cli/pull/4003) by [@jonsamp](https://github.com/jonsamp)) - [eas-cli] Improve `eas workflow:create`: add a `--template` flag, generate a placeholder workflow when a file name is passed, use shorter default file names (`build.yml`, `update.yml`, `deploy.yml`), configure EAS Build and EAS Update automatically when the chosen template requires them, set default app identifiers without prompting for the development build and deploy templates, install `expo-dev-client` during development build setup, and tighten the generated comments and next steps. ([#3943](https://github.com/expo/eas-cli/pull/3943) by [@jonsamp](https://github.com/jonsamp)) - [eas-cli] `eas integrations:posthog:dashboard` now opens PostHog via a signed-in link, skipping the login prompt. ([#3975](https://github.com/expo/eas-cli/pull/3975) by [@gwdp](https://github.com/gwdp)) - [build-tools] Add `eas/posthog_capture_event` workflow function to send PostHog events from workflow runs. ([#3934](https://github.com/expo/eas-cli/pull/3934) by [@gwdp](https://github.com/gwdp)) From 0c556501f075c280130e1715316b87e052290d4b Mon Sep 17 00:00:00 2001 From: Jon Samp Date: Sun, 12 Jul 2026 09:29:04 -0500 Subject: [PATCH 3/5] [eas-cli] Add `observe` page to `eas browse` --- packages/eas-cli/README.md | 2 +- packages/eas-cli/src/commands/__tests__/browse.test.ts | 8 ++++++++ packages/eas-cli/src/commands/browse.ts | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/eas-cli/README.md b/packages/eas-cli/README.md index 47b3ef19d1..5215894683 100644 --- a/packages/eas-cli/README.md +++ b/packages/eas-cli/README.md @@ -444,7 +444,7 @@ USAGE ARGUMENTS [PAGE] (build|builds|submit|submissions|update|updates|workflow|workflows|cicd|hosting|deployments|credentials|env|in - sights|settings) Project subpage to open. Defaults to the project dashboard. + sights|observe|settings) Project subpage to open. Defaults to the project dashboard. FLAGS -n, --no-browser Print the URL instead of opening it in a web browser diff --git a/packages/eas-cli/src/commands/__tests__/browse.test.ts b/packages/eas-cli/src/commands/__tests__/browse.test.ts index 9cfa09a464..2f0437d671 100644 --- a/packages/eas-cli/src/commands/__tests__/browse.test.ts +++ b/packages/eas-cli/src/commands/__tests__/browse.test.ts @@ -73,6 +73,14 @@ describe(Browse, () => { ); }); + it('opens the observe page', async () => { + await createCommand(['observe']).runAsync(); + + expect(openBrowserAsync).toHaveBeenCalledWith( + 'https://expo.dev/accounts/testuser/projects/testapp/observe' + ); + }); + it('prints the URL without opening a browser when --no-browser is passed', async () => { await createCommand(['hosting', '--no-browser']).runAsync(); diff --git a/packages/eas-cli/src/commands/browse.ts b/packages/eas-cli/src/commands/browse.ts index 9018b91de9..4e4b0b0f83 100644 --- a/packages/eas-cli/src/commands/browse.ts +++ b/packages/eas-cli/src/commands/browse.ts @@ -25,6 +25,7 @@ const PROJECT_PAGES: Record = { credentials: 'credentials', env: 'environment-variables', insights: 'insights', + observe: 'observe', settings: 'settings', }; From fe82555bab8bc5af71c156951bbbcfa8bc1283dd Mon Sep 17 00:00:00 2001 From: Jon Samp Date: Sun, 12 Jul 2026 10:29:35 -0500 Subject: [PATCH 4/5] [eas-cli] Support --json and --non-interactive in eas browse, hide eas open --- packages/eas-cli/README.md | 25 ++++------------ .../src/commands/__tests__/browse.test.ts | 29 +++++++++++++++++++ packages/eas-cli/src/commands/browse.ts | 24 +++++++++++---- packages/eas-cli/src/commands/open.ts | 2 ++ 4 files changed, 55 insertions(+), 25 deletions(-) diff --git a/packages/eas-cli/README.md b/packages/eas-cli/README.md index 5215894683..99c02d9a67 100644 --- a/packages/eas-cli/README.md +++ b/packages/eas-cli/README.md @@ -143,7 +143,6 @@ eas --help COMMAND * [`eas observe:routes`](#eas-observeroutes) * [`eas observe:session SESSIONID`](#eas-observesession-sessionid) * [`eas observe:versions`](#eas-observeversions) -* [`eas open`](#eas-open) * [`eas project:info`](#eas-projectinfo) * [`eas project:init`](#eas-projectinit) * [`eas project:new [PATH]`](#eas-projectnew-path) @@ -436,21 +435,23 @@ _See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/exp ## `eas browse [PAGE]` -open the project page in a web browser +Transition from the terminal to the web browser to view and interact with your project on https://expo.dev ``` USAGE - $ eas browse [PAGE] [-n] + $ eas browse [PAGE] [-n] [--json] [--non-interactive] ARGUMENTS [PAGE] (build|builds|submit|submissions|update|updates|workflow|workflows|cicd|hosting|deployments|credentials|env|in sights|observe|settings) Project subpage to open. Defaults to the project dashboard. FLAGS - -n, --no-browser Print the URL instead of opening it in a web browser + -n, --no-browser Print the URL instead of opening it in a web browser + --json Enable JSON output, non-JSON messages will be printed to stderr. Implies --non-interactive. + --non-interactive Run the command in non-interactive mode. DESCRIPTION - open the project page in a web browser + Transition from the terminal to the web browser to view and interact with your project on https://expo.dev ``` _See code: [packages/eas-cli/src/commands/browse.ts](https://github.com/expo/eas-cli/blob/v20.5.1/packages/eas-cli/src/commands/browse.ts)_ @@ -2214,20 +2215,6 @@ DESCRIPTION _See code: [packages/eas-cli/src/commands/observe/versions.ts](https://github.com/expo/eas-cli/blob/v21.0.0/packages/eas-cli/src/commands/observe/versions.ts)_ -## `eas open` - -open the project page in a web browser (alias of `eas browse`) - -``` -USAGE - $ eas open - -DESCRIPTION - open the project page in a web browser (alias of `eas browse`) -``` - -_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v21.0.0/packages/eas-cli/src/commands/open.ts)_ - ## `eas project:info` information about the current project diff --git a/packages/eas-cli/src/commands/__tests__/browse.test.ts b/packages/eas-cli/src/commands/__tests__/browse.test.ts index 2f0437d671..c33e4e69f8 100644 --- a/packages/eas-cli/src/commands/__tests__/browse.test.ts +++ b/packages/eas-cli/src/commands/__tests__/browse.test.ts @@ -7,11 +7,13 @@ import { AccountFragment } from '../../graphql/generated'; import Log from '../../log'; import { ora } from '../../ora'; import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils'; +import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json'; import Browse from '../browse'; jest.mock('better-opn'); jest.mock('../../log'); jest.mock('../../project/projectUtils'); +jest.mock('../../utils/json'); jest.mock('../../ora', () => ({ ora: jest.fn(() => ({ start: jest.fn().mockReturnThis(), @@ -95,6 +97,33 @@ describe(Browse, () => { expect(openBrowserAsync).not.toHaveBeenCalled(); }); + it('forwards non-interactive mode to the context so it can run without prompts', async () => { + const command = createCommand(['--non-interactive']); + + await command.runAsync(); + + expect((command as any).getContextAsync).toHaveBeenCalledWith( + Browse, + expect.objectContaining({ nonInteractive: true }) + ); + }); + + it('prints the URL as JSON without opening a browser when --json is passed', async () => { + const command = createCommand(['build', '--json']); + + await command.runAsync(); + + expect(enableJsonOutput).toHaveBeenCalled(); + expect(printJsonOnlyOutput).toHaveBeenCalledWith({ + url: 'https://expo.dev/accounts/testuser/projects/testapp/builds', + }); + expect(openBrowserAsync).not.toHaveBeenCalled(); + expect((command as any).getContextAsync).toHaveBeenCalledWith( + Browse, + expect.objectContaining({ nonInteractive: true }) + ); + }); + it('fails the spinner when the browser cannot be opened', async () => { jest.mocked(openBrowserAsync).mockResolvedValue(false); const spinner = { diff --git a/packages/eas-cli/src/commands/browse.ts b/packages/eas-cli/src/commands/browse.ts index 4e4b0b0f83..16e8a5f186 100644 --- a/packages/eas-cli/src/commands/browse.ts +++ b/packages/eas-cli/src/commands/browse.ts @@ -3,13 +3,15 @@ import openBrowserAsync from 'better-opn'; import { getProjectPageUrl } from '../build/utils/url'; import EasCommand from '../commandUtils/EasCommand'; +import { + EasNonInteractiveAndJsonFlags, + resolveNonInteractiveAndJsonFlags, +} from '../commandUtils/flags'; import Log from '../log'; import { ora } from '../ora'; import { getOwnerAccountForProjectIdAsync } from '../project/projectUtils'; +import { enableJsonOutput, printJsonOnlyOutput } from '../utils/json'; -// Maps the page argument to its path on the project dashboard. Keys are the accepted -// argument values; multiple keys can point at the same page so both the EAS command name -// (e.g. `build`) and the website's wording (e.g. `builds`) work. const PROJECT_PAGES: Record = { build: 'builds', builds: 'builds', @@ -30,7 +32,8 @@ const PROJECT_PAGES: Record = { }; export default class Browse extends EasCommand { - static override description = 'open the project page in a web browser'; + static override description = + 'Transition from the terminal to the web browser to view and interact with your project on https://expo.dev'; static override args = { page: Args.string({ @@ -46,6 +49,7 @@ export default class Browse extends EasCommand { description: 'Print the URL instead of opening it in a web browser', default: false, }), + ...EasNonInteractiveAndJsonFlags, }; static override contextDefinition = { @@ -56,19 +60,27 @@ export default class Browse extends EasCommand { async runAsync(): Promise { const { args, flags } = await this.parse(Browse); const page = args.page ? PROJECT_PAGES[args.page] : null; + const { json: jsonFlag, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags); + if (jsonFlag) { + enableJsonOutput(); + } - // this command is interactive by nature (only really run by humans in a terminal) const { privateProjectConfig: { projectId, exp }, loggedIn: { graphqlClient }, } = await this.getContextAsync(Browse, { - nonInteractive: false, + nonInteractive, withServerSideEnvironment: null, }); const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId); const url = getProjectPageUrl(account.name, exp.slug, page); + if (jsonFlag) { + printJsonOnlyOutput({ url }); + return; + } + if (flags['no-browser']) { Log.log(url); return; diff --git a/packages/eas-cli/src/commands/open.ts b/packages/eas-cli/src/commands/open.ts index ae3ca53216..6ba2b1bcd0 100644 --- a/packages/eas-cli/src/commands/open.ts +++ b/packages/eas-cli/src/commands/open.ts @@ -4,6 +4,8 @@ import Browse from './browse'; export default class Open extends EasCommand { static override description = 'open the project page in a web browser (alias of `eas browse`)'; + static override hidden = true; + async runAsync(): Promise { await Browse.run([]); } From e62fd192079522da5b4c300db16eacafd8c82b34 Mon Sep 17 00:00:00 2001 From: Douglas Lowder Date: Mon, 13 Jul 2026 12:41:34 -0700 Subject: [PATCH 5/5] Fix CHANGELOG --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53a0d73a19..e65de4470d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ This is the log of notable changes to EAS CLI and related packages. ### ๐ŸŽ‰ New features +- [eas-cli] Add `eas browse` to open the project page on expo.dev in a web browser, with an optional page argument (e.g. `eas browse build`, `eas browse hosting`) for project subpages and a `--no-browser` flag to print the URL. `eas open` is now an alias of `eas browse`. ([#4003](https://github.com/expo/eas-cli/pull/4003) by [@jonsamp](https://github.com/jonsamp)) + ### ๐Ÿ› Bug fixes ### ๐Ÿงน Chores @@ -20,7 +22,6 @@ This is the log of notable changes to EAS CLI and related packages. ### ๐ŸŽ‰ New features -- [eas-cli] Add `eas browse` to open the project page on expo.dev in a web browser, with an optional page argument (e.g. `eas browse build`, `eas browse hosting`) for project subpages and a `--no-browser` flag to print the URL. `eas open` is now an alias of `eas browse`. ([#4003](https://github.com/expo/eas-cli/pull/4003) by [@jonsamp](https://github.com/jonsamp)) - [eas-cli] Improve `eas workflow:create`: add a `--template` flag, generate a placeholder workflow when a file name is passed, use shorter default file names (`build.yml`, `update.yml`, `deploy.yml`), configure EAS Build and EAS Update automatically when the chosen template requires them, set default app identifiers without prompting for the development build and deploy templates, install `expo-dev-client` during development build setup, and tighten the generated comments and next steps. ([#3943](https://github.com/expo/eas-cli/pull/3943) by [@jonsamp](https://github.com/jonsamp)) - [eas-cli] `eas integrations:posthog:dashboard` now opens PostHog via a signed-in link, skipping the login prompt. ([#3975](https://github.com/expo/eas-cli/pull/3975) by [@gwdp](https://github.com/gwdp)) - [build-tools] Add `eas/posthog_capture_event` workflow function to send PostHog events from workflow runs. ([#3934](https://github.com/expo/eas-cli/pull/3934) by [@gwdp](https://github.com/gwdp))