Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/private-simulator-preview-links.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'eas-cli': major
---

Default new simulator sessions to authenticated preview links and omit preview credentials from private output; use `simulator:start --public-link` for public sharing, while `simulator:get` follows the saved preference.
49 changes: 33 additions & 16 deletions packages/eas-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2836,7 +2836,7 @@ ALIASES

## `eas sim:get`

[EXPERIMENTAL] get info about a remote simulator session on EAS by its simulator session ID
[EXPERIMENTAL] get info about a remote simulator session on EAS by its simulator session ID. Preview links follow the session's saved sharing preference.

```
USAGE
Expand All @@ -2848,7 +2848,8 @@ FLAGS
--non-interactive Run the command in non-interactive mode.

DESCRIPTION
[EXPERIMENTAL] get info about a remote simulator session on EAS by its simulator session ID
[EXPERIMENTAL] get info about a remote simulator session on EAS by its simulator session ID. Preview links follow the
session's saved sharing preference.

ALIASES
$ eas sim:get
Expand Down Expand Up @@ -2889,16 +2890,16 @@ ALIASES

## `eas sim:start`

[EXPERIMENTAL] start a remote simulator session on EAS and get instructions to connect to it
[EXPERIMENTAL] start a remote simulator session on EAS and get instructions to connect to it. The default browser preview link requires sign-in.

```
USAGE
$ eas sim:start [-p android|ios] [--name <value>] [--tag <value>...] [--device <value>] [--build-id <value> |
--build-fingerprint <value> | --application-archive-url <value> | --expo-go] [--sdk-version <value>] [--launch-arg
<value>...] [--open-url <value>] [--type agent-device|appium|argent|web-preview-only] [--package-version <value>]
[--network-capture-field <value>... --network-capture] [--max-duration-minutes <value>] [--max-idle-time-minutes
<value>] [--egress-allow <value>... --egress local] [--force] [--out-config-type env|dotenv] [--json]
[--non-interactive]
<value>] [--egress-allow <value>... --egress local] [--force] [--out-config-type env|dotenv] [--public-link]
[--json] [--non-interactive]

FLAGS
-p, --platform=<option>
Expand Down Expand Up @@ -2981,6 +2982,10 @@ FLAGS
--package-version=<value>
Version of the package backing the simulator session (e.g. "0.1.3-alpha.3"). Defaults to "latest" when omitted.

--public-link
Enable the simulator preview Share button and print a shareable link containing an access token. Anyone with the
link can control the simulator. With --json, include previewApiUrl and its token for API clients when available.

--sdk-version=<value>
Expo SDK version used to select Expo Go when --expo-go is passed. Defaults to the current project SDK.

Expand All @@ -2994,7 +2999,8 @@ FLAGS
<options: agent-device|appium|argent|web-preview-only>

DESCRIPTION
[EXPERIMENTAL] start a remote simulator session on EAS and get instructions to connect to it
[EXPERIMENTAL] start a remote simulator session on EAS and get instructions to connect to it. The default browser
preview link requires sign-in.

ALIASES
$ eas simulator:start
Expand Down Expand Up @@ -3023,16 +3029,16 @@ ALIASES

## `eas simulator`

[EXPERIMENTAL] start a remote simulator session on EAS and get instructions to connect to it
[EXPERIMENTAL] start a remote simulator session on EAS and get instructions to connect to it. The default browser preview link requires sign-in.

```
USAGE
$ eas simulator [-p android|ios] [--name <value>] [--tag <value>...] [--device <value>] [--build-id <value> |
--build-fingerprint <value> | --application-archive-url <value> | --expo-go] [--sdk-version <value>] [--launch-arg
<value>...] [--open-url <value>] [--type agent-device|appium|argent|web-preview-only] [--package-version <value>]
[--network-capture-field <value>... --network-capture] [--max-duration-minutes <value>] [--max-idle-time-minutes
<value>] [--egress-allow <value>... --egress local] [--force] [--out-config-type env|dotenv] [--json]
[--non-interactive]
<value>] [--egress-allow <value>... --egress local] [--force] [--out-config-type env|dotenv] [--public-link]
[--json] [--non-interactive]

FLAGS
-p, --platform=<option>
Expand Down Expand Up @@ -3115,6 +3121,10 @@ FLAGS
--package-version=<value>
Version of the package backing the simulator session (e.g. "0.1.3-alpha.3"). Defaults to "latest" when omitted.

--public-link
Enable the simulator preview Share button and print a shareable link containing an access token. Anyone with the
link can control the simulator. With --json, include previewApiUrl and its token for API clients when available.

--sdk-version=<value>
Expo SDK version used to select Expo Go when --expo-go is passed. Defaults to the current project SDK.

Expand All @@ -3128,7 +3138,8 @@ FLAGS
<options: agent-device|appium|argent|web-preview-only>

DESCRIPTION
[EXPERIMENTAL] start a remote simulator session on EAS and get instructions to connect to it
[EXPERIMENTAL] start a remote simulator session on EAS and get instructions to connect to it. The default browser
preview link requires sign-in.

ALIASES
$ eas simulator:start
Expand Down Expand Up @@ -3223,7 +3234,7 @@ _See code: [packages/eas-cli/src/commands/simulator/exec.ts](https://github.com/

## `eas simulator:get`

[EXPERIMENTAL] get info about a remote simulator session on EAS by its simulator session ID
[EXPERIMENTAL] get info about a remote simulator session on EAS by its simulator session ID. Preview links follow the session's saved sharing preference.

```
USAGE
Expand All @@ -3235,7 +3246,8 @@ FLAGS
--non-interactive Run the command in non-interactive mode.

DESCRIPTION
[EXPERIMENTAL] get info about a remote simulator session on EAS by its simulator session ID
[EXPERIMENTAL] get info about a remote simulator session on EAS by its simulator session ID. Preview links follow the
session's saved sharing preference.

ALIASES
$ eas sim:get
Expand Down Expand Up @@ -3280,16 +3292,16 @@ _See code: [packages/eas-cli/src/commands/simulator/list.ts](https://github.com/

## `eas simulator:start`

[EXPERIMENTAL] start a remote simulator session on EAS and get instructions to connect to it
[EXPERIMENTAL] start a remote simulator session on EAS and get instructions to connect to it. The default browser preview link requires sign-in.

```
USAGE
$ eas simulator:start [-p android|ios] [--name <value>] [--tag <value>...] [--device <value>] [--build-id <value> |
--build-fingerprint <value> | --application-archive-url <value> | --expo-go] [--sdk-version <value>] [--launch-arg
<value>...] [--open-url <value>] [--type agent-device|appium|argent|web-preview-only] [--package-version <value>]
[--network-capture-field <value>... --network-capture] [--max-duration-minutes <value>] [--max-idle-time-minutes
<value>] [--egress-allow <value>... --egress local] [--force] [--out-config-type env|dotenv] [--json]
[--non-interactive]
<value>] [--egress-allow <value>... --egress local] [--force] [--out-config-type env|dotenv] [--public-link]
[--json] [--non-interactive]

FLAGS
-p, --platform=<option>
Expand Down Expand Up @@ -3372,6 +3384,10 @@ FLAGS
--package-version=<value>
Version of the package backing the simulator session (e.g. "0.1.3-alpha.3"). Defaults to "latest" when omitted.

--public-link
Enable the simulator preview Share button and print a shareable link containing an access token. Anyone with the
link can control the simulator. With --json, include previewApiUrl and its token for API clients when available.

--sdk-version=<value>
Expo SDK version used to select Expo Go when --expo-go is passed. Defaults to the current project SDK.

Expand All @@ -3385,7 +3401,8 @@ FLAGS
<options: agent-device|appium|argent|web-preview-only>

DESCRIPTION
[EXPERIMENTAL] start a remote simulator session on EAS and get instructions to connect to it
[EXPERIMENTAL] start a remote simulator session on EAS and get instructions to connect to it. The default browser
preview link requires sign-in.

ALIASES
$ eas simulator:start
Expand Down
28 changes: 28 additions & 0 deletions packages/eas-cli/graphql.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 14 additions & 1 deletion packages/eas-cli/src/build/utils/__tests__/url.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { getProjectDashboardUrl, getProjectPageUrl, getSubmissionUrl } from '../url';
import {
getDeviceRunSessionPreviewUrl,
getProjectDashboardUrl,
getProjectPageUrl,
getSubmissionUrl,
} from '../url';

describe(getProjectPageUrl, () => {
it('builds the project dashboard URL when no page is provided', () => {
Expand Down Expand Up @@ -41,3 +46,11 @@ describe(getSubmissionUrl, () => {
);
});
});

describe(getDeviceRunSessionPreviewUrl, () => {
it('builds the authenticated preview route with encoded path segments', () => {
expect(getDeviceRunSessionPreviewUrl('my org', 'my app', 'session/id')).toBe(
'https://expo.dev/accounts/my%20org/projects/my%20app/simulator-sessions/session%2Fid/preview'
);
});
});
8 changes: 8 additions & 0 deletions packages/eas-cli/src/build/utils/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ export function getDeviceRunSessionUrl(
).toString();
}

export function getDeviceRunSessionPreviewUrl(
accountName: string,
projectName: string,
deviceRunSessionId: string
): string {
return `${getDeviceRunSessionUrl(accountName, projectName, deviceRunSessionId)}/preview`;
}

/**
* @deprecated Links to the raw job-run page; prefer a higher-level URL (e.g. the workflow run
* or the feature-specific dashboard) that gives users more context. Use this only for internal
Expand Down
109 changes: 108 additions & 1 deletion packages/eas-cli/src/commands/simulator/__tests__/get.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ function makeDeviceRunSession(overrides: Partial<DeviceRunSessionById> = {}): De
id: 'session-123',
name: null,
tags: [],
publicPreview: false,
status: DeviceRunSessionStatus.InProgress,
type: DeviceRunSessionType.AgentDevice,
platform: AppPlatform.Ios,
Expand Down Expand Up @@ -154,11 +155,78 @@ describe(SimulatorGet, () => {
updatedAt: '2025-01-01T00:01:00.000Z',
deviceRunSessionUrl:
'https://expo.dev/accounts/testuser/projects/testapp/simulator-sessions/session-123',
remoteConfig: session.remoteConfig,
remoteConfig: {
__typename: 'ServeSimRunSessionRemoteConfig',
previewUrl:
'https://expo.dev/accounts/testuser/projects/testapp/simulator-sessions/session-123/preview',
previewApiUrl: null,
},
artifacts: session.artifacts,
});
});

it.each([false, true])(
'uses the saved publicPreview=%s preference for JSON and human previews',
async publicPreview => {
const publicUrl = 'https://expo.dev/simulator-preview/preview-123';
const privateUrl =
'https://expo.dev/accounts/testuser/projects/testapp/simulator-sessions/session-123/preview';
mockByIdAsync.mockResolvedValue(
makeDeviceRunSession({
publicPreview,
type: DeviceRunSessionType.WebPreviewOnly,
remoteConfig: {
__typename: 'WebPreviewOnlyRunSessionRemoteConfig',
previewUrl: publicUrl,
previewToken: 'preview-secret',
previewApiUrl: 'https://preview-api.example.test',
},
})
);
const expectedUrl = publicPreview ? `${publicUrl}#token=preview-secret` : privateUrl;
const expectedApiUrl = publicPreview
? 'https://preview-api.example.test/?token=preview-secret'
: null;

const { command: jsonCommand } = createCommand(['--id', 'session-123', '--json']);
await jsonCommand.runAsync();
expect(mockPrintJsonOnlyOutput).toHaveBeenCalledWith(
expect.objectContaining({
remoteConfig: expect.objectContaining({
previewUrl: expectedUrl,
previewApiUrl: expectedApiUrl,
}),
})
);
if (!publicPreview) {
expect(JSON.stringify(mockPrintJsonOnlyOutput.mock.calls[0]?.[0])).not.toContain(
'preview-secret'
);
}

jest.clearAllMocks();
mockLoadSimulatorEnvironmentVariablesAsync.mockResolvedValue();
mockByIdAsync.mockResolvedValue(
makeDeviceRunSession({
publicPreview,
type: DeviceRunSessionType.WebPreviewOnly,
remoteConfig: {
__typename: 'WebPreviewOnlyRunSessionRemoteConfig',
previewUrl: publicUrl,
previewToken: 'preview-secret',
previewApiUrl: 'https://preview-api.example.test',
},
})
);
const { command: humanCommand } = createCommand(['--id', 'session-123']);
await humanCommand.runAsync();
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining(expectedUrl));
if (!publicPreview) {
expect(JSON.stringify(jest.mocked(Log.log).mock.calls)).not.toContain('preview-secret');
}
}
);

it('includes the session name in JSON and human output when the session is named', async () => {
mockByIdAsync.mockResolvedValue(makeDeviceRunSession({ name: 'Checkout regression' }));

Expand All @@ -179,6 +247,45 @@ describe(SimulatorGet, () => {
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('Checkout regression'));
});

it.each([false, true])(
'uses saved publicPreview=%s for Android while preserving controller access',
async publicPreview => {
mockByIdAsync.mockResolvedValue(
makeDeviceRunSession({
platform: AppPlatform.Android,
publicPreview,
remoteConfig: {
__typename: 'AgentDeviceRunSessionRemoteConfig',
agentDeviceRemoteSessionUrl: 'https://agent.example.test',
agentDeviceRemoteSessionToken: 'controller-secret',
webPreviewUrl: 'https://expo.dev/simulator-preview/android-123',
webPreviewToken: 'preview-secret',
previewApiUrl: 'https://preview-api.example.test',
},
})
);

const { command } = createCommand(['--id', 'session-123', '--json']);
await command.runAsync();

expect(mockPrintJsonOnlyOutput).toHaveBeenCalledWith(
expect.objectContaining({
remoteConfig: {
__typename: 'AgentDeviceRunSessionRemoteConfig',
agentDeviceRemoteSessionUrl: 'https://agent.example.test',
agentDeviceRemoteSessionToken: 'controller-secret',
webPreviewUrl: publicPreview
? 'https://expo.dev/simulator-preview/android-123#token=preview-secret'
: 'https://expo.dev/accounts/testuser/projects/testapp/simulator-sessions/session-123/preview',
previewApiUrl: publicPreview
? 'https://preview-api.example.test/?token=preview-secret'
: null,
},
})
);
}
);

it(`uses ${EAS_SIMULATOR_SESSION_ID} from simulator env when --id is not passed`, async () => {
const previousDeviceRunSessionId = process.env[EAS_SIMULATOR_SESSION_ID];
const session = makeDeviceRunSession({ id: 'session-from-env' });
Expand Down
Loading
Loading