eas-cli 18.11.0 → 18.12.1

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.
@@ -12,6 +12,7 @@ export default class IntegrationsConvexConnect extends EasCommand {
12
12
  'non-interactive': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
13
13
  };
14
14
  runAsync(): Promise<void>;
15
+ private upsertConvexUrlEasEnvVarAsync;
15
16
  private resolveRegionAsync;
16
17
  private resolveTeamNameAsync;
17
18
  private resolveProjectNameAsync;
@@ -7,11 +7,15 @@ const chalk_1 = tslib_1.__importDefault(require("chalk"));
7
7
  const dotenv_1 = tslib_1.__importDefault(require("dotenv"));
8
8
  const fs = tslib_1.__importStar(require("fs-extra"));
9
9
  const path_1 = tslib_1.__importDefault(require("path"));
10
+ const environment_1 = require("../../../build/utils/environment");
10
11
  const EasCommand_1 = tslib_1.__importDefault(require("../../../commandUtils/EasCommand"));
11
12
  const convex_1 = require("../../../commandUtils/convex");
12
13
  const flags_1 = require("../../../commandUtils/flags");
14
+ const generated_1 = require("../../../graphql/generated");
13
15
  const ConvexMutation_1 = require("../../../graphql/mutations/ConvexMutation");
16
+ const EnvironmentVariableMutation_1 = require("../../../graphql/mutations/EnvironmentVariableMutation");
14
17
  const ConvexQuery_1 = require("../../../graphql/queries/ConvexQuery");
18
+ const EnvironmentVariablesQuery_1 = require("../../../graphql/queries/EnvironmentVariablesQuery");
15
19
  const log_1 = tslib_1.__importDefault(require("../../../log"));
16
20
  const ora_1 = require("../../../ora");
17
21
  const projectUtils_1 = require("../../../project/projectUtils");
@@ -21,6 +25,12 @@ const CONVEX_REGIONS = [
21
25
  { title: 'EU West (aws-eu-west-1)', value: 'aws-eu-west-1' },
22
26
  ];
23
27
  const DEFAULT_REGION = 'aws-us-east-1';
28
+ const EAS_CONVEX_ENV_VAR_NAME = 'EXPO_PUBLIC_CONVEX_URL';
29
+ const EAS_CONVEX_ENVIRONMENTS = [
30
+ environment_1.DefaultEnvironment.Production,
31
+ environment_1.DefaultEnvironment.Preview,
32
+ environment_1.DefaultEnvironment.Development,
33
+ ];
24
34
  class IntegrationsConvexConnect extends EasCommand_1.default {
25
35
  static description = 'connect Convex to your Expo project';
26
36
  static contextDefinition = {
@@ -99,23 +109,63 @@ class IntegrationsConvexConnect extends EasCommand_1.default {
99
109
  spinner.fail('Failed to set up Convex project');
100
110
  throw error;
101
111
  }
102
- // 6. Send team invite (non-fatal)
112
+ // 6. Save the Convex URL as an EAS environment variable for builds
113
+ await this.upsertConvexUrlEasEnvVarAsync(graphqlClient, projectId, setupResult.convexDeploymentUrl, nonInteractive);
114
+ // 7. Send team invite (non-fatal)
103
115
  const teamInviteResult = await this.sendTeamInviteAsync(graphqlClient, connection, actor, {
104
116
  nonInteractive,
105
117
  });
106
- // 7. Write deploy key and URL to .env.local
118
+ // 8. Write deploy key and URL to .env.local
107
119
  await this.writeEnvLocalAsync(projectDir, setupResult.deployKey, setupResult.convexDeploymentUrl, nonInteractive);
108
- // 8. Success message
120
+ // 9. Success message
109
121
  log_1.default.addNewLineIfNone();
110
122
  log_1.default.log(chalk_1.default.green('Convex is ready!'));
111
123
  log_1.default.newLine();
112
124
  log_1.default.log('Next steps:');
113
125
  log_1.default.log(` 1. Start the Convex dev server: ${chalk_1.default.cyan('npx convex dev')}`);
126
+ log_1.default.log(` 2. Learn how to connect to your new Convex database by following our quickstart guide: ${chalk_1.default.cyan('https://docs.expo.dev/guides/using-convex')}`);
127
+ log_1.default.log(` 3. Read more about Convex: ${chalk_1.default.cyan('https://docs.convex.dev/')}`);
114
128
  log_1.default.newLine();
115
129
  if (teamInviteResult === 'sent') {
116
130
  log_1.default.log(`Check your email for an invitation to join your Convex team. Accept it for full dashboard access.`);
117
131
  }
118
132
  }
133
+ async upsertConvexUrlEasEnvVarAsync(graphqlClient, projectId, convexUrl, nonInteractive) {
134
+ const existingVariables = await EnvironmentVariablesQuery_1.EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, {
135
+ appId: projectId,
136
+ filterNames: [EAS_CONVEX_ENV_VAR_NAME],
137
+ });
138
+ const existingProjectVariable = existingVariables.find(variable => variable.scope === generated_1.EnvironmentVariableScope.Project);
139
+ if (existingProjectVariable) {
140
+ if (!nonInteractive) {
141
+ const overwrite = await (0, prompts_1.confirmAsync)({
142
+ message: `EAS already has an ${EAS_CONVEX_ENV_VAR_NAME} environment variable for this project. Overwrite it?`,
143
+ });
144
+ if (!overwrite) {
145
+ log_1.default.warn(`Skipped updating EAS environment variable ${chalk_1.default.bold(EAS_CONVEX_ENV_VAR_NAME)}.`);
146
+ return;
147
+ }
148
+ }
149
+ await EnvironmentVariableMutation_1.EnvironmentVariableMutation.updateAsync(graphqlClient, {
150
+ id: existingProjectVariable.id,
151
+ name: EAS_CONVEX_ENV_VAR_NAME,
152
+ value: convexUrl,
153
+ environments: EAS_CONVEX_ENVIRONMENTS,
154
+ visibility: generated_1.EnvironmentVariableVisibility.Public,
155
+ type: generated_1.EnvironmentSecretType.String,
156
+ });
157
+ log_1.default.withTick(`Updated EAS environment variable ${chalk_1.default.bold(EAS_CONVEX_ENV_VAR_NAME)} for builds`);
158
+ return;
159
+ }
160
+ await EnvironmentVariableMutation_1.EnvironmentVariableMutation.createForAppAsync(graphqlClient, {
161
+ name: EAS_CONVEX_ENV_VAR_NAME,
162
+ value: convexUrl,
163
+ environments: EAS_CONVEX_ENVIRONMENTS,
164
+ visibility: generated_1.EnvironmentVariableVisibility.Public,
165
+ type: generated_1.EnvironmentSecretType.String,
166
+ }, projectId);
167
+ log_1.default.withTick(`Created EAS environment variable ${chalk_1.default.bold(EAS_CONVEX_ENV_VAR_NAME)} for builds`);
168
+ }
119
169
  async resolveRegionAsync(flagValue, nonInteractive) {
120
170
  if (flagValue) {
121
171
  return flagValue;
@@ -0,0 +1,13 @@
1
+ import EasCommand from '../../commandUtils/EasCommand';
2
+ export default class SimulatorGet extends EasCommand {
3
+ static hidden: boolean;
4
+ static description: string;
5
+ static flags: {
6
+ 'non-interactive': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
7
+ id: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
8
+ };
9
+ static contextDefinition: {
10
+ loggedIn: import("../../commandUtils/context/LoggedInContextField").default;
11
+ };
12
+ runAsync(): Promise<void>;
13
+ }
@@ -0,0 +1,60 @@
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 url_1 = require("../../build/utils/url");
6
+ const EasCommand_1 = tslib_1.__importDefault(require("../../commandUtils/EasCommand"));
7
+ const flags_1 = require("../../commandUtils/flags");
8
+ const generated_1 = require("../../graphql/generated");
9
+ const DeviceRunSessionQuery_1 = require("../../graphql/queries/DeviceRunSessionQuery");
10
+ const log_1 = tslib_1.__importStar(require("../../log"));
11
+ const ora_1 = require("../../ora");
12
+ const utils_1 = require("../../simulator/utils");
13
+ class SimulatorGet extends EasCommand_1.default {
14
+ static hidden = true;
15
+ static description = '[EXPERIMENTAL] get info about a remote simulator session on EAS by its device run session ID';
16
+ static flags = {
17
+ id: core_1.Flags.string({
18
+ description: 'Device run session ID',
19
+ required: true,
20
+ }),
21
+ ...flags_1.EASNonInteractiveFlag,
22
+ };
23
+ static contextDefinition = {
24
+ ...this.ContextOptions.LoggedIn,
25
+ };
26
+ async runAsync() {
27
+ const { flags } = await this.parse(SimulatorGet);
28
+ const { loggedIn: { graphqlClient }, } = await this.getContextAsync(SimulatorGet, {
29
+ nonInteractive: flags['non-interactive'],
30
+ });
31
+ const fetchSpinner = (0, ora_1.ora)(`Fetching device run session ${flags.id}`).start();
32
+ let session;
33
+ try {
34
+ session = await DeviceRunSessionQuery_1.DeviceRunSessionQuery.byIdAsync(graphqlClient, flags.id);
35
+ fetchSpinner.succeed(`Fetched device run session ${session.id}`);
36
+ }
37
+ catch (err) {
38
+ fetchSpinner.fail(`Failed to fetch device run session ${flags.id}`);
39
+ throw err;
40
+ }
41
+ const jobRunUrl = session.turtleJobRun
42
+ ? (0, url_1.getBareJobRunUrl)(session.app.ownerAccount.name, session.app.slug, session.turtleJobRun.id)
43
+ : '';
44
+ log_1.default.newLine();
45
+ log_1.default.log(`ID: ${session.id}`);
46
+ log_1.default.log(`Type: ${session.type}`);
47
+ log_1.default.log(`Status: ${session.status}`);
48
+ log_1.default.log(`URL: ${jobRunUrl ? (0, log_1.link)(jobRunUrl) : ''}`);
49
+ if (session.status === generated_1.DeviceRunSessionStatus.InProgress) {
50
+ log_1.default.newLine();
51
+ if (session.remoteConfig) {
52
+ log_1.default.log((0, utils_1.formatRemoteSessionInstructions)(session.remoteConfig));
53
+ }
54
+ else {
55
+ log_1.default.log('⏳ Session is starting up — remote config is not available yet. Re-run this command in a moment.');
56
+ }
57
+ }
58
+ }
59
+ }
60
+ exports.default = SimulatorGet;
@@ -10,6 +10,7 @@ const DeviceRunSessionMutation_1 = require("../../graphql/mutations/DeviceRunSes
10
10
  const DeviceRunSessionQuery_1 = require("../../graphql/queries/DeviceRunSessionQuery");
11
11
  const log_1 = tslib_1.__importStar(require("../../log"));
12
12
  const ora_1 = require("../../ora");
13
+ const utils_1 = require("../../simulator/utils");
13
14
  const promise_1 = require("../../utils/promise");
14
15
  const nullthrows_1 = tslib_1.__importDefault(require("nullthrows"));
15
16
  const POLL_INTERVAL_MS = 5_000; // 5 seconds
@@ -18,11 +19,12 @@ const POLL_TIMEOUT_MS = 15 * 60 * 1_000; // 15 minutes
18
19
  // so adding a new enum value in codegen fails the build until it is wired up here.
19
20
  const DEVICE_RUN_SESSION_TYPE_FLAG_VALUES = {
20
21
  [generated_1.DeviceRunSessionType.AgentDevice]: 'agent-device',
22
+ [generated_1.DeviceRunSessionType.ServeSim]: 'serve-sim',
21
23
  };
22
24
  const DEVICE_RUN_SESSION_TYPE_BY_FLAG_VALUE = Object.fromEntries(Object.entries(DEVICE_RUN_SESSION_TYPE_FLAG_VALUES).map(([type, value]) => [value, type]));
23
25
  class SimulatorStart extends EasCommand_1.default {
24
26
  static hidden = true;
25
- static description = '[EXPERIMENTAL] start a remote simulator session on EAS and get the credentials to connect to it with the CLI tool of your choice';
27
+ static description = '[EXPERIMENTAL] start a remote simulator session on EAS and get instructions to connect to it';
26
28
  static flags = {
27
29
  platform: core_1.Flags.option({
28
30
  description: 'Device platform',
@@ -68,46 +70,42 @@ class SimulatorStart extends EasCommand_1.default {
68
70
  createSpinner.fail('Failed to create device run session');
69
71
  throw err;
70
72
  }
71
- const checkReadiness = getReadinessCheckerForType(flags.type);
72
- const pollSpinner = (0, ora_1.ora)(`⏳ Waiting for ${flags.type} daemon to start`).start();
73
+ const pollSpinner = (0, ora_1.ora)(`⏳ Waiting for ${flags.type} session to be ready`).start();
73
74
  const deadline = Date.now() + POLL_TIMEOUT_MS;
74
- let result = { ready: false };
75
+ let remoteConfig;
75
76
  try {
76
77
  while (Date.now() < deadline) {
77
78
  const session = await DeviceRunSessionQuery_1.DeviceRunSessionQuery.byIdAsync(graphqlClient, deviceRunSessionId);
78
79
  if (session.status === generated_1.DeviceRunSessionStatus.Errored ||
79
80
  session.status === generated_1.DeviceRunSessionStatus.Stopped) {
80
- throw new Error(`Device run session ${deviceRunSessionId} ${session.status.toLowerCase()} before the ${flags.type} daemon was ready. ${(0, log_1.link)(jobRunUrl)}`);
81
+ throw new Error(`Device run session ${deviceRunSessionId} ${session.status.toLowerCase()} before the ${flags.type} session was ready. ${(0, log_1.link)(jobRunUrl)}`);
81
82
  }
82
83
  const jobRunStatus = session.turtleJobRun?.status;
83
84
  if (jobRunStatus === generated_1.JobRunStatus.Errored ||
84
85
  jobRunStatus === generated_1.JobRunStatus.Canceled ||
85
86
  jobRunStatus === generated_1.JobRunStatus.Finished) {
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)}`);
87
+ throw new Error(`Turtle job run for device run session ${deviceRunSessionId} ${jobRunStatus.toLowerCase()} before the ${flags.type} session was ready. ${(0, log_1.link)(jobRunUrl)}`);
87
88
  }
88
- const logMessages = await fetchLogMessagesAsync(session.turtleJobRun?.logFileUrls ?? []);
89
- result = checkReadiness(logMessages);
90
- if (result.ready) {
91
- pollSpinner.succeed(`🎉 ${flags.type} daemon is ready`);
89
+ if (session.remoteConfig) {
90
+ remoteConfig = session.remoteConfig;
91
+ pollSpinner.succeed(`🎉 ${flags.type} session is ready`);
92
92
  break;
93
93
  }
94
94
  await (0, promise_1.sleepAsync)(POLL_INTERVAL_MS);
95
95
  }
96
96
  }
97
97
  catch (err) {
98
- pollSpinner.fail(`Failed while polling for ${flags.type} daemon logs`);
98
+ pollSpinner.fail(`Failed while polling for ${flags.type} session to be ready`);
99
99
  await ensureDeviceRunSessionStoppedSafelyAsync(graphqlClient, deviceRunSessionId);
100
100
  throw err;
101
101
  }
102
- if (!result.ready) {
103
- pollSpinner.fail(`Timed out waiting for ${flags.type} daemon to start`);
102
+ if (!remoteConfig) {
103
+ pollSpinner.fail(`Timed out waiting for ${flags.type} session to be ready`);
104
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)}`);
105
+ throw new Error(`Timed out after ${Math.round(POLL_TIMEOUT_MS / 1000)}s waiting for ${flags.type} session to be ready. ${(0, log_1.link)(jobRunUrl)}`);
106
106
  }
107
107
  log_1.default.newLine();
108
- log_1.default.log(`🔑 Run the following in your shell to attach to ${flags.type}:`);
109
- log_1.default.newLine();
110
- log_1.default.log(result.message);
108
+ log_1.default.log((0, utils_1.formatRemoteSessionInstructions)(remoteConfig));
111
109
  log_1.default.newLine();
112
110
  if (flags['non-interactive']) {
113
111
  log_1.default.log(`When you are done, stop the session with: eas simulator:stop --id ${deviceRunSessionId}`);
@@ -189,96 +187,3 @@ async function ensureDeviceRunSessionStoppedSafelyAsync(graphqlClient, deviceRun
189
187
  return false;
190
188
  }
191
189
  }
192
- function getReadinessCheckerForType(type) {
193
- switch (type) {
194
- case DEVICE_RUN_SESSION_TYPE_FLAG_VALUES[generated_1.DeviceRunSessionType.AgentDevice]:
195
- return checkAgentDeviceReadiness;
196
- default:
197
- throw new Error(`Unsupported device run session type: ${type}`);
198
- }
199
- }
200
- const AGENT_DEVICE_BASE_URL_ENV_VAR = 'AGENT_DEVICE_DAEMON_BASE_URL';
201
- const AGENT_DEVICE_AUTH_TOKEN_ENV_VAR = 'AGENT_DEVICE_DAEMON_AUTH_TOKEN';
202
- function checkAgentDeviceReadiness(logMessages) {
203
- let baseUrl;
204
- let authToken;
205
- for (const msg of logMessages) {
206
- baseUrl = baseUrl ?? extractExportedEnvValue(msg, AGENT_DEVICE_BASE_URL_ENV_VAR);
207
- authToken = authToken ?? extractExportedEnvValue(msg, AGENT_DEVICE_AUTH_TOKEN_ENV_VAR);
208
- if (baseUrl && authToken) {
209
- break;
210
- }
211
- }
212
- if (baseUrl && authToken) {
213
- return {
214
- ready: true,
215
- message: [
216
- `export ${AGENT_DEVICE_BASE_URL_ENV_VAR}='${baseUrl}'`,
217
- `export ${AGENT_DEVICE_AUTH_TOKEN_ENV_VAR}='${authToken}'`,
218
- ].join('\n'),
219
- };
220
- }
221
- return { ready: false };
222
- }
223
- async function fetchLogMessagesAsync(logUrls) {
224
- const messages = [];
225
- for (const url of logUrls) {
226
- const text = await fetchLogTextAsync(url);
227
- if (!text) {
228
- continue;
229
- }
230
- for (const line of text.split('\n')) {
231
- if (!line.trim()) {
232
- continue;
233
- }
234
- messages.push(extractLogMessage(line));
235
- }
236
- }
237
- return messages;
238
- }
239
- async function fetchLogTextAsync(url) {
240
- try {
241
- const response = await fetch(url);
242
- if (!response.ok) {
243
- return undefined;
244
- }
245
- return await response.text();
246
- }
247
- catch {
248
- return undefined;
249
- }
250
- }
251
- function extractLogMessage(line) {
252
- // Turtle job run logs are JSONL (bunyan-shaped), e.g.
253
- // {"msg":"export FOO=\"bar\"","time":"...","logId":"..."}
254
- // Fall back to the raw line if it's not JSON or doesn't have a string msg.
255
- const trimmed = line.trim();
256
- if (!trimmed.startsWith('{')) {
257
- return line;
258
- }
259
- try {
260
- const parsed = JSON.parse(trimmed);
261
- if (parsed && typeof parsed === 'object' && 'msg' in parsed) {
262
- const msg = parsed.msg;
263
- if (typeof msg === 'string') {
264
- return msg;
265
- }
266
- }
267
- }
268
- catch {
269
- // not JSON, fall through
270
- }
271
- return line;
272
- }
273
- function extractExportedEnvValue(text, varName) {
274
- // Matches: export NAME=value | export NAME="value" | export NAME='value'
275
- const pattern = new RegExp(`export\\s+${escapeRegExp(varName)}=(?:"([^"]*)"|'([^']*)'|(\\S+))`);
276
- const match = pattern.exec(text);
277
- if (!match) {
278
- return undefined;
279
- }
280
- return match[1] ?? match[2] ?? match[3];
281
- }
282
- function escapeRegExp(value) {
283
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
284
- }