local-expo-build 0.2.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 (58) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/LICENSE +21 -0
  3. package/README.md +372 -0
  4. package/bin/local-expo-build.js +2 -0
  5. package/dist/cli.js +33 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/commands/build.js +121 -0
  8. package/dist/commands/build.js.map +1 -0
  9. package/dist/commands/doctor.js +606 -0
  10. package/dist/commands/doctor.js.map +1 -0
  11. package/dist/commands/init.js +125 -0
  12. package/dist/commands/init.js.map +1 -0
  13. package/dist/commands/keystore.js +47 -0
  14. package/dist/commands/keystore.js.map +1 -0
  15. package/dist/core/bumpVersion.js +70 -0
  16. package/dist/core/bumpVersion.js.map +1 -0
  17. package/dist/core/easLink.js +64 -0
  18. package/dist/core/easLink.js.map +1 -0
  19. package/dist/core/expoConfig.js +71 -0
  20. package/dist/core/expoConfig.js.map +1 -0
  21. package/dist/core/gradleRun.js +31 -0
  22. package/dist/core/gradleRun.js.map +1 -0
  23. package/dist/core/keystore/easFetch.js +109 -0
  24. package/dist/core/keystore/easFetch.js.map +1 -0
  25. package/dist/core/keystore/existing.js +135 -0
  26. package/dist/core/keystore/existing.js.map +1 -0
  27. package/dist/core/keystore/generate.js +72 -0
  28. package/dist/core/keystore/generate.js.map +1 -0
  29. package/dist/core/keystore/index.js +62 -0
  30. package/dist/core/keystore/index.js.map +1 -0
  31. package/dist/core/keystore/rehydrate.js +88 -0
  32. package/dist/core/keystore/rehydrate.js.map +1 -0
  33. package/dist/core/pinGradle.js +50 -0
  34. package/dist/core/pinGradle.js.map +1 -0
  35. package/dist/core/prebuild.js +16 -0
  36. package/dist/core/prebuild.js.map +1 -0
  37. package/dist/core/sdkDetect.js +26 -0
  38. package/dist/core/sdkDetect.js.map +1 -0
  39. package/dist/core/setupSigning.js +168 -0
  40. package/dist/core/setupSigning.js.map +1 -0
  41. package/dist/core/syncEasVersion.js +97 -0
  42. package/dist/core/syncEasVersion.js.map +1 -0
  43. package/dist/core/writeCredentialsJson.js +51 -0
  44. package/dist/core/writeCredentialsJson.js.map +1 -0
  45. package/dist/util/ctx.js +17 -0
  46. package/dist/util/ctx.js.map +1 -0
  47. package/dist/util/gitignore.js +23 -0
  48. package/dist/util/gitignore.js.map +1 -0
  49. package/dist/util/log.js +16 -0
  50. package/dist/util/log.js.map +1 -0
  51. package/package.json +64 -0
  52. package/templates/keystore.properties.example +4 -0
  53. package/templates/scripts/build.js +79 -0
  54. package/templates/scripts/bump-version.js +65 -0
  55. package/templates/scripts/pin-gradle.js +45 -0
  56. package/templates/scripts/print-artifact.js +36 -0
  57. package/templates/scripts/setup-signing.js +137 -0
  58. package/templates/scripts/sync-eas-version.js +59 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,99 @@
1
+ # Changelog
2
+
3
+ All notable changes to `local-expo-build` are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.2.0] — 2026-06-28
9
+
10
+ ### Added
11
+
12
+ - **Doctor as a setup wizard.** `doctor` now chains interactive auto-fixes for the
13
+ most common blockers: missing `expo.android.package`, missing EAS link
14
+ (`eas init`), missing `eas.json` (`eas build:configure`), and missing keystore
15
+ setup. Each step is gated on the previous outcome and the check rows are
16
+ mutated in place so the exit code reflects the post-fix state.
17
+ - **Dynamic config (`app.config.{js,ts,cjs,mjs}`) support.** `doctor`'s
18
+ `Android package` and `EAS project linked` checks now read the resolved
19
+ Expo config via `npx expo config --json --type public`, falling back to
20
+ `app.json` for static projects. Per-process cache keeps the cost down.
21
+ - **`keystore rehydrate --move` flag.** Deletes the source `.jks` after a
22
+ successful copy into `android/app/` (default is to leave the source as a
23
+ prebuild-survivable backup).
24
+ - **Root `.jks` backup for `keystore create` / `keystore import`.** Both
25
+ providers now also place the `.jks` at project root (gitignored via `*.jks`).
26
+ Closes the data-loss hole where `expo prebuild --clean` would leave a
27
+ freshly-generated keystore with no recoverable source.
28
+ - **Single-script orchestrator (`scripts/build.js`).** Replaces the long
29
+ `&&`-chained npm scripts with a single Node entry point. `package.json` now
30
+ carries `"build:android:apk": "node scripts/build.js apk"` instead of a
31
+ multi-step shell pipeline. Prints numbered progress and a total time.
32
+ - **Build artifact path + size printed at the end of every build.** Runner
33
+ mode and scaffold mode both surface the absolute path so you always know
34
+ where the APK / AAB landed.
35
+ - **Subtle contextual disclaimers** ("Local build · saves an EAS cloud build
36
+ credit · first run is slowest ~5 min") at the top of `build`, `init`, and
37
+ the build orchestrator. One dim line, not repeated mid-pipeline.
38
+ - **`keystore import` auto-detects matching `credentials.json`.** When the
39
+ user points at a `.jks` and a `credentials.json` exists describing the
40
+ same file (matched by absolute path OR sha1 content hash), the password +
41
+ alias prompts are skipped and the values are reused. Falls back to the
42
+ full prompt flow when no match — so this is purely an opt-in shortcut.
43
+ - **`--dry-run` honored by `build android`** (runner mode) and the scaffolded
44
+ `scripts/build.js` orchestrator. Prints every step's command + cwd without
45
+ executing them. Useful for screenshots, sanity-checking the pipeline order,
46
+ and CI plan-mode.
47
+ - **Pre-flight `doctor` in `init`.** `npx local-expo-build init` now runs
48
+ `doctor` first and only proceeds with scaffolding once the environment is
49
+ healthy. Use `--no-doctor` to skip.
50
+ - **Keystore `rehydrate` provider.** New `keystore rehydrate` subcommand (also
51
+ surfaced in the `keystore setup` picker when applicable) binds an existing
52
+ `credentials.json` + `.jks` pair to `keystore.properties` and copies the
53
+ `.jks` into `android/app/`. No password re-entry.
54
+ - **`credentials.json` scaffolder.** `ensureKeystore` now writes/maintains
55
+ `credentials.json` at project root from the same source as
56
+ `keystore.properties`, and gitignores it.
57
+ - **Build artifact path printed at the end of every build.** Both runner mode
58
+ and scaffolded `npm run build:android:*` now print the absolute path and size
59
+ of the produced APK / AAB.
60
+ - **`.jks` recovery in `setup-signing`.** If `expo prebuild --clean` wipes
61
+ `android/app/<storeFile>`, the build chain restores it from a stable source
62
+ (`credentials.json` keystorePath, `credentials/android/`, project root)
63
+ before invoking Gradle. Both `src/core/setupSigning.ts` and
64
+ `templates/scripts/setup-signing.js` carry the recovery.
65
+ - New doctor checks: `Android package (app.json)` (critical), `EAS project
66
+ linked` (yellow when half-linked), `keystore.properties`, `Signing keystore
67
+ (.jks)`, `credentials.json (EAS)`. Suggestions block prints a numbered
68
+ remediation list when anything is missing.
69
+
70
+ ### Changed
71
+
72
+ - `keystore setup` picker now lists `Rehydrate from credentials.json` as the
73
+ top, recommended option whenever a complete `credentials.json` + `.jks` are
74
+ on disk.
75
+ - `fetchKeystoreFromEas` pre-flights both `expo.extra.eas.projectId` and
76
+ `eas.json` and offers to run `eas init` / `eas build:configure` interactively
77
+ before launching the EAS credentials menu.
78
+ - `.gitignore` entries created by `init` / `keystore setup` now include
79
+ `credentials.json` alongside `keystore.properties` and `*.jks`.
80
+
81
+ ### Fixed
82
+
83
+ - Builds failing with `validateSigningRelease > Keystore file not found` after
84
+ `expo prebuild --clean` (or after accepting the "android project is malformed
85
+ — reinitialize?" prompt). The keystore is now restored from a stable source
86
+ before Gradle runs.
87
+ - Cryptic "credentials command failed" from `keystore fetch` on projects
88
+ without `eas.json` or a linked EAS project. Replaced with a guided
89
+ pre-flight.
90
+ - `templates/scripts/build.js` orchestrator was passing `shell: false` to
91
+ `execSync` on Unix, which is undefined behavior. Now omits the option so
92
+ Node uses the platform default (`/bin/sh` on Unix, `cmd.exe` on Windows).
93
+ Fully cross-platform.
94
+
95
+ ## [0.1.0]
96
+
97
+ Initial release. Local Expo Android APK/AAB pipeline with `prebuild`, Gradle
98
+ wrapper pinning, version bump, signing config injection, Gradle build, and EAS
99
+ versionCode sync.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nikhil Dhawan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,372 @@
1
+ # local-expo-build
2
+
3
+ > One-stop CLI for **local** Expo Android APK / AAB builds. Bypass EAS cloud builds, keep full control of signing, and stop waiting in queues.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/local-expo-build.svg)](https://www.npmjs.com/package/local-expo-build)
6
+ [![npm downloads](https://img.shields.io/npm/dm/local-expo-build.svg)](https://www.npmjs.com/package/local-expo-build)
7
+ [![license](https://img.shields.io/npm/l/local-expo-build.svg)](https://github.com/nikhild64/local-expo-build/blob/main/LICENSE)
8
+ [![node](https://img.shields.io/node/v/local-expo-build.svg)](https://nodejs.org/)
9
+
10
+ `local-expo-build` automates the painful parts of running `expo prebuild` + `gradlew bundleRelease` yourself:
11
+
12
+ - Detects your Expo SDK and pins the Gradle wrapper to a version that actually works (e.g. SDK 55 → Gradle 8.13, working around the `expo-manifests` `components.release` bug).
13
+ - Bumps your app version and pulls the next `versionCode` from EAS so Play Store ingest doesn't reject the upload.
14
+ - Injects a release `signingConfig` into the generated `android/app/build.gradle` from a `keystore.properties` you control.
15
+ - Scaffolds `credentials.json` from the same source so EAS submit / cloud builds can reuse your local JKS.
16
+ - Restores your `.jks` into `android/app/` if `expo prebuild --clean` wipes it (no more `validateSigningRelease > Keystore file not found`).
17
+ - Runs `gradlew assembleRelease` / `bundleRelease` and prints the absolute path + size of the produced artifact.
18
+ - Pushes the new `versionCode` back to EAS via GraphQL so `eas build` / `eas submit` stay in sync.
19
+
20
+ **`doctor` is a setup wizard, not just a health check.** It detects missing pieces (`expo.android.package`, EAS link, `eas.json`, keystore) and offers to fix each one interactively — `eas init`, `eas build:configure`, keystore picker (with one-prompt `rehydrate` from `credentials.json` when possible), all chained.
21
+
22
+ ![local-expo-build init: doctor pre-flight + scaffolding in one command](https://raw.githubusercontent.com/nikhild64/local-expo-build/main/assets/screenshots/setup_init.png)
23
+
24
+ Two modes:
25
+
26
+ - **Scaffold** _(recommended)_ — `npx local-expo-build init` drops reusable, committable scripts into your project; you run `npm run build:android:aab` from then on.
27
+ - **Runner** — `npx local-expo-build build android --aab`; one command, no files touched in your repo.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ npm i -g local-expo-build
33
+ # or use it ad hoc
34
+ npx local-expo-build --help
35
+ ```
36
+
37
+ ## Quick start (recommended — scaffold mode)
38
+
39
+ ```bash
40
+ cd <your-expo-project>
41
+ npx local-expo-build init
42
+ ```
43
+
44
+ `init` runs `doctor` first as a pre-flight, walks you through any missing setup (EAS link, `eas.json`, keystore), then drops the build scripts and adds the `build:android:apk` / `build:android:aab` entries to your `package.json`. Then:
45
+
46
+ ```bash
47
+ npm run build:android:aab # release AAB → android/app/build/outputs/bundle/release/app-release.aab
48
+ npm run build:android:apk # release APK → android/app/build/outputs/apk/release/app-release.apk
49
+ ```
50
+
51
+ After the build finishes, the absolute path + size of the artifact is printed at the very end so you always know where it landed.
52
+
53
+ Skip the pre-flight in CI: `npx local-expo-build init --no-doctor --no-keystore`.
54
+
55
+ ### Alternative — runner mode (no scaffold)
56
+
57
+ ```bash
58
+ npx local-expo-build doctor # env + setup wizard
59
+ npx local-expo-build build android --aab # full pipeline → .aab
60
+ ```
61
+
62
+ Useful if you don't want any files committed to your repo and prefer to drive the whole pipeline from a single CLI call each time.
63
+
64
+ ## Commands
65
+
66
+ ```text
67
+ local-expo-build init [--force] [--no-keystore] [--no-doctor]
68
+ Scaffold scripts + package.json entries
69
+ (runs `doctor` first by default)
70
+
71
+ local-expo-build build android [--apk|--aab] [--profile <name>]
72
+ [--clean] [--no-bump] [--no-sync] [--no-prebuild]
73
+ Run the full pipeline → .aab|.apk
74
+
75
+ local-expo-build doctor Env check + interactive auto-fix wizard
76
+ (eas init → eas build:configure →
77
+ keystore setup)
78
+
79
+ local-expo-build keystore setup Interactive picker:
80
+ rehydrate | existing | generate | EAS
81
+ local-expo-build keystore import Register an existing .jks
82
+ local-expo-build keystore create Generate a new keystore via keytool
83
+ local-expo-build keystore fetch Open `eas credentials` to download a .jks
84
+ local-expo-build keystore rehydrate [--move]
85
+ Bind credentials.json + .jks into
86
+ keystore.properties (no password re-entry).
87
+ --move deletes the source .jks after copy.
88
+ ```
89
+
90
+ Global flags: `--cwd <path>`, `--verbose`, `--dry-run`.
91
+
92
+ > **Dry-run** is wired into `build android` and the scaffolded orchestrator. Use it to preview the full pipeline (great for screenshots, sanity checks, CI plan-mode):
93
+ >
94
+ > ```bash
95
+ > npx local-expo-build --dry-run build android --aab # runner mode
96
+ > npm run build:android:aab -- --dry-run # scaffold mode (or: node scripts/build.js aab --dry-run)
97
+ > ```
98
+
99
+ ![Dry-run output: the full 7-step build pipeline with no side effects](https://raw.githubusercontent.com/nikhild64/local-expo-build/main/assets/screenshots/dryrun-build.png)
100
+
101
+ ## How it compares
102
+
103
+ | | `eas build` (cloud) | `npx expo run:android` | `local-expo-build` |
104
+ | --- | --- | --- | --- |
105
+ | Runs locally | No | Yes | **Yes** |
106
+ | Produces a signed release `.aab` / `.apk` | Yes | No (debug) | **Yes** |
107
+ | Manages release `signingConfig` for you | Yes | No | **Yes** |
108
+ | Bumps `versionCode` from EAS automatically | Yes | No | **Yes** |
109
+ | Wait in cloud queue | Sometimes | Never | Never |
110
+ | Works offline | No | Yes | **Yes** (after first prebuild) |
111
+ | Needs `eas-cli` | Yes | No | Optional (only for version sync / EAS keystore) |
112
+
113
+ If you're happy with cloud builds, use `eas build`. This CLI is for teams who want the EAS workflow (managed signing, synced `versionCode`) but the speed and control of building on their own machine.
114
+
115
+ ## Keystore sources
116
+
117
+ When `keystore setup` runs (or when `doctor` / `init` prompt for it), you'll see one of these. **Rehydrate** appears at the top conditionally — only when a complete `credentials.json` + `.jks` are already on disk:
118
+
119
+ | Source | What happens |
120
+ | --- | --- |
121
+ | **Rehydrate** | Reads `credentials.json` + the `.jks` it points at, copies the `.jks` into `android/app/<basename>`, writes `keystore.properties`. **No password re-entry.** Ideal after `keystore fetch`. |
122
+ | **Existing .jks** | You point to a file + provide alias/passwords. Copied into `android/app/`. If a matching `credentials.json` is on disk (same path or same content hash), the password prompts are skipped — values reused automatically. |
123
+ | **Generate new** | Wizard runs `keytool -genkeypair` with sane defaults (RSA 2048, 10000d). |
124
+ | **EAS** | Opens `eas credentials` so you can download the project's current keystore from EAS. |
125
+
126
+ In every case, after the keystore is registered the CLI also writes a matching `credentials.json` at the project root and adds `keystore.properties`, `*.jks`, and `credentials.json` to `.gitignore`.
127
+
128
+ ## Bringing your own keystore (EAS-managed flow)
129
+
130
+ The lowest-friction path from a brand-new clone to a buildable project when your team already has a keystore on EAS. This is what `doctor` will walk you through.
131
+
132
+ ### 1. Fetch the keystore via EAS CLI
133
+
134
+ ```bash
135
+ npx local-expo-build keystore fetch
136
+ # Pre-flight: if eas.json or projectId is missing, we'll offer
137
+ # to run `eas init` / `eas build:configure` before launching EAS.
138
+ ```
139
+
140
+ EAS opens its interactive menu. Pick:
141
+
142
+ ```text
143
+ ✔ Which build profile do you want to configure? › production
144
+ ✔ What do you want to do? › Keystore: Manage everything needed to build your project
145
+ ✔ What do you want to do? › Download existing keystore
146
+ ✔ Display sensitive information? › Yes
147
+ ✔ Go back
148
+ ✔ What do you want to do? › credentials.json: Upload/Download credentials …
149
+ ✔ What do you want to do? › Download credentials from EAS to credentials.json
150
+ ✔ What do you want to do? › Exit
151
+ ```
152
+
153
+ You now have:
154
+
155
+ - `<scope>__<project>.jks` at project root (from "Download existing keystore"),
156
+ - `credentials.json` at project root with all four required fields,
157
+ - `credentials/android/keystore.jks` (the file `credentials.json` actually references).
158
+
159
+ ### 2. Bind everything in one step
160
+
161
+ ```bash
162
+ npx local-expo-build keystore rehydrate
163
+ ```
164
+
165
+ That copies the `.jks` referenced by `credentials.json` into `android/app/<basename>` and writes `keystore.properties` using the passwords already in `credentials.json` — **no re-typing**.
166
+
167
+ ```text
168
+ ✓ Copied credentials/android/keystore.jks → android/app/keystore.jks
169
+ ✓ keystore.properties written from credentials.json (alias=6805615551f1…)
170
+ ✓ Wrote credentials.json (keystorePath=android/app/keystore.jks)
171
+ ```
172
+
173
+ ### 3. Build
174
+
175
+ ```bash
176
+ npm run build:android:aab # scaffold mode
177
+ # or
178
+ npx local-expo-build build android --aab # runner mode
179
+ ```
180
+
181
+ > Tip: you can skip step 2 entirely — `doctor` detects the rehydrate state automatically and offers the same one-prompt fix inline.
182
+
183
+ ## Files this CLI creates / touches
184
+
185
+ | Path | Purpose | Gitignored? |
186
+ | --- | --- | --- |
187
+ | `keystore.properties` (root) | Gradle release `signingConfig` source of truth: `storeFile`, `storePassword`, `keyAlias`, `keyPassword` | Yes (auto) |
188
+ | `credentials.json` (root) | EAS submit/cloud's local-credential pointer. Kept in sync with `keystore.properties`. | Yes (auto) |
189
+ | `android/app/<storeFile>` | The actual `.jks` that Gradle reads | Yes (`*.jks`, auto) |
190
+ | `eas.json` (root) | EAS build profile config (created by `eas build:configure`) | No — commit it |
191
+ | `app.json` → `expo.extra.eas.projectId` | EAS link (written by `eas init`) | No — commit it |
192
+ | `app.json` → `expo.android.package` | Android applicationId | No — commit it |
193
+ | `scripts/build.js` | (Scaffold mode only) single orchestrator entry point — what `npm run build:android:*` actually calls | No — commit it |
194
+ | `scripts/{pin-gradle,bump-version,setup-signing,sync-eas-version,print-artifact}.js` | (Scaffold mode only) per-step modules orchestrated by `build.js`; edit any one to customize that step for your project | No — commit them |
195
+
196
+ > **Security:** `keystore.properties` and `credentials.json` both contain plaintext keystore passwords. The CLI gitignores them automatically. **Don't commit them. Don't paste them into chat.** If you need them in CI, base64-encode and inject via secrets.
197
+
198
+ ## Multi-SDK support
199
+
200
+ `local-expo-build` carries a small table of Gradle wrapper versions per SDK in [`src/core/pinGradle.ts`](src/core/pinGradle.ts):
201
+
202
+ ```ts
203
+ export const GRADLE_PIN: Record<number, string | null> = {
204
+ 50: null,
205
+ 51: null,
206
+ 52: null,
207
+ 53: null,
208
+ 54: null,
209
+ 55: '8.13', // expo-manifests components.release workaround
210
+ 56: null,
211
+ };
212
+ ```
213
+
214
+ If your SDK isn't pinned, `pinGradle` is a no-op. Add a row + open a PR if a future SDK needs one.
215
+
216
+ ## Requirements
217
+
218
+ - Node ≥ 18
219
+ - JDK 17 (recommended for Expo SDK 55)
220
+ - Android SDK + `ANDROID_HOME` env var
221
+ - `keytool` on `PATH` (ships with the JDK)
222
+ - `eas-cli` is **optional** — only needed for EAS version sync, EAS keystore fetch, or doctor's `eas init` / `eas build:configure` auto-fixes
223
+
224
+ Run `local-expo-build doctor` to verify all of the above.
225
+
226
+ ## How it works (pipeline)
227
+
228
+ ```text
229
+ expo prebuild --platform android
230
+ → pin Gradle wrapper (src/core/pinGradle.ts)
231
+ → bump version + EAS code (src/core/bumpVersion.ts)
232
+ → ensure keystore (src/core/keystore/*)
233
+ → restore .jks into android/app (src/core/setupSigning.ts → ensureKeystoreInAndroidApp)
234
+ → inject release signing (src/core/setupSigning.ts)
235
+ → write/sync credentials.json (src/core/writeCredentialsJson.ts)
236
+ → gradlew {assemble|bundle}Release
237
+ → sync versionCode to EAS (src/core/syncEasVersion.ts)
238
+ → print artifact path + size (templates/scripts/print-artifact.js)
239
+ ```
240
+
241
+ The scaffolded `scripts/*.js` files mirror the same logic so they're vendorable and editable per-project.
242
+
243
+ ### Doctor's auto-fix chain
244
+
245
+ ```text
246
+ expo.android.package (app.json or app.config.*) → prompt + write to app.json
247
+ EAS link (expo.extra.eas.projectId) → offer `eas init`
248
+ eas.json → offer `eas build:configure --platform android`
249
+ keystore.properties → offer keystore picker
250
+ ├─ credentials.json + .jks present → rehydrate (no password re-entry)
251
+ └─ else → existing | generate | EAS
252
+ ```
253
+
254
+ Each accepted step re-runs the affected checks in place. If everything ends up green, doctor exits 0; otherwise the remaining items are printed under **Suggested next steps to complete setup**.
255
+
256
+ Dynamic configs (`app.config.{js,ts,cjs,mjs}`) are supported: doctor shells out to `npx expo config --json --type public` and reads the resolved config. If the Expo CLI fails to resolve (e.g. you haven't `npm install`ed yet), the affected rows show a yellow warning instead of pretending. Auto-writes still target `app.json` only — we don't modify dynamic config files.
257
+
258
+ `init` runs the entire doctor chain as its pre-flight before scaffolding. Pass `--no-doctor` to skip.
259
+
260
+ Your `package.json` ends up with just two added lines, both pointing at the orchestrator:
261
+
262
+ ```json
263
+ {
264
+ "scripts": {
265
+ "build:android:apk": "node scripts/build.js apk",
266
+ "build:android:aab": "node scripts/build.js aab"
267
+ }
268
+ }
269
+ ```
270
+
271
+ The orchestrator (`scripts/build.js`) prints numbered progress (`▸ [3/7] bump version`, etc.) and a final total time. EAS version sync is treated as non-fatal — if your EAS login expires, the build still succeeds and you get a single warning line instead of a hard fail.
272
+
273
+ ## Troubleshooting
274
+
275
+ ### `validateSigningRelease > Keystore file '…/android/app/keystore.jks' not found`
276
+
277
+ This happens when `expo prebuild --clean` (or the "android project is malformed — reinitialize?" prompt) wipes `android/` between keystore setup and the Gradle build. The pipeline restores it automatically as of v0.2.0 — make sure your scaffolded `scripts/setup-signing.js` is up to date:
278
+
279
+ ```bash
280
+ npx local-expo-build init --force
281
+ ```
282
+
283
+ If the recovery itself fails, you'll get a clear list of paths it tried; the fix is usually:
284
+
285
+ ```bash
286
+ npx local-expo-build keystore rehydrate # if you have credentials.json
287
+ # or
288
+ npx local-expo-build keystore import <path-to-jks>
289
+ ```
290
+
291
+ ### `eas credentials failed: Command failed with exit code 1`
292
+
293
+ `eas credentials` refuses to start without `eas.json` and a linked `expo.extra.eas.projectId`. v0.2.0 pre-flights both and offers to run `eas init` / `eas build:configure` interactively. If you skipped those prompts, run them manually:
294
+
295
+ ```bash
296
+ eas init
297
+ eas build:configure --platform android
298
+ ```
299
+
300
+ Then re-run `npx local-expo-build keystore fetch`.
301
+
302
+ ### `Missing expo.android.package in app.json`
303
+
304
+ Doctor catches this as a critical check and offers to write it for you with a sensible default derived from `expo.slug` or `expo.ios.bundleIdentifier`. If you're using `app.config.{js,ts}`, doctor can't statically write to it — add the field by hand:
305
+
306
+ ```json
307
+ {
308
+ "expo": {
309
+ "android": {
310
+ "package": "com.yourcompany.yourapp"
311
+ }
312
+ }
313
+ }
314
+ ```
315
+
316
+ ### "The android project is malformed, would you like to clear and reinitialize?"
317
+
318
+ This is `expo prebuild` detecting that our injected `signingConfigs.release` block doesn't match what it would generate fresh. Accepting it is safe — the build chain re-injects signing and the recovery step puts the `.jks` back before Gradle runs.
319
+
320
+ ### `expo CLI (in project) not found` in doctor
321
+
322
+ You haven't installed deps in the target Expo project yet. Run `npm install` (or `pnpm install` / `yarn`) in the project root.
323
+
324
+ ### Build artifact is signed but Play Console rejects it
325
+
326
+ Two common causes:
327
+
328
+ 1. **`versionCode` already used.** The `--no-sync` flag suppresses pushing the new `versionCode` back to EAS — if you used it and Play sees the same code as a previous upload, it'll reject. Don't pass `--no-sync` unless you're managing versions manually.
329
+ 2. **Different keystore than the one originally registered.** Play Store requires the *same* keystore for all updates to a published app. Use `keystore fetch` + `keystore rehydrate` to get back the original.
330
+
331
+ ## Testing the CLI locally in another Expo app
332
+
333
+ Three iteration loops, fastest to most-realistic:
334
+
335
+ ```bash
336
+ # 1. npm link — fastest dev loop
337
+ cd local-expo-build && npm run build && npm link
338
+ cd ../my-test-app && npm link local-expo-build
339
+ # now changes in local-expo-build (with `npm run dev` watching) are picked up
340
+ # on the next `npx local-expo-build ...` call from my-test-app.
341
+
342
+ # 2. npm pack — exactly what end users will install
343
+ cd local-expo-build && npm run build && npm pack
344
+ cd ../my-test-app && npm i ../local-expo-build/local-expo-build-0.2.0.tgz
345
+
346
+ # 3. Direct invocation — no install at all
347
+ cd local-expo-build && npm run build
348
+ node /abs/path/local-expo-build/bin/local-expo-build.js doctor --cwd /abs/path/my-test-app
349
+ ```
350
+
351
+ ## Roadmap
352
+
353
+ - [ ] iOS local builds
354
+ - [ ] Auto-update `GRADLE_PIN` table from a hosted manifest
355
+ - [ ] Symbol upload (`mapping.txt` → Play Console / Sentry)
356
+ - [ ] CI presets (`init --ci` that scaffolds a GitHub Actions / GitLab CI workflow with base64-encoded secrets)
357
+
358
+ ## Contributing
359
+
360
+ PRs welcome — see [CONTRIBUTING.md](CONTRIBUTING.md) for dev setup, code layout, and conventions.
361
+
362
+ ## Changelog
363
+
364
+ See [CHANGELOG.md](CHANGELOG.md).
365
+
366
+ ## License
367
+
368
+ [MIT](LICENSE) © Nikhil Dhawan
369
+
370
+ ---
371
+
372
+ **Not affiliated with Expo or Google.** "Expo" and "EAS" are trademarks of 650 Industries, Inc. This project consumes EAS's public APIs (`api.expo.dev/graphql`, `eas-cli`) but is independently maintained.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ require('../dist/cli.js');
package/dist/cli.js ADDED
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const commander_1 = require("commander");
7
+ const kleur_1 = __importDefault(require("kleur"));
8
+ const build_1 = require("./commands/build");
9
+ const init_1 = require("./commands/init");
10
+ const keystore_1 = require("./commands/keystore");
11
+ const doctor_1 = require("./commands/doctor");
12
+ const pkg = require('../package.json');
13
+ const program = new commander_1.Command();
14
+ program
15
+ .name('local-expo-build')
16
+ .description('Local Expo Android build CLI — bypasses EAS cloud builds. ' +
17
+ 'Prebuild, pin Gradle, bump version, sign with your JKS, run gradlew, sync EAS.')
18
+ .version(pkg.version)
19
+ .option('--cwd <path>', 'project directory (default: process.cwd())')
20
+ .option('--verbose', 'verbose logging')
21
+ .option('--dry-run', 'print actions without executing destructive steps');
22
+ (0, build_1.registerBuildCommand)(program);
23
+ (0, init_1.registerInitCommand)(program);
24
+ (0, keystore_1.registerKeystoreCommand)(program);
25
+ (0, doctor_1.registerDoctorCommand)(program);
26
+ program
27
+ .parseAsync(process.argv)
28
+ .catch((err) => {
29
+ console.error(kleur_1.default.red('\nlocal-expo-build failed:'));
30
+ console.error(err?.stack || err?.message || err);
31
+ process.exit(1);
32
+ });
33
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";;;;;AAAA,yCAAoC;AACpC,kDAA0B;AAC1B,4CAAwD;AACxD,0CAAsD;AACtD,kDAA8D;AAC9D,8CAA0D;AAE1D,MAAM,GAAG,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAEvC,MAAM,OAAO,GAAG,IAAI,mBAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,kBAAkB,CAAC;KACxB,WAAW,CACV,4DAA4D;IAC1D,gFAAgF,CACnF;KACA,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC;KACpB,MAAM,CAAC,cAAc,EAAE,4CAA4C,CAAC;KACpE,MAAM,CAAC,WAAW,EAAE,iBAAiB,CAAC;KACtC,MAAM,CAAC,WAAW,EAAE,mDAAmD,CAAC,CAAC;AAE5E,IAAA,4BAAoB,EAAC,OAAO,CAAC,CAAC;AAC9B,IAAA,0BAAmB,EAAC,OAAO,CAAC,CAAC;AAC7B,IAAA,kCAAuB,EAAC,OAAO,CAAC,CAAC;AACjC,IAAA,8BAAqB,EAAC,OAAO,CAAC,CAAC;AAE/B,OAAO;KACJ,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC;KACxB,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACb,OAAO,CAAC,KAAK,CAAC,eAAK,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAC,CAAC;IACvD,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,IAAI,GAAG,EAAE,OAAO,IAAI,GAAG,CAAC,CAAC;IACjD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.registerBuildCommand = registerBuildCommand;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const ctx_1 = require("../util/ctx");
9
+ const log_1 = require("../util/log");
10
+ const sdkDetect_1 = require("../core/sdkDetect");
11
+ const prebuild_1 = require("../core/prebuild");
12
+ const pinGradle_1 = require("../core/pinGradle");
13
+ const bumpVersion_1 = require("../core/bumpVersion");
14
+ const setupSigning_1 = require("../core/setupSigning");
15
+ const gradleRun_1 = require("../core/gradleRun");
16
+ const syncEasVersion_1 = require("../core/syncEasVersion");
17
+ const keystore_1 = require("../core/keystore");
18
+ function registerBuildCommand(program) {
19
+ const build = program.command('build').description('Build commands');
20
+ build
21
+ .command('android')
22
+ .description('Build a local Android APK or AAB')
23
+ .option('--apk', 'build APK (assembleRelease)')
24
+ .option('--aab', 'build AAB (bundleRelease) — default')
25
+ .option('--profile <profile>', 'EAS profile for versionCode fetch', 'production')
26
+ .option('--clean', 'pass --clean to expo prebuild')
27
+ .option('--no-bump', 'skip version bump')
28
+ .option('--no-sync', 'skip EAS versionCode sync after build')
29
+ .option('--no-prebuild', 'skip expo prebuild step')
30
+ .action(async (opts, cmd) => {
31
+ const ctx = (0, ctx_1.getCtx)(cmd);
32
+ const task = opts.apk ? 'assembleRelease' : 'bundleRelease';
33
+ const kind = task === 'bundleRelease' ? 'AAB' : 'APK';
34
+ log_1.log.step(`local-expo-build android (${kind})`);
35
+ log_1.log.dim('Local build · runs on your machine · saves an EAS cloud build credit');
36
+ log_1.log.dim(`cwd: ${ctx.cwd}`);
37
+ if (ctx.dryRun) {
38
+ log_1.log.warn('DRY RUN — no files modified, no Gradle build executed.');
39
+ }
40
+ const sdk = (0, sdkDetect_1.detectExpoSdk)(ctx.cwd);
41
+ log_1.log.ok(`Detected Expo SDK ${sdk.major} (${sdk.raw})`);
42
+ if (opts.prebuild !== false) {
43
+ log_1.log.step('1/6 expo prebuild');
44
+ if (ctx.dryRun) {
45
+ log_1.log.dim(`[dry-run] would run: expo prebuild --platform android${opts.clean ? ' --clean' : ''}`);
46
+ }
47
+ else {
48
+ await (0, prebuild_1.prebuild)({ cwd: ctx.cwd, clean: Boolean(opts.clean) });
49
+ }
50
+ }
51
+ else {
52
+ log_1.log.dim('Skipping prebuild (--no-prebuild)');
53
+ }
54
+ log_1.log.step('2/6 pin Gradle wrapper');
55
+ if (ctx.dryRun) {
56
+ log_1.log.dim(`[dry-run] would pin Gradle wrapper for SDK ${sdk.major} (see src/core/pinGradle.ts)`);
57
+ }
58
+ else {
59
+ (0, pinGradle_1.pinGradle)({ cwd: ctx.cwd, sdk: sdk.major });
60
+ }
61
+ if (opts.bump !== false) {
62
+ log_1.log.step('3/6 bump version');
63
+ if (ctx.dryRun) {
64
+ log_1.log.dim(`[dry-run] would fetch next versionCode from EAS (profile=${opts.profile}) and write app.json + build.gradle`);
65
+ }
66
+ else {
67
+ (0, bumpVersion_1.bumpVersion)({ cwd: ctx.cwd, profile: opts.profile });
68
+ }
69
+ }
70
+ else {
71
+ log_1.log.dim('Skipping version bump (--no-bump)');
72
+ }
73
+ log_1.log.step('4/6 ensure keystore + inject signing config');
74
+ if (ctx.dryRun) {
75
+ log_1.log.dim('[dry-run] would ensure keystore.properties + .jks present, then inject release signingConfig into build.gradle');
76
+ }
77
+ else {
78
+ await (0, keystore_1.ensureKeystore)(ctx.cwd);
79
+ (0, setupSigning_1.setupSigning)({ cwd: ctx.cwd });
80
+ }
81
+ log_1.log.step(`5/6 gradle ${task}`);
82
+ let artifact = '';
83
+ if (ctx.dryRun) {
84
+ const isWin = process.platform === 'win32';
85
+ const wrapper = isWin ? 'gradlew.bat' : './gradlew';
86
+ log_1.log.dim(`[dry-run] would run (cwd=android/): ${wrapper} ${task}`);
87
+ }
88
+ else {
89
+ artifact = await (0, gradleRun_1.gradleRun)({ cwd: ctx.cwd, task });
90
+ }
91
+ if (opts.sync !== false) {
92
+ log_1.log.step('6/6 sync EAS versionCode');
93
+ if (ctx.dryRun) {
94
+ log_1.log.dim('[dry-run] would POST new versionCode to api.expo.dev/graphql (non-fatal on failure)');
95
+ }
96
+ else {
97
+ try {
98
+ await (0, syncEasVersion_1.syncEasVersion)({ cwd: ctx.cwd });
99
+ }
100
+ catch (err) {
101
+ log_1.log.warn(`EAS sync failed (non-fatal): ${err?.message || err}`);
102
+ }
103
+ }
104
+ }
105
+ else {
106
+ log_1.log.dim('Skipping EAS sync (--no-sync)');
107
+ }
108
+ log_1.log.step('Done');
109
+ if (ctx.dryRun) {
110
+ log_1.log.ok(`DRY RUN complete — 6 steps shown for ${kind}. Re-run without --dry-run to actually build.`);
111
+ }
112
+ else if (fs_1.default.existsSync(artifact)) {
113
+ const sizeMb = (fs_1.default.statSync(artifact).size / 1024 / 1024).toFixed(2);
114
+ log_1.log.ok(`Build complete (${kind}, ${sizeMb} MB):\n ${artifact}`);
115
+ }
116
+ else {
117
+ log_1.log.warn(`Build finished but artifact not found at ${artifact}`);
118
+ }
119
+ });
120
+ }
121
+ //# sourceMappingURL=build.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build.js","sourceRoot":"","sources":["../../src/commands/build.ts"],"names":[],"mappings":"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"}