eas-cli 18.10.0 → 18.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +107 -102
  2. package/build/build/runBuildAndSubmit.d.ts +3 -1
  3. package/build/build/runBuildAndSubmit.js +12 -4
  4. package/build/build/utils/repository.js +8 -0
  5. package/build/build/validateLockfile.d.ts +1 -0
  6. package/build/build/validateLockfile.js +37 -0
  7. package/build/commandUtils/buildFlags.d.ts +1 -0
  8. package/build/commandUtils/buildFlags.js +18 -0
  9. package/build/commandUtils/convex.d.ts +1 -0
  10. package/build/commandUtils/convex.js +8 -2
  11. package/build/commands/build/dev.d.ts +1 -0
  12. package/build/commands/build/dev.js +12 -2
  13. package/build/commands/build/run.d.ts +1 -0
  14. package/build/commands/build/run.js +12 -3
  15. package/build/commands/integrations/convex/connect.js +12 -4
  16. package/build/commands/integrations/convex/team/invite.js +6 -1
  17. package/build/commands/observe/events.js +12 -24
  18. package/build/commands/observe/logs.d.ts +29 -0
  19. package/build/commands/observe/logs.js +163 -0
  20. package/build/commands/observe/metrics.js +11 -19
  21. package/build/commands/observe/versions.js +11 -19
  22. package/build/commands/simulator/start.js +84 -5
  23. package/build/commands/simulator/stop.js +1 -1
  24. package/build/graphql/generated.d.ts +180 -6
  25. package/build/graphql/generated.js +16 -3
  26. package/build/graphql/mutations/DeviceRunSessionMutation.d.ts +2 -2
  27. package/build/graphql/mutations/DeviceRunSessionMutation.js +4 -4
  28. package/build/graphql/queries/ObserveQuery.d.ts +21 -1
  29. package/build/graphql/queries/ObserveQuery.js +80 -0
  30. package/build/graphql/types/ConvexTeamConnection.d.ts +1 -1
  31. package/build/graphql/types/ConvexTeamConnection.js +1 -0
  32. package/build/graphql/types/Observe.d.ts +1 -0
  33. package/build/graphql/types/Observe.js +26 -1
  34. package/build/observe/fetchCustomEvents.d.ts +19 -0
  35. package/build/observe/fetchCustomEvents.js +21 -0
  36. package/build/observe/formatCustomEvents.d.ts +70 -0
  37. package/build/observe/formatCustomEvents.js +140 -0
  38. package/build/observe/formatEvents.js +5 -34
  39. package/build/observe/formatMetrics.js +2 -7
  40. package/build/observe/formatUtils.d.ts +27 -0
  41. package/build/observe/formatUtils.js +64 -0
  42. package/build/observe/formatVersions.js +2 -9
  43. package/build/observe/platforms.d.ts +21 -0
  44. package/build/observe/platforms.js +48 -0
  45. package/build/observe/resolveProjectContext.d.ts +22 -0
  46. package/build/observe/resolveProjectContext.js +21 -0
  47. package/build/run/ios/run.d.ts +2 -1
  48. package/build/run/ios/run.js +6 -2
  49. package/build/run/ios/simulator.d.ts +4 -1
  50. package/build/run/ios/simulator.js +14 -2
  51. package/build/run/run.d.ts +2 -1
  52. package/build/run/run.js +2 -2
  53. package/oclif.manifest.json +955 -762
  54. package/package.json +2 -2
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const core_1 = require("@oclif/core");
5
+ const EasCommand_1 = tslib_1.__importDefault(require("../../commandUtils/EasCommand"));
6
+ const flags_1 = require("../../commandUtils/flags");
7
+ const pagination_1 = require("../../commandUtils/pagination");
8
+ const log_1 = tslib_1.__importDefault(require("../../log"));
9
+ const ObserveQuery_1 = require("../../graphql/queries/ObserveQuery");
10
+ const fetchCustomEvents_1 = require("../../observe/fetchCustomEvents");
11
+ const formatCustomEvents_1 = require("../../observe/formatCustomEvents");
12
+ const platforms_1 = require("../../observe/platforms");
13
+ const resolveProjectContext_1 = require("../../observe/resolveProjectContext");
14
+ const startAndEndTime_1 = require("../../observe/startAndEndTime");
15
+ const json_1 = require("../../utils/json");
16
+ const DEFAULT_EVENTS_LIMIT = 10;
17
+ class ObserveLogs extends EasCommand_1.default {
18
+ static hidden = true;
19
+ static description = 'display individual custom events (logs) emitted by the app, filtered by the event name in the argument. With no arguments, a list of the available event names and associated event counts is returned.';
20
+ static args = {
21
+ eventName: core_1.Args.string({
22
+ description: 'Custom event name to filter by',
23
+ required: false,
24
+ }),
25
+ };
26
+ static flags = {
27
+ platform: core_1.Flags.option({
28
+ description: 'Filter by platform',
29
+ options: platforms_1.allowedPlatformFlagValues,
30
+ })(),
31
+ after: core_1.Flags.string({
32
+ description: 'Cursor for pagination. Use the endCursor from a previous query to fetch the next page.',
33
+ }),
34
+ limit: (0, pagination_1.getLimitFlagWithCustomValues)({
35
+ defaultTo: DEFAULT_EVENTS_LIMIT,
36
+ limit: 100,
37
+ }),
38
+ start: core_1.Flags.string({
39
+ description: 'Start of time range (ISO date)',
40
+ exclusive: ['days'],
41
+ }),
42
+ end: core_1.Flags.string({
43
+ description: 'End of time range (ISO date)',
44
+ exclusive: ['days'],
45
+ }),
46
+ days: core_1.Flags.integer({
47
+ description: 'Show events from the last N days (mutually exclusive with --start/--end)',
48
+ min: 1,
49
+ exclusive: ['start', 'end'],
50
+ }),
51
+ 'app-version': core_1.Flags.string({
52
+ description: 'Filter by app version',
53
+ }),
54
+ 'update-id': core_1.Flags.string({
55
+ description: 'Filter by EAS update ID',
56
+ }),
57
+ 'session-id': core_1.Flags.string({
58
+ description: 'Filter by session ID',
59
+ }),
60
+ 'all-events': core_1.Flags.boolean({
61
+ description: 'When no event name argument is provided, list all events across all event names instead of a summary of event names + counts.',
62
+ default: false,
63
+ }),
64
+ 'project-id': core_1.Flags.string({
65
+ description: 'EAS project ID (defaults to the project ID of the current directory)',
66
+ }),
67
+ ...flags_1.EasNonInteractiveAndJsonFlags,
68
+ };
69
+ static contextDefinition = {
70
+ ...this.ContextOptions.ProjectId,
71
+ ...this.ContextOptions.LoggedIn,
72
+ };
73
+ static loggedInOnlyContextDefinition = {
74
+ ...this.ContextOptions.LoggedIn,
75
+ };
76
+ async runAsync() {
77
+ const { flags, args } = await this.parse(ObserveLogs);
78
+ if (args.eventName && flags['all-events']) {
79
+ throw new Error('--all-events cannot be combined with an event name argument. Pass an event name to filter by it, or pass --all-events to list all events across all event names.');
80
+ }
81
+ const { projectId, graphqlClient } = await (0, resolveProjectContext_1.resolveObserveCommandContextAsync)({
82
+ command: this,
83
+ commandClass: ObserveLogs,
84
+ loggedInOnlyContextDefinition: ObserveLogs.loggedInOnlyContextDefinition,
85
+ projectIdOverride: flags['project-id'],
86
+ nonInteractive: flags['non-interactive'],
87
+ });
88
+ if (flags.json) {
89
+ (0, json_1.enableJsonOutput)();
90
+ }
91
+ else {
92
+ log_1.default.warn('EAS Observe is in preview and subject to breaking changes.');
93
+ }
94
+ const { daysBack, startTime, endTime } = (0, startAndEndTime_1.resolveTimeRange)(flags);
95
+ const platform = (0, platforms_1.appObservePlatformFromFlag)(flags.platform);
96
+ if (!args.eventName && !flags['all-events']) {
97
+ const { names, isTruncated } = await ObserveQuery_1.ObserveQuery.customEventNamesAsync(graphqlClient, {
98
+ appId: projectId,
99
+ startTime,
100
+ endTime,
101
+ platform,
102
+ });
103
+ if (flags.json) {
104
+ (0, json_1.printJsonOnlyOutput)((0, formatCustomEvents_1.buildObserveCustomEventNamesJson)(names, isTruncated));
105
+ }
106
+ else {
107
+ log_1.default.addNewLineIfNone();
108
+ log_1.default.log((0, formatCustomEvents_1.buildObserveCustomEventNamesTable)(names, {
109
+ daysBack,
110
+ startTime,
111
+ endTime,
112
+ isTruncated,
113
+ }));
114
+ }
115
+ return;
116
+ }
117
+ const { events, pageInfo } = await (0, fetchCustomEvents_1.fetchObserveCustomEventsAsync)(graphqlClient, projectId, {
118
+ eventName: args.eventName,
119
+ limit: flags.limit ?? DEFAULT_EVENTS_LIMIT,
120
+ ...(flags.after && { after: flags.after }),
121
+ startTime,
122
+ endTime,
123
+ platform,
124
+ appVersion: flags['app-version'],
125
+ updateId: flags['update-id'],
126
+ sessionId: flags['session-id'],
127
+ });
128
+ if (args.eventName && events.length === 0) {
129
+ const { names, isTruncated } = await ObserveQuery_1.ObserveQuery.customEventNamesAsync(graphqlClient, {
130
+ appId: projectId,
131
+ startTime,
132
+ endTime,
133
+ platform,
134
+ });
135
+ if (flags.json) {
136
+ (0, json_1.printJsonOnlyOutput)((0, formatCustomEvents_1.buildObserveCustomEventsEmptyWithSuggestionsJson)(args.eventName, names, isTruncated));
137
+ }
138
+ else {
139
+ log_1.default.addNewLineIfNone();
140
+ log_1.default.log((0, formatCustomEvents_1.buildObserveCustomEventsEmptyWithSuggestionsTable)(args.eventName, names, {
141
+ daysBack,
142
+ startTime,
143
+ endTime,
144
+ isTruncated,
145
+ }));
146
+ }
147
+ return;
148
+ }
149
+ if (flags.json) {
150
+ (0, json_1.printJsonOnlyOutput)((0, formatCustomEvents_1.buildObserveCustomEventsJson)(events, pageInfo));
151
+ }
152
+ else {
153
+ log_1.default.addNewLineIfNone();
154
+ log_1.default.log((0, formatCustomEvents_1.buildObserveCustomEventsTable)(events, pageInfo, {
155
+ eventName: args.eventName,
156
+ daysBack,
157
+ startTime,
158
+ endTime,
159
+ }));
160
+ }
161
+ }
162
+ }
163
+ exports.default = ObserveLogs;
@@ -4,11 +4,12 @@ const tslib_1 = require("tslib");
4
4
  const core_1 = require("@oclif/core");
5
5
  const EasCommand_1 = tslib_1.__importDefault(require("../../commandUtils/EasCommand"));
6
6
  const flags_1 = require("../../commandUtils/flags");
7
- const generated_1 = require("../../graphql/generated");
8
7
  const log_1 = tslib_1.__importDefault(require("../../log"));
9
8
  const fetchMetrics_1 = require("../../observe/fetchMetrics");
10
9
  const formatMetrics_1 = require("../../observe/formatMetrics");
11
10
  const metricNames_1 = require("../../observe/metricNames");
11
+ const platforms_1 = require("../../observe/platforms");
12
+ const resolveProjectContext_1 = require("../../observe/resolveProjectContext");
12
13
  const startAndEndTime_1 = require("../../observe/startAndEndTime");
13
14
  const json_1 = require("../../utils/json");
14
15
  const DEFAULT_METRICS = [
@@ -35,7 +36,7 @@ class ObserveMetrics extends EasCommand_1.default {
35
36
  static flags = {
36
37
  platform: core_1.Flags.option({
37
38
  description: 'Filter by platform',
38
- options: Object.values(generated_1.AppObservePlatform).map(s => s.toLowerCase()),
39
+ options: platforms_1.allowedPlatformFlagValues,
39
40
  })(),
40
41
  metric: core_1.Flags.option({
41
42
  description: 'Metric name to display (can be specified multiple times).',
@@ -74,20 +75,13 @@ class ObserveMetrics extends EasCommand_1.default {
74
75
  };
75
76
  async runAsync() {
76
77
  const { flags } = await this.parse(ObserveMetrics);
77
- let projectId;
78
- let graphqlClient;
79
- if (flags['project-id']) {
80
- projectId = flags['project-id'];
81
- const ctx = await this.getContextAsync({ contextDefinition: ObserveMetrics.loggedInOnlyContextDefinition }, { nonInteractive: flags['non-interactive'] });
82
- graphqlClient = ctx.loggedIn.graphqlClient;
83
- }
84
- else {
85
- const ctx = await this.getContextAsync(ObserveMetrics, {
86
- nonInteractive: flags['non-interactive'],
87
- });
88
- projectId = ctx.projectId;
89
- graphqlClient = ctx.loggedIn.graphqlClient;
90
- }
78
+ const { projectId, graphqlClient } = await (0, resolveProjectContext_1.resolveObserveCommandContextAsync)({
79
+ command: this,
80
+ commandClass: ObserveMetrics,
81
+ loggedInOnlyContextDefinition: ObserveMetrics.loggedInOnlyContextDefinition,
82
+ projectIdOverride: flags['project-id'],
83
+ nonInteractive: flags['non-interactive'],
84
+ });
91
85
  if (flags.json) {
92
86
  (0, json_1.enableJsonOutput)();
93
87
  }
@@ -98,9 +92,7 @@ class ObserveMetrics extends EasCommand_1.default {
98
92
  ? flags.metric.map(metricNames_1.resolveMetricName)
99
93
  : DEFAULT_METRICS;
100
94
  const { daysBack, startTime, endTime } = (0, startAndEndTime_1.resolveTimeRange)(flags);
101
- const platforms = flags.platform
102
- ? [flags.platform === 'android' ? generated_1.AppPlatform.Android : generated_1.AppPlatform.Ios]
103
- : [generated_1.AppPlatform.Android, generated_1.AppPlatform.Ios];
95
+ const platforms = (0, platforms_1.appPlatformsFromFlag)(flags.platform);
104
96
  const { metricsMap, buildNumbersMap, updateIdsMap, totalEventCounts } = await (0, fetchMetrics_1.fetchObserveMetricsAsync)(graphqlClient, projectId, metricNames, platforms, startTime, endTime);
105
97
  const argumentsStat = flags.stat?.length
106
98
  ? Array.from(new Set(flags.stat.map(formatMetrics_1.resolveStatKey)))
@@ -4,10 +4,11 @@ const tslib_1 = require("tslib");
4
4
  const core_1 = require("@oclif/core");
5
5
  const EasCommand_1 = tslib_1.__importDefault(require("../../commandUtils/EasCommand"));
6
6
  const flags_1 = require("../../commandUtils/flags");
7
- const generated_1 = require("../../graphql/generated");
8
7
  const log_1 = tslib_1.__importDefault(require("../../log"));
9
8
  const fetchVersions_1 = require("../../observe/fetchVersions");
10
9
  const formatVersions_1 = require("../../observe/formatVersions");
10
+ const platforms_1 = require("../../observe/platforms");
11
+ const resolveProjectContext_1 = require("../../observe/resolveProjectContext");
11
12
  const startAndEndTime_1 = require("../../observe/startAndEndTime");
12
13
  const json_1 = require("../../utils/json");
13
14
  class ObserveVersions extends EasCommand_1.default {
@@ -16,7 +17,7 @@ class ObserveVersions extends EasCommand_1.default {
16
17
  static flags = {
17
18
  platform: core_1.Flags.option({
18
19
  description: 'Filter by platform',
19
- options: Object.values(generated_1.AppObservePlatform).map(s => s.toLowerCase()),
20
+ options: platforms_1.allowedPlatformFlagValues,
20
21
  })(),
21
22
  start: core_1.Flags.string({
22
23
  description: 'Start of time range (ISO date)',
@@ -45,20 +46,13 @@ class ObserveVersions extends EasCommand_1.default {
45
46
  };
46
47
  async runAsync() {
47
48
  const { flags } = await this.parse(ObserveVersions);
48
- let projectId;
49
- let graphqlClient;
50
- if (flags['project-id']) {
51
- projectId = flags['project-id'];
52
- const ctx = await this.getContextAsync({ contextDefinition: ObserveVersions.loggedInOnlyContextDefinition }, { nonInteractive: flags['non-interactive'] });
53
- graphqlClient = ctx.loggedIn.graphqlClient;
54
- }
55
- else {
56
- const ctx = await this.getContextAsync(ObserveVersions, {
57
- nonInteractive: flags['non-interactive'],
58
- });
59
- projectId = ctx.projectId;
60
- graphqlClient = ctx.loggedIn.graphqlClient;
61
- }
49
+ const { projectId, graphqlClient } = await (0, resolveProjectContext_1.resolveObserveCommandContextAsync)({
50
+ command: this,
51
+ commandClass: ObserveVersions,
52
+ loggedInOnlyContextDefinition: ObserveVersions.loggedInOnlyContextDefinition,
53
+ projectIdOverride: flags['project-id'],
54
+ nonInteractive: flags['non-interactive'],
55
+ });
62
56
  if (flags.json) {
63
57
  (0, json_1.enableJsonOutput)();
64
58
  }
@@ -66,9 +60,7 @@ class ObserveVersions extends EasCommand_1.default {
66
60
  log_1.default.warn('EAS Observe is in preview and subject to breaking changes.');
67
61
  }
68
62
  const { startTime, endTime } = (0, startAndEndTime_1.resolveTimeRange)(flags);
69
- const platforms = flags.platform
70
- ? [flags.platform === 'android' ? generated_1.AppPlatform.Android : generated_1.AppPlatform.Ios]
71
- : [generated_1.AppPlatform.Android, generated_1.AppPlatform.Ios];
63
+ const platforms = (0, platforms_1.appPlatformsFromFlag)(flags.platform);
72
64
  const results = await (0, fetchVersions_1.fetchObserveVersionsAsync)(graphqlClient, projectId, platforms, startTime, endTime);
73
65
  if (flags.json) {
74
66
  (0, json_1.printJsonOnlyOutput)((0, formatVersions_1.buildObserveVersionsJson)(results));
@@ -51,6 +51,7 @@ class SimulatorStart extends EasCommand_1.default {
51
51
  const platform = flags.platform === 'android' ? generated_1.AppPlatform.Android : generated_1.AppPlatform.Ios;
52
52
  const createSpinner = (0, ora_1.ora)('🚀 Creating device run session').start();
53
53
  let deviceRunSessionId;
54
+ let jobRunUrl;
54
55
  try {
55
56
  const session = await DeviceRunSessionMutation_1.DeviceRunSessionMutation.createDeviceRunSessionAsync(graphqlClient, {
56
57
  appId: projectId,
@@ -60,7 +61,7 @@ class SimulatorStart extends EasCommand_1.default {
60
61
  });
61
62
  deviceRunSessionId = session.id;
62
63
  const jobRunId = (0, nullthrows_1.default)(session.turtleJobRun?.id, 'Expected device run session to start');
63
- const jobRunUrl = (0, url_1.getBareJobRunUrl)(session.app.ownerAccount.name, session.app.slug, jobRunId);
64
+ jobRunUrl = (0, url_1.getBareJobRunUrl)(session.app.ownerAccount.name, session.app.slug, jobRunId);
64
65
  createSpinner.succeed(`Device run session created (id: ${deviceRunSessionId}) ${(0, log_1.link)(jobRunUrl)}`);
65
66
  }
66
67
  catch (err) {
@@ -76,13 +77,13 @@ class SimulatorStart extends EasCommand_1.default {
76
77
  const session = await DeviceRunSessionQuery_1.DeviceRunSessionQuery.byIdAsync(graphqlClient, deviceRunSessionId);
77
78
  if (session.status === generated_1.DeviceRunSessionStatus.Errored ||
78
79
  session.status === generated_1.DeviceRunSessionStatus.Stopped) {
79
- throw new Error(`Device run session ${deviceRunSessionId} ${session.status.toLowerCase()} before the ${flags.type} daemon was ready.`);
80
+ throw new Error(`Device run session ${deviceRunSessionId} ${session.status.toLowerCase()} before the ${flags.type} daemon was ready. ${(0, log_1.link)(jobRunUrl)}`);
80
81
  }
81
82
  const jobRunStatus = session.turtleJobRun?.status;
82
83
  if (jobRunStatus === generated_1.JobRunStatus.Errored ||
83
84
  jobRunStatus === generated_1.JobRunStatus.Canceled ||
84
85
  jobRunStatus === generated_1.JobRunStatus.Finished) {
85
- throw new Error(`Turtle job run for device run session ${deviceRunSessionId} ${jobRunStatus.toLowerCase()} before the ${flags.type} daemon was ready.`);
86
+ throw new Error(`Turtle job run for device run session ${deviceRunSessionId} ${jobRunStatus.toLowerCase()} before the ${flags.type} daemon was ready. ${(0, log_1.link)(jobRunUrl)}`);
86
87
  }
87
88
  const logMessages = await fetchLogMessagesAsync(session.turtleJobRun?.logFileUrls ?? []);
88
89
  result = checkReadiness(logMessages);
@@ -95,21 +96,99 @@ class SimulatorStart extends EasCommand_1.default {
95
96
  }
96
97
  catch (err) {
97
98
  pollSpinner.fail(`Failed while polling for ${flags.type} daemon logs`);
99
+ await ensureDeviceRunSessionStoppedSafelyAsync(graphqlClient, deviceRunSessionId);
98
100
  throw err;
99
101
  }
100
102
  if (!result.ready) {
101
103
  pollSpinner.fail(`Timed out waiting for ${flags.type} daemon to start`);
102
- throw new Error(`Timed out after ${Math.round(POLL_TIMEOUT_MS / 1000)}s waiting for ${flags.type} daemon to start.`);
104
+ await ensureDeviceRunSessionStoppedSafelyAsync(graphqlClient, deviceRunSessionId);
105
+ throw new Error(`Timed out after ${Math.round(POLL_TIMEOUT_MS / 1000)}s waiting for ${flags.type} daemon to start. ${(0, log_1.link)(jobRunUrl)}`);
103
106
  }
104
107
  log_1.default.newLine();
105
108
  log_1.default.log(`🔑 Run the following in your shell to attach to ${flags.type}:`);
106
109
  log_1.default.newLine();
107
110
  log_1.default.log(result.message);
108
111
  log_1.default.newLine();
109
- log_1.default.log(`When you are done, stop the session with: eas simulator:stop --id ${deviceRunSessionId}`);
112
+ if (flags['non-interactive']) {
113
+ log_1.default.log(`When you are done, stop the session with: eas simulator:stop --id ${deviceRunSessionId}`);
114
+ return;
115
+ }
116
+ await waitForSessionEndOrInterruptAsync({
117
+ graphqlClient,
118
+ deviceRunSessionId,
119
+ jobRunUrl,
120
+ });
110
121
  }
111
122
  }
112
123
  exports.default = SimulatorStart;
124
+ async function waitForSessionEndOrInterruptAsync({ graphqlClient, deviceRunSessionId, jobRunUrl, }) {
125
+ const spinner = (0, ora_1.ora)(`Device run session active — press Ctrl+C to stop, or run \`eas simulator:stop --id ${deviceRunSessionId}\` from another shell`).start();
126
+ const abortController = new AbortController();
127
+ const { signal } = abortController;
128
+ const abortPromise = new Promise(resolve => {
129
+ signal.addEventListener('abort', () => {
130
+ resolve();
131
+ }, { once: true });
132
+ });
133
+ const sigintHandler = () => {
134
+ if (signal.aborted) {
135
+ // Force exit on a second Ctrl+C in case cleanup is hanging. The session may still be
136
+ // running on EAS, so tell the user how to make sure it gets terminated.
137
+ spinner.fail(`Aborted before the device run session could be stopped. Run \`eas simulator:stop --id ${deviceRunSessionId}\` to terminate it and avoid unexpected charges.`);
138
+ process.exit(130);
139
+ }
140
+ abortController.abort();
141
+ };
142
+ process.on('SIGINT', sigintHandler);
143
+ try {
144
+ while (!signal.aborted) {
145
+ let session;
146
+ try {
147
+ session = await DeviceRunSessionQuery_1.DeviceRunSessionQuery.byIdAsync(graphqlClient, deviceRunSessionId);
148
+ }
149
+ catch (err) {
150
+ log_1.default.debug(`Failed to poll device run session: ${err instanceof Error ? err.message : String(err)}`);
151
+ await Promise.race([(0, promise_1.sleepAsync)(POLL_INTERVAL_MS), abortPromise]);
152
+ continue;
153
+ }
154
+ const jobRunStatus = session.turtleJobRun?.status;
155
+ if (session.status === generated_1.DeviceRunSessionStatus.Errored ||
156
+ jobRunStatus === generated_1.JobRunStatus.Errored) {
157
+ spinner.fail(`Device run session errored. ${(0, log_1.link)(jobRunUrl)}`);
158
+ throw new Error(`Device run session ${deviceRunSessionId} errored.`);
159
+ }
160
+ if (session.status === generated_1.DeviceRunSessionStatus.Stopped ||
161
+ jobRunStatus === generated_1.JobRunStatus.Canceled ||
162
+ jobRunStatus === generated_1.JobRunStatus.Finished) {
163
+ spinner.succeed(`Device run session ended. ${(0, log_1.link)(jobRunUrl)}`);
164
+ return;
165
+ }
166
+ await Promise.race([(0, promise_1.sleepAsync)(POLL_INTERVAL_MS), abortPromise]);
167
+ }
168
+ spinner.text = 'Stopping device run session...';
169
+ const stopped = await ensureDeviceRunSessionStoppedSafelyAsync(graphqlClient, deviceRunSessionId);
170
+ if (stopped) {
171
+ spinner.succeed('Device run session stopped');
172
+ }
173
+ else {
174
+ spinner.fail(`Could not confirm the device run session was stopped. Run \`eas simulator:stop --id ${deviceRunSessionId}\` to terminate it and avoid unexpected charges.`);
175
+ }
176
+ }
177
+ finally {
178
+ process.removeListener('SIGINT', sigintHandler);
179
+ }
180
+ }
181
+ async function ensureDeviceRunSessionStoppedSafelyAsync(graphqlClient, deviceRunSessionId) {
182
+ try {
183
+ await DeviceRunSessionMutation_1.DeviceRunSessionMutation.ensureDeviceRunSessionStoppedAsync(graphqlClient, deviceRunSessionId);
184
+ return true;
185
+ }
186
+ catch (err) {
187
+ // Cleanup is best-effort; surface the failure but don't mask the original error.
188
+ log_1.default.warn(`Failed to stop device run session ${deviceRunSessionId}: ${err instanceof Error ? err.message : String(err)}`);
189
+ return false;
190
+ }
191
+ }
113
192
  function getReadinessCheckerForType(type) {
114
193
  switch (type) {
115
194
  case DEVICE_RUN_SESSION_TYPE_FLAG_VALUES[generated_1.DeviceRunSessionType.AgentDevice]:
@@ -26,7 +26,7 @@ class SimulatorStop extends EasCommand_1.default {
26
26
  });
27
27
  const stopSpinner = (0, ora_1.ora)(`🛑 Stopping device run session ${flags.id}`).start();
28
28
  try {
29
- const session = await DeviceRunSessionMutation_1.DeviceRunSessionMutation.stopDeviceRunSessionAsync(graphqlClient, flags.id);
29
+ const session = await DeviceRunSessionMutation_1.DeviceRunSessionMutation.ensureDeviceRunSessionStoppedAsync(graphqlClient, flags.id);
30
30
  stopSpinner.succeed(`🎉 Device run session ${session.id} is ${session.status.toLowerCase()}`);
31
31
  }
32
32
  catch (err) {