@ship-et/buildatlas-cli 0.12.0 → 0.14.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 (76) hide show
  1. package/dist/build-source.d.ts +57 -0
  2. package/dist/build-source.d.ts.map +1 -0
  3. package/dist/build-source.js +84 -0
  4. package/dist/build-source.js.map +1 -0
  5. package/dist/build-source.test.d.ts +2 -0
  6. package/dist/build-source.test.d.ts.map +1 -0
  7. package/dist/build-source.test.js +86 -0
  8. package/dist/build-source.test.js.map +1 -0
  9. package/dist/client.d.ts.map +1 -1
  10. package/dist/client.js +8 -0
  11. package/dist/client.js.map +1 -1
  12. package/dist/client.test.js +31 -0
  13. package/dist/client.test.js.map +1 -1
  14. package/dist/commands/build.d.ts.map +1 -1
  15. package/dist/commands/build.js +53 -27
  16. package/dist/commands/build.js.map +1 -1
  17. package/dist/commands/credentials-wizard.d.ts +1 -1
  18. package/dist/commands/credentials-wizard.d.ts.map +1 -1
  19. package/dist/commands/credentials-wizard.js +20 -0
  20. package/dist/commands/credentials-wizard.js.map +1 -1
  21. package/dist/commands/credentials.d.ts.map +1 -1
  22. package/dist/commands/credentials.js +11 -3
  23. package/dist/commands/credentials.js.map +1 -1
  24. package/dist/commands/migrate/auth.d.ts +9 -0
  25. package/dist/commands/migrate/auth.d.ts.map +1 -0
  26. package/dist/commands/migrate/auth.js +8 -0
  27. package/dist/commands/migrate/auth.js.map +1 -0
  28. package/dist/commands/migrate/build-numbers.d.ts +12 -0
  29. package/dist/commands/migrate/build-numbers.d.ts.map +1 -0
  30. package/dist/commands/migrate/build-numbers.js +34 -0
  31. package/dist/commands/migrate/build-numbers.js.map +1 -0
  32. package/dist/commands/migrate/credentials.d.ts +24 -0
  33. package/dist/commands/migrate/credentials.d.ts.map +1 -0
  34. package/dist/commands/migrate/credentials.js +78 -0
  35. package/dist/commands/migrate/credentials.js.map +1 -0
  36. package/dist/commands/migrate/eas-client.d.ts +88 -0
  37. package/dist/commands/migrate/eas-client.d.ts.map +1 -0
  38. package/dist/commands/migrate/eas-client.js +186 -0
  39. package/dist/commands/migrate/eas-client.js.map +1 -0
  40. package/dist/commands/migrate/expo-session.d.ts +10 -0
  41. package/dist/commands/migrate/expo-session.d.ts.map +1 -0
  42. package/dist/commands/migrate/expo-session.js +51 -0
  43. package/dist/commands/migrate/expo-session.js.map +1 -0
  44. package/dist/commands/migrate/index.d.ts +3 -0
  45. package/dist/commands/migrate/index.d.ts.map +1 -0
  46. package/dist/commands/migrate/index.js +117 -0
  47. package/dist/commands/migrate/index.js.map +1 -0
  48. package/dist/commands/migrate/link-project.d.ts +13 -0
  49. package/dist/commands/migrate/link-project.d.ts.map +1 -0
  50. package/dist/commands/migrate/link-project.js +74 -0
  51. package/dist/commands/migrate/link-project.js.map +1 -0
  52. package/dist/commands/migrate/retry.d.ts +8 -0
  53. package/dist/commands/migrate/retry.d.ts.map +1 -0
  54. package/dist/commands/migrate/retry.js +25 -0
  55. package/dist/commands/migrate/retry.js.map +1 -0
  56. package/dist/config.d.ts +2 -1
  57. package/dist/config.d.ts.map +1 -1
  58. package/dist/config.js +12 -3
  59. package/dist/config.js.map +1 -1
  60. package/dist/index.js +4 -0
  61. package/dist/index.js.map +1 -1
  62. package/dist/profiles.test.js +10 -0
  63. package/dist/profiles.test.js.map +1 -1
  64. package/dist/project-hint.d.ts.map +1 -1
  65. package/dist/project-hint.js +9 -5
  66. package/dist/project-hint.js.map +1 -1
  67. package/dist/spinner.d.ts +8 -0
  68. package/dist/spinner.d.ts.map +1 -0
  69. package/dist/spinner.js +35 -0
  70. package/dist/spinner.js.map +1 -0
  71. package/dist/tarball.d.ts.map +1 -1
  72. package/dist/tarball.js +39 -0
  73. package/dist/tarball.js.map +1 -1
  74. package/dist/tarball.test.js +54 -0
  75. package/dist/tarball.test.js.map +1 -1
  76. package/package.json +1 -1
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Listing an Expo account's EAS projects.
3
+ *
4
+ * `meActor` and `app.byId` are proven elsewhere in this repo — BuildAtlas's own
5
+ * server (`packages/shared/src/expo-auth.ts`) uses both to validate a token and
6
+ * check project ownership. `account.byName(...).apps` is not proven anywhere in
7
+ * this repo; it matches the pattern eas-cli itself uses, but if Expo's schema
8
+ * has drifted this throws the raw GraphQL error rather than a wrong-looking
9
+ * empty list.
10
+ */
11
+ const EXPO_GRAPHQL_URL = 'https://api.expo.dev/graphql';
12
+ async function expoGraphQL(auth, query, variables) {
13
+ let res;
14
+ try {
15
+ res = await fetch(EXPO_GRAPHQL_URL, {
16
+ method: 'POST',
17
+ headers: {
18
+ ...(auth.kind === 'token'
19
+ ? { authorization: `Bearer ${auth.token}` }
20
+ : { 'expo-session': auth.sessionSecret }),
21
+ 'content-type': 'application/json',
22
+ },
23
+ body: JSON.stringify({ query, variables }),
24
+ signal: AbortSignal.timeout(15_000),
25
+ });
26
+ }
27
+ catch (err) {
28
+ throw new Error(`Could not reach api.expo.dev: ${err.message}`);
29
+ }
30
+ if (res.status === 401 || res.status === 403) {
31
+ throw new Error(auth.kind === 'token' ? 'Expo rejected this token.' : 'Expo rejected this session.');
32
+ }
33
+ const body = (await res.json().catch(() => undefined));
34
+ const error = body?.errors?.[0];
35
+ if (error)
36
+ throw new Error(`Expo API: ${error.message ?? 'unknown error'}`);
37
+ if (!body?.data)
38
+ throw new Error(`Expo API returned no data (HTTP ${res.status}).`);
39
+ return body.data;
40
+ }
41
+ /**
42
+ * The last version Expo submitted to a store for this app, per platform — mirrors
43
+ * eas-cli's own `AppVersionQuery.latestVersionAsync` query (`latestAppVersionByPlatformAndApplicationIdentifier`).
44
+ * `applicationIdentifier` is the bundle id (iOS) or package name (Android); Expo
45
+ * tracks versions per identifier since an app can rebrand its bundle id over time.
46
+ */
47
+ export async function getLatestBuildVersion(auth, appId, platform, applicationIdentifier) {
48
+ const { app } = await expoGraphQL(auth, `query LatestAppVersion($appId: String!, $platform: AppPlatform!, $applicationIdentifier: String!) {
49
+ app {
50
+ byId(appId: $appId) {
51
+ id
52
+ latestAppVersionByPlatformAndApplicationIdentifier(
53
+ platform: $platform
54
+ applicationIdentifier: $applicationIdentifier
55
+ ) {
56
+ id
57
+ buildVersion
58
+ }
59
+ }
60
+ }
61
+ }`, { appId, platform: platform === 'ios' ? 'IOS' : 'ANDROID', applicationIdentifier });
62
+ const raw = app.byId.latestAppVersionByPlatformAndApplicationIdentifier?.buildVersion;
63
+ if (!raw)
64
+ return undefined;
65
+ const parsed = Number.parseInt(raw, 10);
66
+ return Number.isFinite(parsed) ? parsed : undefined;
67
+ }
68
+ /**
69
+ * The bundle id / package name EAS already has on file for this app, read off its
70
+ * credentials (`appleAppIdentifier.bundleIdentifier`, `androidAppCredentials.applicationIdentifier`)
71
+ * rather than a local app.json — so migrate works from any directory, not just
72
+ * the project's own checkout. Empty when the app has never had credentials
73
+ * generated on EAS for that platform.
74
+ */
75
+ export async function getAppIdentifiers(auth, appId) {
76
+ const { app } = await expoGraphQL(auth, `query AppIdentifiers($appId: String!) {
77
+ app {
78
+ byId(appId: $appId) {
79
+ id
80
+ iosAppCredentials { appleAppIdentifier { bundleIdentifier } }
81
+ androidAppCredentials { applicationIdentifier }
82
+ }
83
+ }
84
+ }`, { appId });
85
+ return {
86
+ bundleIdentifier: app.byId.iosAppCredentials[0]?.appleAppIdentifier.bundleIdentifier,
87
+ androidPackage: app.byId.androidAppCredentials.find((c) => c.applicationIdentifier)?.applicationIdentifier ?? undefined,
88
+ };
89
+ }
90
+ /**
91
+ * The actual signing material EAS holds for this app — a distribution cert
92
+ * (+ its provisioning profile) for iOS, a keystore for Android, and an APNs
93
+ * push key if one was ever generated for push notifications — in the same
94
+ * base64 shape `buildatlas credentials add` already expects (`ios_dist_cert`,
95
+ * `ios_provisioning_profile`, `android_keystore`, `ios_push_key`), so it can be
96
+ * forwarded as-is.
97
+ *
98
+ * Picks the App Store build credentials for iOS (ad-hoc/enterprise/development
99
+ * profiles are for internal testing, not what a migrated app should ship with)
100
+ * and whichever Android keystore is marked default. Omits a platform, or the
101
+ * push key, entirely rather than guessing when EAS has never generated one.
102
+ *
103
+ * The push key is unrelated to whether OneSignal is used: OneSignal does not
104
+ * read from EAS at all — it needs the same .p8 uploaded to its own dashboard,
105
+ * which this only pulls into BuildAtlas's vault for safekeeping and for the
106
+ * worker to warn about when a project looks push-capable but has none stored.
107
+ */
108
+ export async function getEasCredentials(auth, appId) {
109
+ const { app } = await expoGraphQL(auth, `query AppCredentials($appId: String!) {
110
+ app {
111
+ byId(appId: $appId) {
112
+ id
113
+ iosAppCredentials {
114
+ pushKey { keyIdentifier keyP8 }
115
+ iosAppBuildCredentialsList {
116
+ iosDistributionType
117
+ distributionCertificate { certificateP12 certificatePassword }
118
+ provisioningProfile { provisioningProfile }
119
+ }
120
+ }
121
+ androidAppCredentials {
122
+ androidAppBuildCredentialsList {
123
+ isDefault
124
+ androidKeystore { keystore keystorePassword keyAlias keyPassword }
125
+ }
126
+ }
127
+ }
128
+ }
129
+ }`, { appId });
130
+ const iosBuildCredentials = app.byId.iosAppCredentials.flatMap((c) => c.iosAppBuildCredentialsList);
131
+ const iosEntry = iosBuildCredentials.find((c) => c.iosDistributionType === 'APP_STORE' && c.distributionCertificate?.certificateP12) ??
132
+ iosBuildCredentials.find((c) => c.distributionCertificate?.certificateP12);
133
+ const androidBuildCredentials = app.byId.androidAppCredentials.flatMap((c) => c.androidAppBuildCredentialsList);
134
+ const androidEntry = androidBuildCredentials.find((c) => c.isDefault && c.androidKeystore) ??
135
+ androidBuildCredentials.find((c) => c.androidKeystore);
136
+ const pushKeyEntry = app.byId.iosAppCredentials.find((c) => c.pushKey)?.pushKey;
137
+ return {
138
+ ios: iosEntry?.distributionCertificate?.certificateP12
139
+ ? {
140
+ p12Base64: iosEntry.distributionCertificate.certificateP12,
141
+ password: iosEntry.distributionCertificate.certificatePassword ?? undefined,
142
+ profileBase64: iosEntry.provisioningProfile?.provisioningProfile ?? undefined,
143
+ }
144
+ : undefined,
145
+ android: androidEntry?.androidKeystore
146
+ ? {
147
+ keystoreBase64: androidEntry.androidKeystore.keystore,
148
+ keystorePassword: androidEntry.androidKeystore.keystorePassword,
149
+ keyAlias: androidEntry.androidKeystore.keyAlias,
150
+ keyPassword: androidEntry.androidKeystore.keyPassword ?? undefined,
151
+ }
152
+ : undefined,
153
+ iosPushKey: pushKeyEntry
154
+ ? { keyId: pushKeyEntry.keyIdentifier, p8Base64: pushKeyEntry.keyP8 }
155
+ : undefined,
156
+ };
157
+ }
158
+ /** Every app in every account this token or session can see. */
159
+ export async function listExpoApps(auth) {
160
+ const { meActor } = await expoGraphQL(auth, `{ meActor { accounts { id name } } }`);
161
+ if (!meActor)
162
+ throw new Error('Expo did not recognise this token.');
163
+ if (meActor.accounts.length === 0)
164
+ return [];
165
+ const PAGE_SIZE = 50;
166
+ const apps = [];
167
+ for (const account of meActor.accounts) {
168
+ for (let offset = 0;; offset += PAGE_SIZE) {
169
+ const { account: acc } = await expoGraphQL(auth, `query AppsForAccount($accountName: String!, $limit: Int!, $offset: Int!) {
170
+ account {
171
+ byName(accountName: $accountName) {
172
+ apps(limit: $limit, offset: $offset) { id name slug fullName }
173
+ }
174
+ }
175
+ }`, { accountName: account.name, limit: PAGE_SIZE, offset });
176
+ const page = acc.byName?.apps ?? [];
177
+ for (const app of page) {
178
+ apps.push({ ...app, accountName: account.name });
179
+ }
180
+ if (page.length < PAGE_SIZE)
181
+ break;
182
+ }
183
+ }
184
+ return apps;
185
+ }
186
+ //# sourceMappingURL=eas-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eas-client.js","sourceRoot":"","sources":["../../../src/commands/migrate/eas-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * An existing EAS/Expo session, or runs `eas login` interactively and reads
3
+ * the session it leaves behind. Lets this script skip the "paste a personal
4
+ * access token" step for anyone who already has `eas-cli` set up.
5
+ */
6
+ export declare function getExpoSession(): Promise<{
7
+ sessionSecret: string;
8
+ username?: string;
9
+ }>;
10
+ //# sourceMappingURL=expo-session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"expo-session.d.ts","sourceRoot":"","sources":["../../../src/commands/migrate/expo-session.ts"],"names":[],"mappings":"AAqCA;;;;GAIG;AACH,wBAAsB,cAAc,IAAI,OAAO,CAAC;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAc5F"}
@@ -0,0 +1,51 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ /**
6
+ * `eas login` / `expo login` both write here. This is the same file `eas
7
+ * whoami` reads to work without EXPO_TOKEN — the GraphQL API accepts a
8
+ * session secret via the `expo-session` header exactly like it accepts a
9
+ * personal access token via `Authorization: Bearer`.
10
+ */
11
+ async function readExpoState() {
12
+ try {
13
+ const raw = await readFile(join(homedir(), '.expo', 'state.json'), 'utf8');
14
+ const parsed = JSON.parse(raw);
15
+ return parsed.auth?.sessionSecret ? parsed.auth : undefined;
16
+ }
17
+ catch {
18
+ return undefined;
19
+ }
20
+ }
21
+ function runEasLogin() {
22
+ return new Promise((resolve, reject) => {
23
+ const child = spawn('npx', ['--yes', 'eas-cli', 'login'], { stdio: 'inherit' });
24
+ child.on('error', reject);
25
+ child.on('exit', (code) => {
26
+ if (code === 0)
27
+ resolve();
28
+ else
29
+ reject(new Error(`\`eas login\` exited with code ${code}`));
30
+ });
31
+ });
32
+ }
33
+ /**
34
+ * An existing EAS/Expo session, or runs `eas login` interactively and reads
35
+ * the session it leaves behind. Lets this script skip the "paste a personal
36
+ * access token" step for anyone who already has `eas-cli` set up.
37
+ */
38
+ export async function getExpoSession() {
39
+ const existing = await readExpoState();
40
+ if (existing?.sessionSecret) {
41
+ return { sessionSecret: existing.sessionSecret, username: existing.username };
42
+ }
43
+ console.log('Not logged in to Expo — running `eas login`…');
44
+ await runEasLogin();
45
+ const after = await readExpoState();
46
+ if (!after?.sessionSecret) {
47
+ throw new Error('`eas login` finished but left no session. Pass --expo-token instead.');
48
+ }
49
+ return { sessionSecret: after.sessionSecret, username: after.username };
50
+ }
51
+ //# sourceMappingURL=expo-session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"expo-session.js","sourceRoot":"","sources":["../../../src/commands/migrate/expo-session.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAOjC;;;;;GAKG;AACH,KAAK,UAAU,aAAa;IAC1B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,YAAY,CAAC,EAAE,MAAM,CAAC,CAAC;QAC3E,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAA6B,CAAC;QAC3D,OAAO,MAAM,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;IAC9D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,SAAS,WAAW;IAClB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;QAChF,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC1B,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;YACxB,IAAI,IAAI,KAAK,CAAC;gBAAE,OAAO,EAAE,CAAC;;gBACrB,MAAM,CAAC,IAAI,KAAK,CAAC,kCAAkC,IAAI,EAAE,CAAC,CAAC,CAAC;QACnE,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc;IAClC,MAAM,QAAQ,GAAG,MAAM,aAAa,EAAE,CAAC;IACvC,IAAI,QAAQ,EAAE,aAAa,EAAE,CAAC;QAC5B,OAAO,EAAE,aAAa,EAAE,QAAQ,CAAC,aAAa,EAAE,QAAQ,EAAE,QAAQ,CAAC,QAAQ,EAAE,CAAC;IAChF,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,8CAA8C,CAAC,CAAC;IAC5D,MAAM,WAAW,EAAE,CAAC;IAEpB,MAAM,KAAK,GAAG,MAAM,aAAa,EAAE,CAAC;IACpC,IAAI,CAAC,KAAK,EAAE,aAAa,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,sEAAsE,CAAC,CAAC;IAC1F,CAAC;IACD,OAAO,EAAE,aAAa,EAAE,KAAK,CAAC,aAAa,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC;AAC1E,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerMigrateCommand(program: Command): void;
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/migrate/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAkBzC,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA4F7D"}
@@ -0,0 +1,117 @@
1
+ import { resolve } from 'node:path';
2
+ import pc from 'picocolors';
3
+ import { AtlasApiError, apiRequest } from '../../client.js';
4
+ import { readResolvedAppConfig } from '../../config.js';
5
+ import { withPrompter } from '../../prompt.js';
6
+ import { withSpinner } from '../../spinner.js';
7
+ import { requireAuth } from './auth.js';
8
+ import { continueBuildNumbers } from './build-numbers.js';
9
+ import { migrateCredentials } from './credentials.js';
10
+ import { getAppIdentifiers, listExpoApps } from './eas-client.js';
11
+ import { getExpoSession } from './expo-session.js';
12
+ import { offerToLinkProject } from './link-project.js';
13
+ export function registerMigrateCommand(program) {
14
+ program
15
+ .command('migrate')
16
+ .description('Pick an EAS project and create the matching BuildAtlas project')
17
+ .option('--expo-token <token>', 'Expo access token. Skips using an `eas login` session.')
18
+ .option('--slug <slug>', 'BuildAtlas slug to use (default: the EAS project slug)')
19
+ .option('--dir <path>', 'Project directory to read app.json / app.config from', process.cwd())
20
+ .action(async (opts) => {
21
+ await run(async () => {
22
+ const config = await requireAuth();
23
+ const dir = resolve(opts.dir);
24
+ const local = await readResolvedAppConfig(dir);
25
+ // A token, if given, is the explicit override. Otherwise reuse whatever
26
+ // `eas login` already set up rather than making someone paste a personal
27
+ // access token they may not even have handy.
28
+ const auth = opts.expoToken
29
+ ? { kind: 'token', token: opts.expoToken }
30
+ : process.env.EXPO_TOKEN
31
+ ? { kind: 'token', token: process.env.EXPO_TOKEN }
32
+ : await (async () => {
33
+ const session = await getExpoSession();
34
+ if (session.username) {
35
+ console.log(pc.dim(` using EAS session for ${session.username}`));
36
+ }
37
+ return { kind: 'session', sessionSecret: session.sessionSecret };
38
+ })();
39
+ const apps = await withSpinner('fetching EAS projects', () => listExpoApps(auth));
40
+ if (apps.length === 0)
41
+ throw new Error('That account has no EAS projects.');
42
+ const app = await pickApp(apps, local);
43
+ const slug = opts.slug ?? app.slug;
44
+ // Local app.json wins when it disagrees — it's the current source of truth for
45
+ // this checkout — but EAS's own credentials fill in whatever local didn't have,
46
+ // so migrate doesn't need to run from inside a fully-configured project dir.
47
+ const remote = await withSpinner('reading EAS credentials', () => getAppIdentifiers(auth, app.id)).catch(() => ({}));
48
+ const bundleIdentifier = local?.bundleIdentifier ?? remote.bundleIdentifier;
49
+ const androidPackage = local?.androidPackage ?? remote.androidPackage;
50
+ const existing = await apiRequest(config, `/projects/${slug}`, {
51
+ token: config.token,
52
+ }).catch((err) => {
53
+ if (err instanceof AtlasApiError && err.code === 'not_found')
54
+ return undefined;
55
+ throw err;
56
+ });
57
+ // Either way, end up with a project to migrate credentials into. A project
58
+ // that already exists is not a reason to stop — it is the common case for
59
+ // someone re-running `migrate` after rotating a certificate on EAS, and
60
+ // stopping here used to skip credentials entirely.
61
+ let project;
62
+ if (existing) {
63
+ console.log(`${pc.dim('already exists —')} ${pc.bold(existing.project.slug)}`);
64
+ project = existing.project;
65
+ }
66
+ else {
67
+ project = (await withSpinner('creating BuildAtlas project', () => apiRequest(config, '/projects', {
68
+ method: 'POST',
69
+ token: config.token,
70
+ body: JSON.stringify({
71
+ slug,
72
+ name: app.name,
73
+ easProjectId: app.id,
74
+ bundleIdentifier,
75
+ androidPackage,
76
+ }),
77
+ }))).project;
78
+ console.log(`${pc.green('✔')} created project ${pc.bold(project.slug)} from ${app.fullName}`);
79
+ }
80
+ await continueBuildNumbers(config, project.slug, auth, app.id, { bundleIdentifier, androidPackage });
81
+ await migrateCredentials(config, project.slug, auth, app.id, app.fullName);
82
+ if (!bundleIdentifier || !androidPackage) {
83
+ console.log(pc.dim(` Bundle id / package name were not both found in ${dir} or on EAS — fill in what's ` +
84
+ `missing with \`buildatlas projects set --project ${project.slug}\`.`));
85
+ }
86
+ await offerToLinkProject(dir, project.slug);
87
+ });
88
+ });
89
+ }
90
+ /**
91
+ * Skips the menu when the directory this ran from already points at one of the
92
+ * visible apps — matched by EAS project id first, since a slug is only unique
93
+ * within an account and `app.json` does not say which account it belongs to.
94
+ */
95
+ async function pickApp(apps, local) {
96
+ const detected = (local?.easProjectId && apps.find((a) => a.id === local.easProjectId)) ||
97
+ (local?.slug && apps.find((a) => a.slug === local.slug));
98
+ if (detected) {
99
+ console.log(pc.dim(` detected ${detected.fullName} from app config`));
100
+ return detected;
101
+ }
102
+ return withPrompter(async (p) => {
103
+ const chosen = await p.select('Which EAS project?', apps.map((a) => ({ value: a.fullName, label: a.fullName, hint: a.name })));
104
+ return apps.find((a) => a.fullName === chosen);
105
+ });
106
+ }
107
+ async function run(fn) {
108
+ try {
109
+ await fn();
110
+ }
111
+ catch (err) {
112
+ const message = err instanceof AtlasApiError ? `${err.message} (${err.code})` : err.message;
113
+ console.error(pc.red('✖'), message);
114
+ process.exitCode = 1;
115
+ }
116
+ }
117
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/commands/migrate/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Points a local checkout's `app.json` at the migrated BuildAtlas project, via
3
+ * the same `expo.extra.buildatlas.project` field `resolveProject` already reads
4
+ * (config.ts) — so `buildatlas build` and friends work from this directory
5
+ * without `--project` or `$BUILDATLAS_PROJECT` from here on.
6
+ *
7
+ * Only touches a static app.json: a dynamic `app.config.js`/`.ts` is code, not
8
+ * data, and rewriting it would mean guessing where to inject the field. When
9
+ * there is no app.json at all — a project configured purely through a dynamic
10
+ * config — `offerToCreateAppJson` below handles that instead.
11
+ */
12
+ export declare function offerToLinkProject(dir: string, slug: string): Promise<void>;
13
+ //# sourceMappingURL=link-project.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link-project.d.ts","sourceRoot":"","sources":["../../../src/commands/migrate/link-project.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;GAUG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA6CjF"}
@@ -0,0 +1,74 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import pc from 'picocolors';
4
+ import { dynamicConfigIn } from '../../config.js';
5
+ import { withPrompter } from '../../prompt.js';
6
+ /**
7
+ * Points a local checkout's `app.json` at the migrated BuildAtlas project, via
8
+ * the same `expo.extra.buildatlas.project` field `resolveProject` already reads
9
+ * (config.ts) — so `buildatlas build` and friends work from this directory
10
+ * without `--project` or `$BUILDATLAS_PROJECT` from here on.
11
+ *
12
+ * Only touches a static app.json: a dynamic `app.config.js`/`.ts` is code, not
13
+ * data, and rewriting it would mean guessing where to inject the field. When
14
+ * there is no app.json at all — a project configured purely through a dynamic
15
+ * config — `offerToCreateAppJson` below handles that instead.
16
+ */
17
+ export async function offerToLinkProject(dir, slug) {
18
+ const path = join(dir, 'app.json');
19
+ const raw = await readFile(path, 'utf8').catch(() => undefined);
20
+ if (raw === undefined) {
21
+ await offerToCreateAppJson(dir, path, slug);
22
+ return;
23
+ }
24
+ let parsed;
25
+ try {
26
+ parsed = JSON.parse(raw);
27
+ }
28
+ catch {
29
+ return;
30
+ }
31
+ const expo = parsed.expo ?? {};
32
+ const current = expo.extra?.buildatlas?.project ?? expo.slug;
33
+ if (current === slug || !process.stdin.isTTY)
34
+ return;
35
+ const link = await withPrompter((p) => p.confirm(`Point app.json at BuildAtlas project "${slug}" for this directory?`, true));
36
+ if (!link)
37
+ return;
38
+ expo.extra = { ...expo.extra, buildatlas: { ...expo.extra?.buildatlas, project: slug } };
39
+ parsed.expo = expo;
40
+ await writeFile(path, `${JSON.stringify(parsed, null, 2)}\n`);
41
+ console.log(`${pc.green('✔')} app.json now points \`buildatlas\` commands at ${pc.bold(slug)}`);
42
+ // Both files existing is common — an app.config.js/ts that does `import base
43
+ // from './app.json'` and layers plugins/env on top. resolveProject reads app.json
44
+ // directly either way, so this write takes effect for `buildatlas` regardless —
45
+ // but `expo config` itself only sees it if the dynamic config actually spreads
46
+ // app.json's `extra`, which we cannot check without running that file's code.
47
+ const dynamic = await dynamicConfigIn(dir);
48
+ if (dynamic) {
49
+ console.log(pc.dim(` Note: ${dynamic} is also here. \`buildatlas\` reads app.json directly, so this ` +
50
+ `takes effect either way — but if ${dynamic} sets "extra" without spreading ` +
51
+ `app.json's, \`expo config\` itself won't show it.`));
52
+ }
53
+ }
54
+ /**
55
+ * No static app.json — if there's a dynamic app.config instead, editing its
56
+ * JavaScript isn't safe to automate. But `resolveProject` (config.ts) checks
57
+ * app.json before it ever evaluates a dynamic config, and its own
58
+ * `noProjectAdvice` documents exactly this escape hatch: a minimal app.json
59
+ * holding only the buildatlas link, which reads cleanly alongside app.config.js
60
+ * without the two ever needing to agree on anything else.
61
+ */
62
+ async function offerToCreateAppJson(dir, path, slug) {
63
+ const dynamic = await dynamicConfigIn(dir);
64
+ if (!dynamic || !process.stdin.isTTY)
65
+ return;
66
+ const create = await withPrompter((p) => p.confirm(`No app.json here (found ${dynamic} instead). Create one just to point \`buildatlas\` ` +
67
+ `commands at project "${slug}"?`, true));
68
+ if (!create)
69
+ return;
70
+ const content = { expo: { extra: { buildatlas: { project: slug } } } };
71
+ await writeFile(path, `${JSON.stringify(content, null, 2)}\n`);
72
+ console.log(`${pc.green('✔')} wrote ${pc.bold('app.json')} — \`buildatlas\` commands from here now use ${pc.bold(slug)}`);
73
+ }
74
+ //# sourceMappingURL=link-project.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link-project.js","sourceRoot":"","sources":["../../../src/commands/migrate/link-project.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Runs `fn` under the spinner, and on failure — a dropped connection, a 504 from
3
+ * a cold worker, anything transient — offers to try again rather than silently
4
+ * giving up on a step whose only cost of re-running is a few seconds. Skips the
5
+ * prompt outside a TTY (CI, piped input) since there's no one there to answer it.
6
+ */
7
+ export declare function withRetry<T>(label: string, fn: () => Promise<T>): Promise<T | undefined>;
8
+ //# sourceMappingURL=retry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../../src/commands/migrate/retry.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAW9F"}
@@ -0,0 +1,25 @@
1
+ import pc from 'picocolors';
2
+ import { withPrompter } from '../../prompt.js';
3
+ import { withSpinner } from '../../spinner.js';
4
+ /**
5
+ * Runs `fn` under the spinner, and on failure — a dropped connection, a 504 from
6
+ * a cold worker, anything transient — offers to try again rather than silently
7
+ * giving up on a step whose only cost of re-running is a few seconds. Skips the
8
+ * prompt outside a TTY (CI, piped input) since there's no one there to answer it.
9
+ */
10
+ export async function withRetry(label, fn) {
11
+ for (;;) {
12
+ try {
13
+ return await withSpinner(label, fn);
14
+ }
15
+ catch (err) {
16
+ console.log(pc.dim(` ${label} failed: ${err.message}`));
17
+ if (!process.stdin.isTTY)
18
+ return undefined;
19
+ const retry = await withPrompter((p) => p.confirm(' Try again?', true));
20
+ if (!retry)
21
+ return undefined;
22
+ }
23
+ }
24
+ }
25
+ //# sourceMappingURL=retry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.js","sourceRoot":"","sources":["../../../src/commands/migrate/retry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,YAAY,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE/C;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAI,KAAa,EAAE,EAAoB;IACpE,SAAS,CAAC;QACR,IAAI,CAAC;YACH,OAAO,MAAM,WAAW,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QACtC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,KAAK,KAAK,YAAa,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;YACpE,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK;gBAAE,OAAO,SAAS,CAAC;YAC3C,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,cAAc,EAAE,IAAI,CAAC,CAAC,CAAC;YACzE,IAAI,CAAC,KAAK;gBAAE,OAAO,SAAS,CAAC;QAC/B,CAAC;IACH,CAAC;AACH,CAAC"}
package/dist/config.d.ts CHANGED
@@ -37,7 +37,7 @@ export declare function defaultProject(): string | undefined;
37
37
  export interface ResolvedProject {
38
38
  slug: string;
39
39
  /** Where the slug came from. Reported when it was inferred, not stated. */
40
- source: 'flag' | 'env' | 'config' | 'app.json' | 'app.config.js';
40
+ source: 'flag' | 'env' | 'buildatlas.json' | 'config' | 'app.json' | 'app.config.js';
41
41
  }
42
42
  export interface ExpoConfigValues {
43
43
  slug?: string;
@@ -53,6 +53,7 @@ export interface ExpoConfigValues {
53
53
  /** Everything a caller might want from the app config, static or dynamic. */
54
54
  export declare function readResolvedAppConfig(dir: string): Promise<ExpoConfigValues | undefined>;
55
55
  export declare function resolveProject(explicit: string | undefined, dir?: string): Promise<ResolvedProject>;
56
+ export declare function dynamicConfigIn(dir: string): Promise<string | undefined>;
56
57
  /**
57
58
  * What to tell someone whose directory has no usable static config.
58
59
  *
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;;GAKG;AACH,eAAO,MAAM,eAAe,QAAyE,CAAC;AAEtG;;;GAGG;AACH,QAAA,MAAM,YAAY;;;;iBAIhB,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAEvD,eAAO,MAAM,UAAU,cAAoD,CAAC;AAS5E;;;;;;GAMG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAUhE;AAED,wBAAsB,UAAU,IAAI,OAAO,CAAC,WAAW,CAAC,CAqBvD;AAED,wBAAsB,UAAU,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAOnE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,IAAI,MAAM,GAAG,SAAS,CAEnD;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,MAAM,EAAE,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,UAAU,GAAG,eAAe,CAAC;CAClE;AAqFD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,8DAA8D;IAC9D,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,6EAA6E;AAC7E,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAiB9F;AAED,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,GAAG,GAAE,MAAsB,GAC1B,OAAO,CAAC,eAAe,CAAC,CAgC1B;AAuBD;;;;;;;GAOG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAoBlE"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB;;;;;GAKG;AACH,eAAO,MAAM,eAAe,QAAyE,CAAC;AAEtG;;;GAGG;AACH,QAAA,MAAM,YAAY;;;;iBAIhB,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAEvD,eAAO,MAAM,UAAU,cAAoD,CAAC;AAS5E;;;;;;GAMG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAUhE;AAED,wBAAsB,UAAU,IAAI,OAAO,CAAC,WAAW,CAAC,CAqBvD;AAED,wBAAsB,UAAU,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAOnE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,IAAI,MAAM,GAAG,SAAS,CAEnD;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,MAAM,EAAE,MAAM,GAAG,KAAK,GAAG,iBAAiB,GAAG,QAAQ,GAAG,UAAU,GAAG,eAAe,CAAC;CACtF;AAqFD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,8DAA8D;IAC9D,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,6EAA6E;AAC7E,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAiB9F;AAED,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,GAAG,GAAE,MAAsB,GAC1B,OAAO,CAAC,eAAe,CAAC,CAwC1B;AAWD,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAU9E;AAED;;;;;;;GAOG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAoBlE"}
package/dist/config.js CHANGED
@@ -5,6 +5,7 @@ import { dirname, join } from 'node:path';
5
5
  import { promisify } from 'node:util';
6
6
  import pc from 'picocolors';
7
7
  import { z } from 'zod';
8
+ import { readDescriptor } from './build-source.js';
8
9
  const execFile = promisify(execFileCb);
9
10
  /**
10
11
  * The instance this CLI ships pointed at, so a first-time user does not have to type a
@@ -183,6 +184,13 @@ export async function resolveProject(explicit, dir = process.cwd()) {
183
184
  const fromEnv = defaultProject();
184
185
  if (fromEnv)
185
186
  return { slug: fromEnv, source: 'env' };
187
+ // The descriptor, before the Expo config. `project` here says which BuildAtlas
188
+ // project this directory builds into; `expo.slug` is a different name for a different
189
+ // thing that only coincided with it by habit — and a bare React Native or Flutter
190
+ // project has no Expo config to read at all.
191
+ const descriptor = await readDescriptor(dir);
192
+ if (descriptor?.project)
193
+ return { slug: descriptor.project, source: 'buildatlas.json' };
186
194
  // Only app.json. Evaluating a dynamic app.config.js means running the project's
187
195
  // own JavaScript just to learn a name, which is not a trade worth making.
188
196
  try {
@@ -204,8 +212,9 @@ export async function resolveProject(explicit, dir = process.cwd()) {
204
212
  if (dynamic?.slug) {
205
213
  return { slug: dynamic.slug, source: 'app.config.js' };
206
214
  }
207
- throw new Error('No project specified. Pass --project <slug>, set BUILDATLAS_PROJECT, or run this ' +
208
- `from a directory whose app.json has an "expo.slug".\n${await noProjectAdvice(dir)}`);
215
+ throw new Error('No project specified. Add "project" to buildatlas.json, pass --project <slug>, set ' +
216
+ 'BUILDATLAS_PROJECT, or run this from a directory whose app.json has an ' +
217
+ `"expo.slug".\n${await noProjectAdvice(dir)}`);
209
218
  }
210
219
  /**
211
220
  * The dynamic config filenames Expo accepts, in its own resolution order.
@@ -215,7 +224,7 @@ export async function resolveProject(explicit, dir = process.cwd()) {
215
224
  * is exactly what the old message did.
216
225
  */
217
226
  const DYNAMIC_CONFIGS = ['app.config.ts', 'app.config.js', 'app.config.mjs', 'app.config.cjs'];
218
- async function dynamicConfigIn(dir) {
227
+ export async function dynamicConfigIn(dir) {
219
228
  for (const name of DYNAMIC_CONFIGS) {
220
229
  try {
221
230
  await readFile(join(dir, name), 'utf8');