eoas 1.0.38 → 2.0.0-alpha.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # EOAS (Expo-Open-OTA Support)
1
+ # EOAS (Expo Open Application Services)
2
2
 
3
- EOAS (Expo-Open-OTA Support) is a powerful helper package designed to simplify the setup and update publication process for the [expo-open-ota](https://github.com/axelmarciano/expo-open-ota) project.
3
+ EOAS ((Expo Open Application Services) is a powerful helper package designed to simplify the setup and update publication process for the [expo-open-ota](https://github.com/axelmarciano/expo-open-ota) project.
4
4
 
5
5
  ## Quick Start
6
6
 
@@ -27,6 +27,16 @@ class Init extends core_1.Command {
27
27
  log_1.default.error('Could not find Expo config in this project. Please make sure you have an Expo config.');
28
28
  return;
29
29
  }
30
+ const detectedAppId = config.extra?.eas
31
+ ?.projectId;
32
+ const { appId } = await (0, prompts_1.promptAsync)({
33
+ message: 'Enter the Expo project id for this project (sent as the expo-app-id header).\n' +
34
+ ' See https://axelmarciano.github.io/expo-open-ota/docs/getting-started/prerequisites for details.',
35
+ name: 'appId',
36
+ type: 'text',
37
+ initial: detectedAppId,
38
+ validate: v => !!v,
39
+ });
30
40
  const { updateUrl: promptedUrl } = await (0, prompts_1.promptAsync)({
31
41
  message: 'Enter the URL of your update server (ex: https://customota.com)',
32
42
  name: 'updateUrl',
@@ -95,6 +105,7 @@ class Init extends core_1.Command {
95
105
  enabled: true,
96
106
  requestHeaders: {
97
107
  'expo-channel-name': 'process.env.RELEASE_CHANNEL',
108
+ 'expo-app-id': appId,
98
109
  },
99
110
  };
100
111
  const updateConfigSpinner = (0, ora_1.ora)('Updating Expo config').start();
@@ -1,17 +1,17 @@
1
1
  import { Command } from '@oclif/core';
2
- import { Config } from '@oclif/core/lib/config';
3
- import { Client } from '../lib/vcs/vcs';
4
2
  export default class Publish extends Command {
5
- vcsClient: Client;
6
- constructor(argv: string[], config: Config);
7
3
  static args: {};
8
4
  static description: string;
9
5
  static examples: string[];
10
6
  static flags: {
11
7
  platform: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
12
- channel: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
8
+ channel: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
9
+ disableRepositoryCheck: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
13
10
  branch: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
14
11
  nonInteractive: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
12
+ outputDir: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
13
+ packageRunner: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
14
+ message: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
15
15
  };
16
16
  private sanitizeFlags;
17
17
  run(): Promise<void>;
@@ -7,25 +7,21 @@ const core_1 = require("@oclif/core");
7
7
  const form_data_1 = tslib_1.__importDefault(require("form-data"));
8
8
  const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
9
9
  const mime_1 = tslib_1.__importDefault(require("mime"));
10
- const node_fetch_1 = tslib_1.__importDefault(require("node-fetch"));
11
10
  const path_1 = tslib_1.__importDefault(require("path"));
12
11
  const assets_1 = require("../lib/assets");
13
12
  const auth_1 = require("../lib/auth");
14
13
  const expoConfig_1 = require("../lib/expoConfig");
14
+ const fetch_1 = require("../lib/fetch");
15
15
  const log_1 = tslib_1.__importDefault(require("../lib/log"));
16
16
  const ora_1 = require("../lib/ora");
17
17
  const package_1 = require("../lib/package");
18
+ const packageRunner_1 = require("../lib/packageRunner");
18
19
  const prompts_1 = require("../lib/prompts");
19
20
  const repo_1 = require("../lib/repo");
20
21
  const runtimeVersion_1 = require("../lib/runtimeVersion");
21
22
  const vcs_1 = require("../lib/vcs");
22
23
  const workflow_1 = require("../lib/workflow");
23
24
  class Publish extends core_1.Command {
24
- vcsClient;
25
- constructor(argv, config) {
26
- super(argv, config);
27
- this.vcsClient = (0, vcs_1.resolveVcsClient)(false);
28
- }
29
25
  static args = {};
30
26
  static description = 'Publish a new update to the self-hosted update server';
31
27
  static examples = ['<%= config.bin %> <%= command.id %>'];
@@ -38,7 +34,15 @@ class Publish extends core_1.Command {
38
34
  }),
39
35
  channel: core_1.Flags.string({
40
36
  description: 'Name of the channel to publish the update to',
41
- required: true,
37
+ required: false,
38
+ deprecated: {
39
+ message: 'Channel was initially used to provide RELEASE_CHANNEL in the environment when resolving the runtime version. It is no longer needed, you can use RELEASE_CHANNEL={channel} eoas publish --branch={branch} instead',
40
+ },
41
+ }),
42
+ disableRepositoryCheck: core_1.Flags.boolean({
43
+ description: 'Disable repository check (Useful for CI/CD)',
44
+ default: false,
45
+ hidden: true,
42
46
  }),
43
47
  branch: core_1.Flags.string({
44
48
  description: 'Name of the branch to point to',
@@ -48,13 +52,30 @@ class Publish extends core_1.Command {
48
52
  description: 'Run command in non-interactive mode',
49
53
  default: false,
50
54
  }),
55
+ outputDir: core_1.Flags.string({
56
+ description: "Where to write build output. You can override the default dist output directory if it's being used by something else",
57
+ default: 'dist',
58
+ }),
59
+ packageRunner: core_1.Flags.string({
60
+ description: 'Package runner to use for spawning Expo CLI commands (e.g. npx, bunx, pnpx). Can also be set via EOAS_PACKAGE_RUNNER env var. Defaults to npx.',
61
+ required: false,
62
+ }),
63
+ message: core_1.Flags.string({
64
+ char: 'm',
65
+ description: 'A short message describing the update. Defaults to the latest git commit message.',
66
+ required: false,
67
+ }),
51
68
  };
52
69
  sanitizeFlags(flags) {
53
70
  return {
71
+ disableRepositoryCheck: flags.disableRepositoryCheck,
54
72
  platform: flags.platform,
55
73
  branch: flags.branch,
56
74
  nonInteractive: flags.nonInteractive,
57
- channel: flags.channel,
75
+ outputDir: flags.outputDir,
76
+ packageRunner: (0, packageRunner_1.resolvePackageRunner)(flags.packageRunner, process.cwd()),
77
+ providedDeprecatedChannel: flags.channel,
78
+ message: flags.message,
58
79
  };
59
80
  }
60
81
  async run() {
@@ -64,45 +85,36 @@ class Publish extends core_1.Command {
64
85
  process.exit(1);
65
86
  }
66
87
  const { flags } = await this.parse(Publish);
67
- const { platform, nonInteractive, branch, channel } = this.sanitizeFlags(flags);
88
+ const { platform, nonInteractive, branch, outputDir, packageRunner, providedDeprecatedChannel, disableRepositoryCheck, message, } = this.sanitizeFlags(flags);
68
89
  if (!branch) {
69
90
  log_1.default.error('Branch name is required');
70
91
  process.exit(1);
71
92
  }
72
- if (!channel) {
73
- log_1.default.error('Channel name is required');
74
- process.exit(1);
75
- }
76
- await this.vcsClient.ensureRepoExistsAsync();
77
- await (0, repo_1.ensureRepoIsCleanAsync)(this.vcsClient, nonInteractive);
78
93
  const projectDir = process.cwd();
79
94
  const hasExpo = (0, package_1.isExpoInstalled)(projectDir);
80
95
  if (!hasExpo) {
81
96
  log_1.default.error('Expo is not installed in this project. Please install Expo first.');
82
97
  process.exit(1);
83
98
  }
84
- const privateConfig = await (0, expoConfig_1.getPrivateExpoConfigAsync)(projectDir, {
99
+ const vcsClient = (0, vcs_1.resolveVcsClient)(true);
100
+ if (!disableRepositoryCheck) {
101
+ await (0, repo_1.ensureRepoIsCleanAsync)(vcsClient, nonInteractive);
102
+ }
103
+ const config = await (0, expoConfig_1.getPrivateExpoConfigAsync)(projectDir, {
85
104
  env: {
86
- RELEASE_CHANNEL: channel,
105
+ ...process.env,
106
+ ...(providedDeprecatedChannel ? { RELEASE_CHANNEL: providedDeprecatedChannel } : {}),
87
107
  },
108
+ packageRunner,
88
109
  });
89
- const updateUrl = (0, expoConfig_1.getExpoConfigUpdateUrl)(privateConfig);
90
- if (!updateUrl) {
91
- log_1.default.error("Update url is not setup in your config. Please run 'eoas init' to setup the update url");
92
- process.exit(1);
93
- }
94
- let baseUrl;
95
- try {
96
- const parsedUrl = new URL(updateUrl);
97
- baseUrl = parsedUrl.origin;
98
- }
99
- catch (e) {
100
- log_1.default.error('Invalid URL', e);
110
+ const serverUrl = await (0, expoConfig_1.resolveServerUrl)(config).catch(e => {
111
+ log_1.default.error(e.message);
101
112
  process.exit(1);
102
- }
113
+ });
114
+ const appId = (0, expoConfig_1.requireExpoAppId)(config);
103
115
  if (!nonInteractive) {
104
116
  const confirmed = await (0, prompts_1.confirmAsync)({
105
- message: `Is this the correct URL of your self-hosted update server? ${baseUrl}`,
117
+ message: `Is this the correct URL of your self-hosted update server? ${serverUrl}`,
106
118
  name: 'export',
107
119
  type: 'confirm',
108
120
  });
@@ -111,45 +123,72 @@ class Publish extends core_1.Command {
111
123
  process.exit(1);
112
124
  }
113
125
  }
126
+ const commitHash = await vcsClient.getCommitHashAsync();
127
+ let resolvedMessage = message;
128
+ if (!resolvedMessage && vcsClient.canGetLastCommitMessage()) {
129
+ resolvedMessage = (await vcsClient.getLastCommitMessageAsync()) ?? undefined;
130
+ }
114
131
  const runtimeSpinner = (0, ora_1.ora)('🔄 Resolving runtime version...').start();
115
132
  const runtimeVersions = [
116
133
  ...(!platform || platform === expoConfig_1.RequestedPlatform.All || platform === expoConfig_1.RequestedPlatform.Ios
117
134
  ? [
118
- (await (0, runtimeVersion_1.resolveRuntimeVersionAsync)({
119
- exp: privateConfig,
135
+ {
136
+ runtimeVersion: (await (0, runtimeVersion_1.resolveRuntimeVersionAsync)({
137
+ exp: config,
138
+ platform: 'ios',
139
+ workflow: await (0, workflow_1.resolveWorkflowAsync)(projectDir, eas_build_job_1.Platform.IOS, vcsClient),
140
+ projectDir,
141
+ env: {
142
+ ...process.env,
143
+ ...(providedDeprecatedChannel
144
+ ? { RELEASE_CHANNEL: providedDeprecatedChannel }
145
+ : {}),
146
+ },
147
+ }))?.runtimeVersion,
120
148
  platform: 'ios',
121
- workflow: await (0, workflow_1.resolveWorkflowAsync)(projectDir, eas_build_job_1.Platform.IOS, this.vcsClient),
122
- projectDir,
123
- env: {
124
- RELEASE_CHANNEL: channel,
125
- },
126
- }))?.runtimeVersion,
149
+ },
127
150
  ]
128
151
  : []),
129
152
  ...(!platform || platform === expoConfig_1.RequestedPlatform.All || platform === expoConfig_1.RequestedPlatform.Android
130
153
  ? [
131
- (await (0, runtimeVersion_1.resolveRuntimeVersionAsync)({
132
- exp: privateConfig,
154
+ {
155
+ runtimeVersion: (await (0, runtimeVersion_1.resolveRuntimeVersionAsync)({
156
+ exp: config,
157
+ platform: 'android',
158
+ workflow: await (0, workflow_1.resolveWorkflowAsync)(projectDir, eas_build_job_1.Platform.ANDROID, vcsClient),
159
+ projectDir,
160
+ env: {
161
+ ...process.env,
162
+ ...(providedDeprecatedChannel
163
+ ? { RELEASE_CHANNEL: providedDeprecatedChannel }
164
+ : {}),
165
+ },
166
+ }))?.runtimeVersion,
133
167
  platform: 'android',
134
- workflow: await (0, workflow_1.resolveWorkflowAsync)(projectDir, eas_build_job_1.Platform.ANDROID, this.vcsClient),
135
- projectDir,
136
- env: {
137
- RELEASE_CHANNEL: channel,
138
- },
139
- }))?.runtimeVersion,
168
+ },
140
169
  ]
141
170
  : []),
142
- ].filter(Boolean);
171
+ ].filter(({ runtimeVersion }) => !!runtimeVersion);
143
172
  if (!runtimeVersions.length) {
144
173
  runtimeSpinner.fail('Could not resolve runtime versions for the requested platforms');
145
174
  log_1.default.error('Could not resolve runtime versions for the requested platforms');
146
175
  process.exit(1);
147
176
  }
148
177
  runtimeSpinner.succeed('✅ Runtime versions resolved');
178
+ const cleaningSpinner = (0, ora_1.ora)(`🗑️ Cleaning up ${outputDir} directory...`).start();
179
+ try {
180
+ await fs_extra_1.default.remove(path_1.default.join(projectDir, outputDir));
181
+ cleaningSpinner.succeed('✅ Cleanup completed');
182
+ }
183
+ catch (e) {
184
+ cleaningSpinner.fail('❌ Failed to clean up the output directory');
185
+ log_1.default.error(e);
186
+ process.exit(1);
187
+ }
149
188
  const exportSpinner = (0, ora_1.ora)('📦 Exporting project files...').start();
150
189
  try {
151
- await (0, spawn_async_1.default)('rm', ['-rf', 'dist'], { cwd: projectDir });
152
- const { stdout } = await (0, spawn_async_1.default)('npx', ['expo', 'export', '--output-dir', 'dist'], {
190
+ const specifiedPlatform = platform === expoConfig_1.RequestedPlatform.All ? [] : ['--platform', platform];
191
+ const { stdout } = await (0, spawn_async_1.default)(packageRunner, ['expo', 'export', '--output-dir', outputDir, ...specifiedPlatform], {
153
192
  cwd: projectDir,
154
193
  env: {
155
194
  ...process.env,
@@ -159,51 +198,65 @@ class Publish extends core_1.Command {
159
198
  exportSpinner.succeed('🚀 Project exported successfully');
160
199
  log_1.default.withInfo(stdout);
161
200
  }
162
- catch {
163
- exportSpinner.fail('❌ Failed to export the project');
201
+ catch (e) {
202
+ exportSpinner.fail(`❌ Failed to export the project, ${e}`);
164
203
  process.exit(1);
165
204
  }
166
205
  const publicConfig = await (0, expoConfig_1.getPublicExpoConfigAsync)(projectDir, {
167
206
  skipSDKVersionRequirement: true,
207
+ packageRunner,
168
208
  });
169
209
  if (!publicConfig) {
170
210
  log_1.default.error('Could not find Expo config in this project. Please make sure you have an Expo config.');
171
211
  process.exit(1);
172
212
  }
173
213
  // eslint-disable-next-line
174
- fs_extra_1.default.writeJsonSync(path_1.default.join(projectDir, 'dist', 'expoConfig.json'), publicConfig, {
214
+ fs_extra_1.default.writeJsonSync(path_1.default.join(projectDir, outputDir, 'expoConfig.json'), publicConfig, {
175
215
  spaces: 2,
176
216
  });
177
- log_1.default.withInfo('expoConfig.json file created in dist directory');
217
+ log_1.default.withInfo(`expoConfig.json file created in ${outputDir} directory`);
178
218
  const uploadFilesSpinner = (0, ora_1.ora)('📤 Uploading files...').start();
179
- const files = (0, assets_1.computeFilesRequests)(projectDir, platform || expoConfig_1.RequestedPlatform.All);
219
+ const files = (0, assets_1.computeFilesRequests)(projectDir, outputDir, platform || expoConfig_1.RequestedPlatform.All);
180
220
  if (!files.length) {
181
221
  uploadFilesSpinner.fail('No files to upload');
182
222
  process.exit(1);
183
223
  }
224
+ let uploadUrls = [];
184
225
  try {
185
- const uploadUrls = await Promise.all(runtimeVersions.map(runtimeVersion => {
226
+ uploadUrls = await Promise.all(runtimeVersions.map(async ({ runtimeVersion, platform }) => {
186
227
  if (!runtimeVersion) {
187
228
  throw new Error('Runtime version is not resolved');
188
229
  }
189
- return (0, assets_1.requestUploadUrls)({
190
- fileNames: files.map(file => file.path),
191
- }, `${baseUrl}/requestUploadUrl/${branch}`, credentials, runtimeVersion);
230
+ return {
231
+ ...(await (0, assets_1.requestUploadUrls)({
232
+ body: {
233
+ fileNames: files.map(file => file.path),
234
+ },
235
+ requestUploadUrl: `${serverUrl}/${appId}/requestUploadUrl/${branch}`,
236
+ auth: credentials,
237
+ runtimeVersion,
238
+ platform,
239
+ commitHash,
240
+ message: resolvedMessage,
241
+ })),
242
+ runtimeVersion,
243
+ platform,
244
+ };
192
245
  }));
193
- const allItems = uploadUrls.flat();
246
+ const allItems = uploadUrls.flatMap(({ uploadRequests }) => uploadRequests);
194
247
  await Promise.all(allItems.map(async (itm) => {
195
- const isLocalBucketFileUpload = itm.requestUploadUrl.startsWith(`${baseUrl}/uploadLocalFile`);
248
+ const isLocalBucketFileUpload = itm.requestUploadUrl.startsWith(`${serverUrl}/${appId}/uploadLocalFile`);
196
249
  const formData = new form_data_1.default();
197
250
  let file;
198
251
  try {
199
- file = fs_extra_1.default.createReadStream(path_1.default.join(projectDir, 'dist', itm.filePath));
252
+ file = fs_extra_1.default.createReadStream(path_1.default.join(projectDir, outputDir, itm.filePath));
200
253
  }
201
254
  catch {
202
255
  throw new Error(`Failed to read file ${itm.filePath}`);
203
256
  }
204
257
  formData.append(itm.fileName, file);
205
258
  if (isLocalBucketFileUpload) {
206
- const response = await (0, node_fetch_1.default)(itm.requestUploadUrl, {
259
+ const response = await (0, fetch_1.fetchWithRetries)(itm.requestUploadUrl, {
207
260
  method: 'PUT',
208
261
  headers: {
209
262
  ...formData.getHeaders(),
@@ -227,8 +280,8 @@ class Publish extends core_1.Command {
227
280
  if (!contentType) {
228
281
  contentType = 'application/octet-stream';
229
282
  }
230
- const buffer = await fs_extra_1.default.readFile(path_1.default.join(projectDir, 'dist', itm.filePath));
231
- const response = await (0, node_fetch_1.default)(itm.requestUploadUrl, {
283
+ const buffer = await fs_extra_1.default.readFile(path_1.default.join(projectDir, outputDir, itm.filePath));
284
+ const response = await (0, fetch_1.fetchWithRetries)(itm.requestUploadUrl, {
232
285
  method: 'PUT',
233
286
  headers: {
234
287
  'Content-Type': contentType,
@@ -244,15 +297,58 @@ class Publish extends core_1.Command {
244
297
  }));
245
298
  uploadFilesSpinner.succeed('✅ Files uploaded successfully');
246
299
  }
247
- catch {
300
+ catch (e) {
248
301
  uploadFilesSpinner.fail('❌ Failed to upload static files');
302
+ log_1.default.error(e);
249
303
  process.exit(1);
250
304
  }
251
- console.log(`\n✅ Your update has been successfully pushed to ${updateUrl}`);
252
- console.log(`🔗 Channel: \`${channel}\``);
253
- console.log(`🌿 Branch: \`${branch}\``);
254
- console.log(`⏳ Deployed at: \`${new Date().toUTCString()}\`\n`);
255
- console.log('🔥 Your users will receive the latest update automatically!');
305
+ const markAsFinishedSpinner = (0, ora_1.ora)('🔗 Marking the updates as finished...').start();
306
+ const results = await Promise.all(uploadUrls.map(async ({ updateId, platform, runtimeVersion }) => {
307
+ const markAsUploadedUrl = new URL(`${serverUrl}/${appId}/markUpdateAsUploaded/${branch}`);
308
+ markAsUploadedUrl.searchParams.set('platform', platform);
309
+ markAsUploadedUrl.searchParams.set('updateId', updateId);
310
+ markAsUploadedUrl.searchParams.set('runtimeVersion', runtimeVersion);
311
+ const response = await (0, fetch_1.fetchWithRetries)(markAsUploadedUrl.toString(), {
312
+ method: 'POST',
313
+ headers: {
314
+ ...(0, auth_1.getAuthExpoHeaders)(credentials),
315
+ 'Content-Type': 'application/json',
316
+ },
317
+ });
318
+ // If success and status code = 200
319
+ if (response.ok) {
320
+ log_1.default.withInfo(`✅ Update ready for ${platform}`);
321
+ return 'deployed';
322
+ }
323
+ // If response.status === 406 duplicate update
324
+ if (response.status === 406) {
325
+ log_1.default.withInfo(`⚠️ There is no change in the update for ${platform}, ignored...`);
326
+ return 'identical';
327
+ }
328
+ log_1.default.error('❌ Failed to mark the update as finished for platform', platform);
329
+ log_1.default.newLine();
330
+ log_1.default.error(await response.text());
331
+ return 'error';
332
+ }));
333
+ const erroredUpdates = results.filter(result => result === 'error');
334
+ const hasSuccess = results.some(result => result === 'deployed');
335
+ const allIdentical = results.every(result => result === 'identical');
336
+ if (allIdentical) {
337
+ markAsFinishedSpinner.warn('⚠️ No changes found in the update, nothing to deploy');
338
+ return;
339
+ }
340
+ if (erroredUpdates.length) {
341
+ markAsFinishedSpinner.fail('❌ Some errors occurred while marking updates as finished');
342
+ throw new Error();
343
+ }
344
+ else {
345
+ markAsFinishedSpinner.succeed(`\n✅ Your update has been successfully pushed to ${serverUrl}`);
346
+ }
347
+ if (hasSuccess) {
348
+ log_1.default.withInfo(`🌿 Branch: \`${branch}\``);
349
+ log_1.default.withInfo(`⏳ Deployed at: \`${new Date().toUTCString()}\`\n`);
350
+ log_1.default.withInfo('🔥 Your users will receive the latest update automatically!');
351
+ }
256
352
  }
257
353
  }
258
354
  exports.default = Publish;
@@ -0,0 +1,12 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Publish extends Command {
3
+ static args: {};
4
+ static description: string;
5
+ static examples: string[];
6
+ static flags: {
7
+ branch: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
8
+ platform: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
9
+ };
10
+ private sanitizeFlags;
11
+ run(): Promise<void>;
12
+ }
@@ -0,0 +1,152 @@
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 ora_1 = tslib_1.__importDefault(require("ora"));
6
+ const auth_1 = require("../lib/auth");
7
+ const expoConfig_1 = require("../lib/expoConfig");
8
+ const fetch_1 = require("../lib/fetch");
9
+ const log_1 = tslib_1.__importDefault(require("../lib/log"));
10
+ const package_1 = require("../lib/package");
11
+ const prompts_1 = require("../lib/prompts");
12
+ const vcs_1 = require("../lib/vcs");
13
+ class Publish extends core_1.Command {
14
+ static args = {};
15
+ static description = 'Republish a previous update to a branch';
16
+ static examples = ['<%= config.bin %> <%= command.id %>'];
17
+ static flags = {
18
+ branch: core_1.Flags.string({
19
+ description: 'Name of the branch to point to',
20
+ required: true,
21
+ }),
22
+ platform: core_1.Flags.string({
23
+ type: 'option',
24
+ options: ['ios', 'android'],
25
+ default: 'all',
26
+ required: true,
27
+ }),
28
+ };
29
+ sanitizeFlags(flags) {
30
+ return {
31
+ branch: flags.branch,
32
+ platform: flags.platform,
33
+ };
34
+ }
35
+ async run() {
36
+ const credentials = (0, auth_1.retrieveExpoCredentials)();
37
+ if (!credentials.token && !credentials.sessionSecret) {
38
+ log_1.default.error('You are not logged to eas, please run `eas login`');
39
+ process.exit(1);
40
+ }
41
+ const { flags } = await this.parse(Publish);
42
+ const { branch, platform } = this.sanitizeFlags(flags);
43
+ if (!branch) {
44
+ log_1.default.error('Branch name is required');
45
+ process.exit(1);
46
+ }
47
+ if (!platform) {
48
+ log_1.default.error('Platform is required');
49
+ process.exit(1);
50
+ }
51
+ const vcsClient = (0, vcs_1.resolveVcsClient)(true);
52
+ await vcsClient.ensureRepoExistsAsync();
53
+ // const commitHash = await vcsClient.getCommitHashAsync();
54
+ const projectDir = process.cwd();
55
+ const hasExpo = (0, package_1.isExpoInstalled)(projectDir);
56
+ if (!hasExpo) {
57
+ log_1.default.error('Expo is not installed in this project. Please install Expo first.');
58
+ process.exit(1);
59
+ }
60
+ const privateConfig = await (0, expoConfig_1.getPrivateExpoConfigAsync)(projectDir, {
61
+ env: process.env,
62
+ });
63
+ const updateUrl = (0, expoConfig_1.getExpoConfigUpdateUrl)(privateConfig);
64
+ if (!updateUrl) {
65
+ log_1.default.error("Update url is not setup in your config. Please run 'eoas init' to setup the update url");
66
+ process.exit(1);
67
+ }
68
+ const appId = (0, expoConfig_1.requireExpoAppId)(privateConfig);
69
+ let baseUrl;
70
+ try {
71
+ const parsedUrl = new URL(updateUrl);
72
+ baseUrl = parsedUrl.origin;
73
+ }
74
+ catch (e) {
75
+ log_1.default.error('Invalid URL', e);
76
+ process.exit(1);
77
+ }
78
+ const runtimeVersionsEndpoint = `${baseUrl}/api/apps/${appId}/branch/${branch}/runtimeVersions`;
79
+ const response = await (0, fetch_1.fetchWithRetries)(runtimeVersionsEndpoint, {
80
+ headers: {
81
+ ...(0, auth_1.getAuthExpoHeaders)(credentials),
82
+ 'use-expo-auth': 'true',
83
+ },
84
+ });
85
+ if (!response.ok) {
86
+ log_1.default.error(`Failed to fetch runtime versions: ${await response.text()}`);
87
+ process.exit(1);
88
+ }
89
+ const runtimeVersions = (await response.json());
90
+ const filteredRuntimeVersions = runtimeVersions.filter(runtimeVersion => runtimeVersion.numberOfUpdates > 1);
91
+ if (filteredRuntimeVersions.length === 0) {
92
+ log_1.default.error('No runtime versions found');
93
+ process.exit(1);
94
+ }
95
+ // Ask the user to select a runtime version
96
+ const selectedRuntimeVersion = await (0, prompts_1.promptAsync)({
97
+ type: 'select',
98
+ name: 'runtimeVersion',
99
+ message: 'Select a runtime version',
100
+ choices: filteredRuntimeVersions.map(runtimeVersion => ({
101
+ title: runtimeVersion.runtimeVersion,
102
+ value: runtimeVersion.runtimeVersion,
103
+ })),
104
+ });
105
+ log_1.default.log(`Selected runtime version: ${selectedRuntimeVersion.runtimeVersion}`);
106
+ const updatesEndpoint = `${baseUrl}/api/apps/${appId}/branch/${branch}/runtimeVersion/${selectedRuntimeVersion.runtimeVersion}/updates`;
107
+ const updatesResponse = await (0, fetch_1.fetchWithRetries)(updatesEndpoint, {
108
+ headers: {
109
+ ...(0, auth_1.getAuthExpoHeaders)(credentials),
110
+ 'use-expo-auth': 'true',
111
+ },
112
+ });
113
+ if (!updatesResponse.ok) {
114
+ log_1.default.error(`Failed to fetch updates: ${await updatesResponse.text()}`);
115
+ process.exit(1);
116
+ }
117
+ const updates = (await updatesResponse.json()).filter(u => {
118
+ return u.updateUUID !== 'Rollback to embedded' && u.platform === platform;
119
+ });
120
+ const selectedUpdated = await (0, prompts_1.promptAsync)({
121
+ type: 'select',
122
+ name: 'update',
123
+ message: 'Select an update to republish',
124
+ choices: updates.map(update => ({
125
+ title: update.updateUUID,
126
+ value: update,
127
+ description: `Created at: ${update.createdAt}, Platform: ${update.platform}, Commit hash: ${update.commitHash}`,
128
+ })),
129
+ });
130
+ log_1.default.log(`Re-publishing update: ${selectedUpdated.update.updateUUID}`);
131
+ const republishUrl = new URL(`${baseUrl}/${appId}/republish/${branch}`);
132
+ republishUrl.searchParams.set('platform', platform);
133
+ republishUrl.searchParams.set('runtimeVersion', selectedRuntimeVersion.runtimeVersion);
134
+ republishUrl.searchParams.set('updateId', selectedUpdated.update.updateId);
135
+ republishUrl.searchParams.set('commitHash', selectedUpdated.update.commitHash);
136
+ const republishSpinner = (0, ora_1.default)('🔄 Republishing update...').start();
137
+ const republishResponse = await (0, fetch_1.fetchWithRetries)(republishUrl.toString(), {
138
+ method: 'POST',
139
+ headers: {
140
+ ...(0, auth_1.getAuthExpoHeaders)(credentials),
141
+ 'Content-Type': 'application/json',
142
+ },
143
+ });
144
+ if (!republishResponse.ok) {
145
+ republishSpinner.fail('❌ Republish failed');
146
+ log_1.default.error(`Failed to republish update: ${await republishResponse.text()}`);
147
+ process.exit(1);
148
+ }
149
+ republishSpinner.succeed('✅ Republish successful');
150
+ }
151
+ }
152
+ exports.default = Publish;
@@ -0,0 +1,13 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Publish extends Command {
3
+ static args: {};
4
+ static description: string;
5
+ static examples: string[];
6
+ static flags: {
7
+ platform: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
8
+ branch: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
9
+ nonInteractive: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
10
+ };
11
+ private sanitizeFlags;
12
+ run(): Promise<void>;
13
+ }
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const eas_build_job_1 = require("@expo/eas-build-job");
5
+ const core_1 = require("@oclif/core");
6
+ const auth_1 = require("../lib/auth");
7
+ const expoConfig_1 = require("../lib/expoConfig");
8
+ const fetch_1 = require("../lib/fetch");
9
+ const log_1 = tslib_1.__importDefault(require("../lib/log"));
10
+ const ora_1 = require("../lib/ora");
11
+ const package_1 = require("../lib/package");
12
+ const prompts_1 = require("../lib/prompts");
13
+ const runtimeVersion_1 = require("../lib/runtimeVersion");
14
+ const vcs_1 = require("../lib/vcs");
15
+ const workflow_1 = require("../lib/workflow");
16
+ class Publish extends core_1.Command {
17
+ static args = {};
18
+ static description = 'Publish a new rollback to the self-hosted update server';
19
+ static examples = ['<%= config.bin %> <%= command.id %>'];
20
+ static flags = {
21
+ platform: core_1.Flags.string({
22
+ type: 'option',
23
+ options: Object.values(expoConfig_1.RequestedPlatform),
24
+ default: expoConfig_1.RequestedPlatform.All,
25
+ required: false,
26
+ }),
27
+ branch: core_1.Flags.string({
28
+ description: 'Name of the branch to point to',
29
+ required: true,
30
+ }),
31
+ nonInteractive: core_1.Flags.boolean({
32
+ description: 'Run command in non-interactive mode',
33
+ default: false,
34
+ }),
35
+ };
36
+ sanitizeFlags(flags) {
37
+ return {
38
+ platform: flags.platform,
39
+ branch: flags.branch,
40
+ nonInteractive: flags.nonInteractive,
41
+ };
42
+ }
43
+ async run() {
44
+ const credentials = (0, auth_1.retrieveExpoCredentials)();
45
+ if (!credentials.token && !credentials.sessionSecret) {
46
+ log_1.default.error('You are not logged to eas, please run `eas login`');
47
+ process.exit(1);
48
+ }
49
+ const { flags } = await this.parse(Publish);
50
+ const { platform, branch, nonInteractive } = this.sanitizeFlags(flags);
51
+ if (!branch) {
52
+ log_1.default.error('Branch name is required');
53
+ process.exit(1);
54
+ }
55
+ const vcsClient = (0, vcs_1.resolveVcsClient)(true);
56
+ await vcsClient.ensureRepoExistsAsync();
57
+ const commitHash = await vcsClient.getCommitHashAsync();
58
+ const projectDir = process.cwd();
59
+ const hasExpo = (0, package_1.isExpoInstalled)(projectDir);
60
+ if (!hasExpo) {
61
+ log_1.default.error('Expo is not installed in this project. Please install Expo first.');
62
+ process.exit(1);
63
+ }
64
+ if (!nonInteractive) {
65
+ const confirmed = await (0, prompts_1.confirmAsync)({
66
+ message: `Are you sure you want to publish a rollback to the branch ${branch} ?`,
67
+ name: 'export',
68
+ type: 'confirm',
69
+ });
70
+ if (!confirmed) {
71
+ log_1.default.error('Operation cancelled');
72
+ process.exit(1);
73
+ }
74
+ }
75
+ const privateConfig = await (0, expoConfig_1.getPrivateExpoConfigAsync)(projectDir, {
76
+ env: process.env,
77
+ });
78
+ if (privateConfig?.updates?.disableAntiBrickingMeasures) {
79
+ log_1.default.error('When using disableAntiBrickingMeasures, expo-updates is ignoring the embeded update of the app, please use republish command instead');
80
+ process.exit(1);
81
+ }
82
+ const updateUrl = (0, expoConfig_1.getExpoConfigUpdateUrl)(privateConfig);
83
+ if (!updateUrl) {
84
+ log_1.default.error("Update url is not setup in your config. Please run 'eoas init' to setup the update url");
85
+ process.exit(1);
86
+ }
87
+ const appId = (0, expoConfig_1.requireExpoAppId)(privateConfig);
88
+ let baseUrl;
89
+ try {
90
+ const parsedUrl = new URL(updateUrl);
91
+ baseUrl = parsedUrl.origin;
92
+ }
93
+ catch (e) {
94
+ log_1.default.error('Invalid URL', e);
95
+ process.exit(1);
96
+ }
97
+ const runtimeSpinner = (0, ora_1.ora)('🔄 Resolving runtime version...').start();
98
+ const runtimeVersions = [
99
+ ...(!platform || platform === expoConfig_1.RequestedPlatform.All || platform === expoConfig_1.RequestedPlatform.Ios
100
+ ? [
101
+ {
102
+ runtimeVersion: (await (0, runtimeVersion_1.resolveRuntimeVersionAsync)({
103
+ exp: privateConfig,
104
+ platform: 'ios',
105
+ workflow: await (0, workflow_1.resolveWorkflowAsync)(projectDir, eas_build_job_1.Platform.IOS, vcsClient),
106
+ projectDir,
107
+ env: process.env,
108
+ }))?.runtimeVersion,
109
+ platform: 'ios',
110
+ },
111
+ ]
112
+ : []),
113
+ ...(!platform || platform === expoConfig_1.RequestedPlatform.All || platform === expoConfig_1.RequestedPlatform.Android
114
+ ? [
115
+ {
116
+ runtimeVersion: (await (0, runtimeVersion_1.resolveRuntimeVersionAsync)({
117
+ exp: privateConfig,
118
+ platform: 'android',
119
+ workflow: await (0, workflow_1.resolveWorkflowAsync)(projectDir, eas_build_job_1.Platform.ANDROID, vcsClient),
120
+ projectDir,
121
+ env: process.env,
122
+ }))?.runtimeVersion,
123
+ platform: 'android',
124
+ },
125
+ ]
126
+ : []),
127
+ ].filter(({ runtimeVersion }) => !!runtimeVersion);
128
+ if (!runtimeVersions.length) {
129
+ runtimeSpinner.fail('Could not resolve runtime versions for the requested platforms');
130
+ log_1.default.error('Could not resolve runtime versions for the requested platforms');
131
+ process.exit(1);
132
+ }
133
+ runtimeSpinner.succeed('✅ Runtime versions resolved');
134
+ const rollbackSpinner = (0, ora_1.ora)('📦 Uploading rollback...').start();
135
+ const erroredPlatforms = [];
136
+ await Promise.all(runtimeVersions.map(async ({ runtimeVersion, platform }) => {
137
+ const rollbackUrl = new URL(`${baseUrl}/${appId}/rollback/${branch}`);
138
+ rollbackUrl.searchParams.set('commitHash', commitHash ?? '');
139
+ rollbackUrl.searchParams.set('platform', platform);
140
+ rollbackUrl.searchParams.set('runtimeVersion', runtimeVersion ?? '');
141
+ const response = await (0, fetch_1.fetchWithRetries)(rollbackUrl.toString(), {
142
+ method: 'POST',
143
+ headers: {
144
+ ...(0, auth_1.getAuthExpoHeaders)(credentials),
145
+ },
146
+ });
147
+ if (!response.ok) {
148
+ erroredPlatforms.push({
149
+ platform,
150
+ reason: await response.text(),
151
+ });
152
+ }
153
+ }));
154
+ if (erroredPlatforms.length) {
155
+ rollbackSpinner.fail('❌ Rollback failed');
156
+ erroredPlatforms.forEach(({ platform, reason }) => {
157
+ log_1.default.error(`Failed to publish rollback for ${platform}: ${reason}`);
158
+ });
159
+ process.exit(1);
160
+ }
161
+ else {
162
+ rollbackSpinner.succeed('✅ Rollback published successfully');
163
+ }
164
+ }
165
+ }
166
+ exports.default = Publish;
@@ -7,13 +7,24 @@ interface AssetToUpload {
7
7
  name: string;
8
8
  ext: string;
9
9
  }
10
- export declare function computeFilesRequests(projectDir: string, requestedPlatform: RequestedPlatform): AssetToUpload[];
10
+ export declare function computeFilesRequests(projectDir: string, outputDir: string, requestedPlatform: RequestedPlatform): AssetToUpload[];
11
11
  export interface RequestUploadUrlItem {
12
12
  requestUploadUrl: string;
13
13
  fileName: string;
14
14
  filePath: string;
15
15
  }
16
- export declare function requestUploadUrls(body: {
17
- fileNames: string[];
18
- }, requestUploadUrl: string, auth: ExpoCredentials, runtimeVersion: string): Promise<RequestUploadUrlItem[]>;
16
+ export declare function requestUploadUrls({ body, requestUploadUrl, auth, runtimeVersion, platform, commitHash, message, }: {
17
+ body: {
18
+ fileNames: string[];
19
+ };
20
+ requestUploadUrl: string;
21
+ auth: ExpoCredentials;
22
+ runtimeVersion: string;
23
+ platform: string;
24
+ commitHash?: string;
25
+ message?: string;
26
+ }): Promise<{
27
+ uploadRequests: RequestUploadUrlItem[];
28
+ updateId: string;
29
+ }>;
19
30
  export {};
@@ -4,10 +4,10 @@ exports.requestUploadUrls = exports.computeFilesRequests = exports.MetadataJoi =
4
4
  const tslib_1 = require("tslib");
5
5
  const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
6
6
  const joi_1 = tslib_1.__importDefault(require("joi"));
7
- const node_fetch_1 = tslib_1.__importDefault(require("node-fetch"));
8
7
  const path_1 = tslib_1.__importDefault(require("path"));
9
8
  const auth_1 = require("./auth");
10
9
  const expoConfig_1 = require("./expoConfig");
10
+ const fetch_1 = require("./fetch");
11
11
  const log_1 = tslib_1.__importDefault(require("./log"));
12
12
  const fileMetadataJoi = joi_1.default.object({
13
13
  assets: joi_1.default.array()
@@ -54,8 +54,8 @@ function loadMetadata(distRoot) {
54
54
  log_1.default.debug(`Loaded ${platforms.length} platform(s): ${platforms.join(', ')}`);
55
55
  return metadata;
56
56
  }
57
- function computeFilesRequests(projectDir, requestedPlatform) {
58
- const metadata = loadMetadata(path_1.default.join(projectDir, 'dist'));
57
+ function computeFilesRequests(projectDir, outputDir, requestedPlatform) {
58
+ const metadata = loadMetadata(path_1.default.join(projectDir, outputDir));
59
59
  const assets = [
60
60
  { path: 'metadata.json', name: 'metadata.json', ext: 'json' },
61
61
  { path: 'expoConfig.json', name: 'expoConfig.json', ext: 'json' },
@@ -73,17 +73,26 @@ function computeFilesRequests(projectDir, requestedPlatform) {
73
73
  return assets;
74
74
  }
75
75
  exports.computeFilesRequests = computeFilesRequests;
76
- async function requestUploadUrls(body, requestUploadUrl, auth, runtimeVersion) {
77
- const response = await (0, node_fetch_1.default)(`${requestUploadUrl}?runtimeVersion=${runtimeVersion}`, {
76
+ async function requestUploadUrls({ body, requestUploadUrl, auth, runtimeVersion, platform, commitHash, message, }) {
77
+ const uploadUrl = new URL(requestUploadUrl);
78
+ uploadUrl.searchParams.set('runtimeVersion', runtimeVersion);
79
+ uploadUrl.searchParams.set('platform', platform);
80
+ uploadUrl.searchParams.set('commitHash', commitHash ?? '');
81
+ const requestBody = { ...body };
82
+ if (message) {
83
+ requestBody.message = message;
84
+ }
85
+ const response = await (0, fetch_1.fetchWithRetries)(uploadUrl.toString(), {
78
86
  method: 'POST',
79
87
  headers: {
80
88
  ...(0, auth_1.getAuthExpoHeaders)(auth),
81
89
  'Content-Type': 'application/json',
82
90
  },
83
- body: JSON.stringify(body),
91
+ body: JSON.stringify(requestBody),
84
92
  });
85
93
  if (!response.ok) {
86
- throw new Error(`Failed to request upload URL`);
94
+ const text = await response.text();
95
+ throw new Error(`Failed to request upload URL: ${text}`);
87
96
  }
88
97
  return await response.json();
89
98
  }
@@ -14,10 +14,13 @@ export interface ExpoConfigOptions {
14
14
  env?: Env;
15
15
  skipSDKVersionRequirement?: boolean;
16
16
  skipPlugins?: boolean;
17
+ packageRunner?: string;
17
18
  }
18
19
  export declare function getPrivateExpoConfigAsync(projectDir: string, opts?: ExpoConfigOptions): Promise<ExpoConfig>;
19
20
  export declare function ensureExpoConfigExists(projectDir: string): void;
20
21
  export declare function isUsingStaticExpoConfig(projectDir: string): boolean;
21
22
  export declare function getPublicExpoConfigAsync(projectDir: string, opts?: ExpoConfigOptions): Promise<PublicExpoConfig>;
22
23
  export declare function getExpoConfigUpdateUrl(config: ExpoConfig): string | undefined;
24
+ export declare function requireExpoAppId(config: ExpoConfig): string;
23
25
  export declare function createOrModifyExpoConfigAsync(projectDir: string, exp: Partial<ExpoConfig>): Promise<void>;
26
+ export declare function resolveServerUrl(config: ExpoConfig): Promise<string>;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createOrModifyExpoConfigAsync = exports.getExpoConfigUpdateUrl = exports.getPublicExpoConfigAsync = exports.isUsingStaticExpoConfig = exports.ensureExpoConfigExists = exports.getPrivateExpoConfigAsync = exports.RequestedPlatform = void 0;
3
+ exports.resolveServerUrl = exports.createOrModifyExpoConfigAsync = exports.requireExpoAppId = exports.getExpoConfigUpdateUrl = exports.getPublicExpoConfigAsync = exports.isUsingStaticExpoConfig = exports.ensureExpoConfigExists = exports.getPrivateExpoConfigAsync = exports.RequestedPlatform = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  // This file is copied from eas-cli[https://github.com/expo/eas-cli] to ensure consistent user experience across the CLI.
6
6
  const config_1 = require("@expo/config");
@@ -11,6 +11,7 @@ const jscodeshift_1 = tslib_1.__importDefault(require("jscodeshift"));
11
11
  const path_1 = tslib_1.__importDefault(require("path"));
12
12
  const log_1 = tslib_1.__importDefault(require("./log"));
13
13
  const package_1 = require("./package");
14
+ const packageRunner_1 = require("./packageRunner");
14
15
  var RequestedPlatform;
15
16
  (function (RequestedPlatform) {
16
17
  RequestedPlatform["Android"] = "android";
@@ -27,8 +28,9 @@ async function getExpoConfigInternalAsync(projectDir, opts = {}) {
27
28
  };
28
29
  let exp;
29
30
  if ((0, package_1.isExpoInstalled)(projectDir)) {
31
+ const runner = (0, packageRunner_1.resolvePackageRunner)(opts.packageRunner, projectDir);
30
32
  try {
31
- const { stdout } = await (0, spawn_async_1.default)('npx', ['expo', 'config', '--json', ...(opts.isPublicConfig ? ['--type', 'public'] : [])], {
33
+ const { stdout } = await (0, spawn_async_1.default)(runner, ['expo', 'config', '--json', ...(opts.isPublicConfig ? ['--type', 'public'] : [])], {
32
34
  cwd: projectDir,
33
35
  env: {
34
36
  ...process.env,
@@ -40,7 +42,7 @@ async function getExpoConfigInternalAsync(projectDir, opts = {}) {
40
42
  }
41
43
  catch (err) {
42
44
  if (!wasExpoConfigWarnPrinted) {
43
- log_1.default.warn(`Failed to read the app config from the project using "npx expo config" command: ${err.message}.`);
45
+ log_1.default.warn(`Failed to read the app config from the project using "${runner} expo config" command: ${err.message}.`);
44
46
  log_1.default.warn('Falling back to the version of "@expo/config" shipped with the EAS CLI.');
45
47
  wasExpoConfigWarnPrinted = true;
46
48
  }
@@ -109,6 +111,18 @@ function getExpoConfigUpdateUrl(config) {
109
111
  return config.updates?.url;
110
112
  }
111
113
  exports.getExpoConfigUpdateUrl = getExpoConfigUpdateUrl;
114
+ function requireExpoAppId(config) {
115
+ const appId = config.updates
116
+ ?.requestHeaders?.['expo-app-id'];
117
+ if (!appId) {
118
+ log_1.default.error("Your Expo config is missing the 'expo-app-id' entry in updates.requestHeaders.");
119
+ log_1.default.error("This usually means you're running eoas v2+ against a v1-style single-app config or your config is missing the 'expo-app-id' entry.");
120
+ log_1.default.error("Fix: run 'npx eoas init' to migrate, or pin to the previous CLI via 'npx eoas@1 ...'.");
121
+ process.exit(1);
122
+ }
123
+ return appId;
124
+ }
125
+ exports.requireExpoAppId = requireExpoAppId;
112
126
  async function createOrModifyExpoConfigAsync(projectDir, exp) {
113
127
  try {
114
128
  ensureExpoConfigExists(projectDir);
@@ -196,3 +210,19 @@ function createValueNode(j, value) {
196
210
  function stringifyWithEnv(obj) {
197
211
  return JSON.stringify(obj, null, 2).replace(/"process\.env\.(\w+)"/g, 'process.env.$1');
198
212
  }
213
+ async function resolveServerUrl(config) {
214
+ const updateUrl = config.updates?.url;
215
+ if (!updateUrl) {
216
+ throw new Error('No update URL found in the Expo config.');
217
+ }
218
+ let baseUrl;
219
+ try {
220
+ const parsedUrl = new URL(updateUrl);
221
+ baseUrl = parsedUrl.origin;
222
+ }
223
+ catch {
224
+ throw new Error('Invalid update URL.');
225
+ }
226
+ return baseUrl;
227
+ }
228
+ exports.resolveServerUrl = resolveServerUrl;
@@ -0,0 +1,2 @@
1
+ import { RequestInit, Response } from 'node-fetch';
2
+ export declare function fetchWithRetries(url: string, options: RequestInit): Promise<Response>;
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fetchWithRetries = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const fetch_retry_1 = tslib_1.__importDefault(require("fetch-retry"));
6
+ const node_fetch_1 = tslib_1.__importDefault(require("node-fetch"));
7
+ const log_1 = tslib_1.__importDefault(require("./log"));
8
+ const fetch = (0, fetch_retry_1.default)(node_fetch_1.default);
9
+ async function fetchWithRetries(url, options) {
10
+ return await fetch(url, {
11
+ ...options,
12
+ retryDelay(attempt) {
13
+ return Math.pow(2, attempt) * 500;
14
+ },
15
+ retryOn: (attempt, error) => {
16
+ if (attempt > 3) {
17
+ return false;
18
+ }
19
+ if (error) {
20
+ log_1.default.warn(`Retry ${attempt} after network error:`, error.message);
21
+ return true;
22
+ }
23
+ return false;
24
+ },
25
+ });
26
+ }
27
+ exports.fetchWithRetries = fetchWithRetries;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Resolves the package runner command to use for spawning Expo CLI commands.
3
+ *
4
+ * Priority:
5
+ * 1. Explicit value passed as argument (e.g. from --packageRunner CLI flag)
6
+ * 2. EOAS_PACKAGE_RUNNER environment variable
7
+ * 3. Inferred from packageManager field in package.json
8
+ * 4. Falls back to 'npx'
9
+ *
10
+ * Supported values: npx, bunx, pnpx, or any other package runner binary.
11
+ */
12
+ export declare function resolvePackageRunner(explicit?: string, projectDir?: string): string;
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolvePackageRunner = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
6
+ const path_1 = tslib_1.__importDefault(require("path"));
7
+ const DEFAULT_PACKAGE_RUNNER = 'npx';
8
+ const VALID_RUNNER_RE = /^[a-zA-Z0-9._-]+$/;
9
+ function assertValidRunner(value, source) {
10
+ if (!VALID_RUNNER_RE.test(value)) {
11
+ throw new Error(`Invalid package runner "${value}" (from ${source}). Expected a simple binary name like npx, bunx or pnpx.`);
12
+ }
13
+ }
14
+ const PACKAGE_MANAGER_RUNNERS = {
15
+ bun: 'bunx',
16
+ pnpm: 'pnpx',
17
+ yarn: 'npx',
18
+ npm: 'npx',
19
+ };
20
+ /**
21
+ * Resolves the package runner command to use for spawning Expo CLI commands.
22
+ *
23
+ * Priority:
24
+ * 1. Explicit value passed as argument (e.g. from --packageRunner CLI flag)
25
+ * 2. EOAS_PACKAGE_RUNNER environment variable
26
+ * 3. Inferred from packageManager field in package.json
27
+ * 4. Falls back to 'npx'
28
+ *
29
+ * Supported values: npx, bunx, pnpx, or any other package runner binary.
30
+ */
31
+ function resolvePackageRunner(explicit, projectDir) {
32
+ if (explicit) {
33
+ assertValidRunner(explicit, '--packageRunner flag');
34
+ return explicit;
35
+ }
36
+ if (process.env.EOAS_PACKAGE_RUNNER) {
37
+ assertValidRunner(process.env.EOAS_PACKAGE_RUNNER, 'EOAS_PACKAGE_RUNNER environment variable');
38
+ return process.env.EOAS_PACKAGE_RUNNER;
39
+ }
40
+ if (projectDir) {
41
+ const detected = detectRunnerFromPackageJson(projectDir);
42
+ if (detected)
43
+ return detected;
44
+ }
45
+ return DEFAULT_PACKAGE_RUNNER;
46
+ }
47
+ exports.resolvePackageRunner = resolvePackageRunner;
48
+ /**
49
+ * Walks up from projectDir to find a package.json with a packageManager field
50
+ * and maps it to the corresponding package runner binary.
51
+ */
52
+ function detectRunnerFromPackageJson(startDir) {
53
+ let dir = path_1.default.resolve(startDir);
54
+ const root = path_1.default.parse(dir).root;
55
+ while (dir !== root) {
56
+ const pkgPath = path_1.default.join(dir, 'package.json');
57
+ try {
58
+ if (fs_extra_1.default.existsSync(pkgPath)) {
59
+ const pkg = fs_extra_1.default.readJsonSync(pkgPath);
60
+ if (pkg.packageManager) {
61
+ const name = pkg.packageManager.split('@')[0];
62
+ return PACKAGE_MANAGER_RUNNERS[name];
63
+ }
64
+ }
65
+ }
66
+ catch {
67
+ // Ignore read errors, keep walking up
68
+ }
69
+ dir = path_1.default.dirname(dir);
70
+ }
71
+ return undefined;
72
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eoas",
3
- "version": "1.0.38",
3
+ "version": "2.0.0-alpha.1",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "build": "tsc --project tsconfig.json",
@@ -23,7 +23,7 @@
23
23
  "repository": "axelmarciano/expo-open-ota",
24
24
  "dependencies": {
25
25
  "@expo/code-signing-certificates": "^0.0.5",
26
- "@expo/config": "10.0.6",
26
+ "@expo/config": "10.0.11",
27
27
  "@expo/config-plugins": "9.0.12",
28
28
  "@expo/eas-build-job": "1.0.165",
29
29
  "@expo/fingerprint": "^0.11.7",
@@ -35,7 +35,9 @@
35
35
  "@urql/exchange-retry": "1.2.0",
36
36
  "better-opn": "3.0.2",
37
37
  "chalk": "4.1.2",
38
+ "eslint": "^8.57.1",
38
39
  "fast-glob": "3.3.2",
40
+ "fetch-retry": "^6.0.0",
39
41
  "figures": "3.2.0",
40
42
  "file-type": "^20.0.0",
41
43
  "form-data": "^4.0.1",
@@ -47,19 +49,18 @@
47
49
  "ignore": "5.3.0",
48
50
  "joi": "17.11.0",
49
51
  "jscodeshift": "^17.1.2",
52
+ "log-symbols": "^4.0.0",
50
53
  "mime": "3.0.0",
51
54
  "node-fetch": "^2.6.7",
52
55
  "ora": "^5.1.0",
56
+ "prettier": "3.1.1",
53
57
  "prompts": "^2.4.2",
54
58
  "recast": "^0.23.9",
55
59
  "resolve-from": "5.0.0",
56
60
  "semver": "7.5.4",
57
61
  "tar": "6.2.1",
58
62
  "terminal-link": "2.1.1",
59
- "uuid": "9.0.1",
60
- "eslint": "^8.57.1",
61
- "prettier": "3.1.1",
62
- "log-symbols": "^4.0.0"
63
+ "uuid": "9.0.1"
63
64
  },
64
65
  "devDependencies": {
65
66
  "@babel/parser": "^7.26.7",