From c26d4b4b29a1bd7c4560a07ac1d166410c50a8c0 Mon Sep 17 00:00:00 2001 From: Andrew Amin Date: Sun, 24 Aug 2025 19:27:32 +0300 Subject: [PATCH 1/2] chore: add init command to CLI --- .eslintrc.js | 11 + cli/README.md | 238 +++++++++++++++++++++ cli/commands/Init.ts | 59 ++++++ cli/index.ts | 4 +- cli/init/init.ts | 342 ++++++++++++++++++++++++++++++ cli/upload/index.ts | 2 + examples/default/ios/Podfile.lock | 4 +- expo-plugin/index.js | 10 + package.json | 1 + 9 files changed, 668 insertions(+), 3 deletions(-) create mode 100644 cli/README.md create mode 100644 cli/commands/Init.ts create mode 100644 cli/init/init.ts create mode 100644 expo-plugin/index.js diff --git a/.eslintrc.js b/.eslintrc.js index f1756a1e2..f62787c9b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -12,6 +12,17 @@ module.exports = { 'jest/globals': true, }, }, + { + // CLI Overrides + files: ['cli/**'], + env: { + node: true, + }, + parserOptions: { + project: './tsconfig.cli.json', + tsconfigRootDir: __dirname, + }, + }, { // Node Scripts Overrides files: ['scripts/**'], diff --git a/cli/README.md b/cli/README.md new file mode 100644 index 000000000..3ec8c7679 --- /dev/null +++ b/cli/README.md @@ -0,0 +1,238 @@ +### Instabug React Native CLI + +A command-line interface to help integrate and operate the Instabug React Native SDK. + +- **Docs**: [Integrating Instabug for React Native](https://docs.instabug.com/docs/react-native-integration) + +### Command overview + +- `init`: Initialize the SDK with basic instrumentation +- `upload-sourcemaps`: Upload JavaScript sourcemaps +- `upload-so-files`: Upload Android NDK `.so` symbols +- `--help`: Show help (for root or any subcommand) +- `--version`: Show CLI version + +### Expo support + +- **Managed**: Supported starting from this CLI version. The `init` command detects Expo (by `app.json`) and will: + + - Inject Instabug initialization into `App.(js|tsx)` if present, or fall back to `index.(js|tsx)` + - Append `instabug-reactnative/expo-plugin` to `expo.plugins` in `app.json` + - Ensure `expo-build-properties` plugin is present with `ios.deploymentTarget = 15.1` + - Skip CocoaPods (native code is generated during `expo prebuild` / EAS builds) + +- **Bare / Prebuild**: Works the same as React Native after `expo prebuild`. + +- Example: + +```bash +npx instabug init --token YOUR_APP_TOKEN --entry App.tsx +# Then, if you need native projects locally +npx expo prebuild +``` + +#### Expo managed quick start + +```bash +# 1) Initialize Instabug (adds plugin entries and injects init code) +npx instabug init --token YOUR_APP_TOKEN --entry App.tsx + +# 2) Ensure build properties plugin is installed in your project +npm i -D expo-build-properties + +# 3) Run on iOS (set CI to auto-accept alternate port if 8081 is busy) +CI=1 npx expo start --ios +``` + +- Notes: + - If another RN/EAS dev server is using port 8081, Expo may prompt to use a new port. Use `CI=1` to auto-accept in non-interactive sessions. + - For native builds (EAS or prebuild), the iOS deployment target will be enforced at 15.1 via `expo-build-properties`. + - You do not need to run `pod install` for Expo managed; native code is handled by Expo during build. + +### Installation + +- If your app already depends on `instabug-reactnative`, you can use the CLI via `npx`: + +```bash +npx instabug --help +``` + +- When developing this repo locally, you can invoke the built CLI directly: + +```bash +node bin/index.js --help +``` + +### Commands + +#### init + +Initialize the Instabug React Native SDK in your project with basic instrumentation. + +```bash +# Using npx (project dependency) +npx instabug init --token YOUR_APP_TOKEN \ + --invocation-events shake,screenshot \ + --debug-logs-level error \ + --code-push-version v10 \ + --network-interception-mode javascript + +# From this repo after build +node bin/index.js init --token YOUR_APP_TOKEN +``` + +- **Options** + + - `-t, --token `: Your app token. Env: `INSTABUG_APP_TOKEN`. Required + - `-e, --entry `: Entry file path (auto-detected if omitted) + - `--invocation-events `: Comma-separated `InvocationEvent` values: `none, shake, screenshot, twoFingersSwipe, floatingButton` + - `--debug-logs-level `: `verbose | debug | error | none` + - `--code-push-version `: CodePush version label + - `--ignore-android-secure-flag`: Allow screenshots even if secure flag is set (Android) + - `--network-interception-mode `: `javascript | native` + - `--npm` / `--yarn`: Force package manager (auto-detected by default) + - `--no-pods`: Skip running `pod install` on iOS + - `--silent`: Reduce logging and avoid exiting the process on errors + +- **What it does** + - Installs `instabug-reactnative` + - Optionally runs `cd ios && pod install` if an iOS project exists + - Injects the following into your entry file (e.g., `index.js`): + +```javascript +import Instabug, { InvocationEvent } from 'instabug-reactnative'; + +Instabug.init({ + token: 'YOUR_APP_TOKEN', + invocationEvents: [InvocationEvent.shake], +}); +``` + +- **Notes** + - iOS may require updating your Podfile platform, for example: `platform :ios, '13.0'` + - For attachments on iOS, add `NSMicrophoneUsageDescription` and `NSPhotoLibraryUsageDescription` to `Info.plist` per the docs + +#### upload-sourcemaps + +Upload JavaScript source maps to Instabug for crash symbolication/deobfuscation. + +```bash +npx instabug upload-sourcemaps \ + --platform ios \ + --file ./path/to/main.jsbundle.map \ + --token $INSTABUG_APP_TOKEN \ + --name 1.0.0 \ + --code 100 +``` + +- **Options** + + - `-p, --platform `: `ios` or `android`. Required + - `-f, --file `: Path to source map file. Required + - `-t, --token `: App token. Env: `INSTABUG_APP_TOKEN`. Required + - `-n, --name `: App version name. Env: `INSTABUG_APP_VERSION_NAME`. Required + - `-c, --code `: App version code. Env: `INSTABUG_APP_VERSION_CODE`. Required + - `-l, --label `: CodePush label (optional). Env: `INSTABUG_APP_VERSION_LABEL` + +- **Examples** + - iOS bundle and upload sourcemap: + +```bash +react-native bundle \ + --platform ios \ + --dev false \ + --entry-file index.js \ + --bundle-output ios/main.jsbundle \ + --sourcemap-output ios/main.jsbundle.map + +npx instabug upload-sourcemaps \ + --platform ios \ + --file ios/main.jsbundle.map \ + --token $INSTABUG_APP_TOKEN \ + --name 1.0.0 \ + --code 1 +``` + +- Android bundle and upload sourcemap: + +```bash +react-native bundle \ + --platform android \ + --dev false \ + --entry-file index.js \ + --bundle-output android/app/src/main/assets/index.android.bundle \ + --sourcemap-output android/app/src/main/assets/index.android.bundle.map + +npx instabug upload-sourcemaps \ + --platform android \ + --file android/app/src/main/assets/index.android.bundle.map \ + --token $INSTABUG_APP_TOKEN \ + --name 1.0.0 \ + --code 1 +``` + +#### upload-so-files + +Upload Android NDK `.so` symbol files for native crash symbolication. + +```bash +npx instabug upload-so-files \ + --arch arm64-v8a \ + --file ./symbols.zip \ + --api_key YOUR_ANDROID_API_KEY \ + --token $INSTABUG_APP_TOKEN \ + --name 1.0.0 +``` + +- **Options** + + - `--arch `: One of `x86`, `x86_64`, `arm64-v8a`, `armeabi-v7a`. Required + - `-f, --file `: Path to zipped `.so` files. Required + - `--api_key `: Your Android API key. Required + - `-t, --token `: App token. Env: `INSTABUG_APP_TOKEN`. Required + - `-n, --name `: App version name. Env: `INSTABUG_APP_VERSION_NAME`. Required + +- **Tips** + - Typical locations for NDK libs: `android/app/build/intermediates/merged_native_libs//out/lib/*/*.so` + - Zip the contents preserving folder structure before uploading + +#### help and version + +- Show root help: + +```bash +npx instabug --help +``` + +- Show subcommand help (example): + +```bash +npx instabug upload-sourcemaps --help +``` + +- Print CLI version: + +```bash +npx instabug --version +``` + +### Troubleshooting + +- iOS CocoaPods errors about Instabug version: + + - Run `pod install --repo-update` + - Ensure `platform :ios, '13.0'` (or higher, per the SDK requirements) + +- Yarn workspace issues when initializing a new RN app: + - Prefer `--skip-install` then run `npm i` inside the app folder + +### Environment variables + +- `INSTABUG_APP_TOKEN` +- `INSTABUG_APP_VERSION_NAME` +- `INSTABUG_APP_VERSION_CODE` +- `INSTABUG_APP_VERSION_LABEL` + +### Reference + +- React Native integration guide: [Instabug Docs](https://docs.instabug.com/docs/react-native-integration) diff --git a/cli/commands/Init.ts b/cli/commands/Init.ts new file mode 100644 index 000000000..7efc2a50b --- /dev/null +++ b/cli/commands/Init.ts @@ -0,0 +1,59 @@ +import { Command, Option } from 'commander'; +import { initInstabug, InitOptions } from '../init/init'; + +export const initCommand = new Command(); + +initCommand + .name('init') + .description('Initialize Instabug React Native SDK with basic instrumentation') + .addOption( + new Option('-t, --token ', 'Your App Token') + .env('INSTABUG_APP_TOKEN') + .makeOptionMandatory(), + ) + .addOption( + new Option( + '-e, --entry ', + 'Path to your React Native entry file (e.g., index.js)', + ).default(''), + ) + .addOption( + new Option( + '--invocation-events ', + 'Comma-separated InvocationEvent values (none,shake,screenshot,twoFingersSwipe,floatingButton)', + ), + ) + .addOption( + new Option('--debug-logs-level ', 'SDK debug logs level').choices([ + 'verbose', + 'debug', + 'error', + 'none', + ]), + ) + .addOption( + new Option('--code-push-version ', 'CodePush version label to attach to reports'), + ) + .addOption( + new Option( + '--ignore-android-secure-flag', + 'Override Android secure flag (allow screenshots)', + ).default(false), + ) + .addOption( + new Option('--network-interception-mode ', 'Network interception mode').choices([ + 'javascript', + 'native', + ]), + ) + .addOption(new Option('--npm', 'Use npm as the package manager').conflicts('yarn')) + .addOption(new Option('--yarn', 'Use yarn as the package manager')) + .addOption(new Option('--no-pods', 'Skip running pod install in the ios directory')) + .addOption( + new Option('--silent', 'Reduce logging and never exit the process on error').default(false), + ) + .action(function (this: Command) { + const opts = this.opts(); + initInstabug(opts); + }) + .showHelpAfterError(); diff --git a/cli/index.ts b/cli/index.ts index 8df747e75..8181af244 100644 --- a/cli/index.ts +++ b/cli/index.ts @@ -3,6 +3,7 @@ import { Command } from 'commander'; import { uploadSourcemapsCommand } from './commands/UploadSourcemaps'; import { UploadSoFilesCommand } from './commands/UploadSoFiles'; +import { initCommand } from './commands/Init'; const program = new Command(); @@ -12,6 +13,7 @@ program .description('A CLI for uploading source maps to Instabug dashboard.') .usage('[command]') .addCommand(uploadSourcemapsCommand) - .addCommand(UploadSoFilesCommand); + .addCommand(UploadSoFilesCommand) + .addCommand(initCommand); program.parse(process.argv); diff --git a/cli/init/init.ts b/cli/init/init.ts new file mode 100644 index 000000000..7aa45ff50 --- /dev/null +++ b/cli/init/init.ts @@ -0,0 +1,342 @@ +// @ts-ignore +import fs from 'fs'; +// @ts-ignore +import path from 'path'; +import { spawnSync } from 'child_process'; + +export interface InitOptions { + token: string; + entry?: string; + npm?: boolean; + yarn?: boolean; + pods?: boolean; // commander will map --no-pods to pods=false + silent?: boolean; + + // Instabug.init config options + invocationEvents?: string; // comma-separated + debugLogsLevel?: 'verbose' | 'debug' | 'error' | 'none'; + codePushVersion?: string; + ignoreAndroidSecureFlag?: boolean; + networkInterceptionMode?: 'javascript' | 'native'; +} + +export const initInstabug = async (opts: InitOptions): Promise => { + const projectRoot = process.cwd(); + + const isExpo = isExpoManagedProject(projectRoot); + const packageManager = resolvePackageManager(projectRoot, opts); + const entryFilePath = resolveEntryFile(projectRoot, opts.entry, isExpo); + + const installed = installSdk(projectRoot, packageManager, opts.silent); + if (!installed) { + return false; + } + + // For Expo managed projects, skip pods. Native code is generated during prebuild/EAS build. + const podInstalled = isExpo ? true : maybeInstallPods(projectRoot, opts.pods, opts.silent); + if (!podInstalled) { + return false; + } + + if (isExpo) { + const configured = configureExpoPlugins(projectRoot, opts.silent); + if (!configured) { + return false; + } + } + + const instrumented = injectInitialization(entryFilePath, opts, opts.silent); + if (!instrumented) { + return false; + } + + if (!opts.silent) { + console.log('Instabug React Native SDK initialized successfully.'); + console.log('Documentation: https://docs.instabug.com/docs/react-native-integration'); + } + + return true; +}; + +const resolvePackageManager = ( + projectRoot: string, + opts: Pick, +): 'npm' | 'yarn' => { + if (opts.npm) { + return 'npm'; + } + if (opts.yarn) { + return 'yarn'; + } + + const hasYarnLock = fs.existsSync(path.join(projectRoot, 'yarn.lock')); + return hasYarnLock ? 'yarn' : 'npm'; +}; + +const resolveEntryFile = ( + projectRoot: string, + customEntry?: string, + preferAppFile?: boolean, +): string => { + if (customEntry) { + const absolute = path.isAbsolute(customEntry) + ? customEntry + : path.join(projectRoot, customEntry); + if (!fs.existsSync(absolute)) { + throw new Error(`Entry file not found at ${absolute}`); + } + return absolute; + } + + const appCandidates = [ + path.join(projectRoot, 'App.tsx'), + path.join(projectRoot, 'App.ts'), + path.join(projectRoot, 'App.jsx'), + path.join(projectRoot, 'App.js'), + ]; + + const indexCandidates = [ + path.join(projectRoot, 'index.js'), + path.join(projectRoot, 'index.ts'), + path.join(projectRoot, 'index.tsx'), + path.join(projectRoot, 'src', 'index.js'), + path.join(projectRoot, 'src', 'index.ts'), + path.join(projectRoot, 'src', 'index.tsx'), + ]; + + const candidates = preferAppFile + ? [...appCandidates, ...indexCandidates] + : [...indexCandidates, ...appCandidates]; + + for (const candidate of candidates) { + if (fs.existsSync(candidate)) { + return candidate; + } + } + + throw new Error('Could not detect an entry file. Pass a path with --entry (e.g., index.js).'); +}; + +const installSdk = ( + projectRoot: string, + packageManager: 'npm' | 'yarn', + silent?: boolean, +): boolean => { + const args = + packageManager === 'yarn' + ? ['add', 'instabug-reactnative'] + : ['install', 'instabug-reactnative']; + + if (!silent) { + console.log(`Installing instabug-reactnative using ${packageManager}...`); + } + + const result = spawnSync(packageManager, args, { stdio: 'inherit', cwd: projectRoot }); + return result.status === 0; +}; + +const maybeInstallPods = ( + projectRoot: string, + pods: boolean | undefined, + silent?: boolean, +): boolean => { + // Default is true unless explicitly disabled via --no-pods + const shouldRunPods = pods !== false; + const iosDir = path.join(projectRoot, 'ios'); + const podfile = path.join(iosDir, 'Podfile'); + + if (!shouldRunPods || !fs.existsSync(iosDir) || !fs.existsSync(podfile)) { + return true; + } + + if (!silent) { + console.log('Running pod install in ios directory...'); + } + + const result = spawnSync('bash', ['-lc', 'cd ios && pod install'], { + stdio: 'inherit', + cwd: projectRoot, + }); + + return result.status === 0; +}; + +const injectInitialization = ( + entryFilePath: string, + opts: InitOptions, + silent?: boolean, +): boolean => { + const original = fs.readFileSync(entryFilePath, 'utf8'); + + if ( + original.includes("from 'instabug-reactnative'") || + original.includes('instabug-reactnative') + ) { + if (!silent) { + console.log( + 'Instabug appears to be already referenced in the entry file. Skipping injection.', + ); + } + return true; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const isTS = entryFilePath.endsWith('.ts') || entryFilePath.endsWith('.tsx'); + const importLine = "import Instabug, { InvocationEvent } from 'instabug-reactnative';\n"; + const configLines: string[] = []; + configLines.push(`token: '${opts.token}',`); + + const events = parseInvocationEvents(opts.invocationEvents); + if (events.length > 0) { + configLines.push( + `invocationEvents: [${events.map((e) => `InvocationEvent.${e}`).join(', ')}],`, + ); + } else { + configLines.push('invocationEvents: [InvocationEvent.shake],'); + } + + if (opts.debugLogsLevel) { + configLines.push(`debugLogsLevel: '${opts.debugLogsLevel}',`); + } + if (opts.codePushVersion) { + configLines.push(`codePushVersion: '${opts.codePushVersion}',`); + } + if (typeof opts.ignoreAndroidSecureFlag === 'boolean' && opts.ignoreAndroidSecureFlag) { + configLines.push('ignoreAndroidSecureFlag: true,'); + } + if (opts.networkInterceptionMode) { + configLines.push(`networkInterceptionMode: '${opts.networkInterceptionMode}',`); + } + + const initBlock = `\nInstabug.init({\n ${configLines.join('\n ')}\n});\n`; + + const hasUseStrict = original.startsWith("'use strict'") || original.startsWith('"use strict"'); + const insertion = hasUseStrict + ? original.replace(/(['\"]use strict['\"];?\s*)/, `$1\n${importLine}`) + : importLine + original; + const withInit = insertion + initBlock; + + fs.writeFileSync(entryFilePath, withInit, 'utf8'); + + if (!silent) { + console.log( + `Injected Instabug initialization into ${path.relative(process.cwd(), entryFilePath)}`, + ); + } + return true; +}; + +const isExpoManagedProject = (projectRoot: string): boolean => { + const appJsonPath = path.join(projectRoot, 'app.json'); + if (!fs.existsSync(appJsonPath)) { + return false; + } + + try { + const content = JSON.parse(fs.readFileSync(appJsonPath, 'utf8')); + return content && typeof content === 'object' && content.expo != null; + } catch { + return false; + } +}; + +const configureExpoPlugins = (projectRoot: string, silent?: boolean): boolean => { + const appJsonPath = path.join(projectRoot, 'app.json'); + try { + const appJson = JSON.parse(fs.readFileSync(appJsonPath, 'utf8')) as Record; + if (!appJson.expo) { + return false; + } + + const expoConfig = appJson.expo as Record; + const plugins: any[] = Array.isArray(expoConfig.plugins) ? [...expoConfig.plugins] : []; + + const hasInstabugPlugin = plugins.some((p) => { + if (typeof p === 'string') { + return p === 'instabug-reactnative/expo-plugin'; + } + return Array.isArray(p) && p[0] === 'instabug-reactnative/expo-plugin'; + }); + + if (!hasInstabugPlugin) { + plugins.push('instabug-reactnative/expo-plugin'); + } + + let updatedBuildProps = false; + const minExpoIosTarget = '15.1'; + const idx = plugins.findIndex((p) => Array.isArray(p) && p[0] === 'expo-build-properties'); + if (idx === -1) { + plugins.push([ + 'expo-build-properties', + { + ios: { deploymentTarget: minExpoIosTarget }, + }, + ]); + updatedBuildProps = true; + } else { + const [, cfg] = plugins[idx] as [string, any]; + const current = cfg?.ios?.deploymentTarget as string | undefined; + if (!current || compareIosVersions(current, minExpoIosTarget) < 0) { + plugins[idx] = [ + 'expo-build-properties', + { + ...cfg, + ios: { ...(cfg?.ios ?? {}), deploymentTarget: minExpoIosTarget }, + }, + ]; + updatedBuildProps = true; + } + } + + expoConfig.plugins = plugins; + appJson.expo = expoConfig; + + fs.writeFileSync(appJsonPath, JSON.stringify(appJson, null, 2) + '\n', 'utf8'); + + if (!silent) { + console.log( + `Configured Expo plugins: instabug-reactnative/expo-plugin and expo-build-properties${ + updatedBuildProps ? ' (ios.deploymentTarget >= 15.1)' : '' + }`, + ); + } + + return true; + } catch (e) { + if (!silent) { + console.error('Failed to configure Expo plugins in app.json:', e); + } + return false; + } +}; + +const parseInvocationEvents = ( + list?: string, +): Array<'none' | 'shake' | 'screenshot' | 'twoFingersSwipe' | 'floatingButton'> => { + if (!list) { + return []; + } + return list + .split(',') + .map((s) => s.trim()) + .filter((s): s is 'none' | 'shake' | 'screenshot' | 'twoFingersSwipe' | 'floatingButton' => + ['none', 'shake', 'screenshot', 'twoFingersSwipe', 'floatingButton'].includes(s as any), + ); +}; + +const compareIosVersions = (a: string, b: string): number => { + const pa = a.split('.').map((n) => parseInt(n, 10)); + const pb = b.split('.').map((n) => parseInt(n, 10)); + const len = Math.max(pa.length, pb.length); + for (let i = 0; i < len; i++) { + const va = pa[i] ?? 0; + const vb = pb[i] ?? 0; + if (va < vb) { + return -1; + } + if (va > vb) { + return 1; + } + } + return 0; +}; diff --git a/cli/upload/index.ts b/cli/upload/index.ts index b09f4b243..ccb2f1db4 100644 --- a/cli/upload/index.ts +++ b/cli/upload/index.ts @@ -1,2 +1,4 @@ export * from './uploadSourcemaps'; export * from './uploadSoFiles'; +export * from './uploadSourcemaps'; +export * from './uploadSoFiles'; diff --git a/examples/default/ios/Podfile.lock b/examples/default/ios/Podfile.lock index 274dbfc09..674914c0b 100644 --- a/examples/default/ios/Podfile.lock +++ b/examples/default/ios/Podfile.lock @@ -2096,8 +2096,8 @@ SPEC CHECKSUMS: RNSVG: 8b1a777d54096b8c2a0fd38fc9d5a454332bbb4d RNVectorIcons: 6382277afab3c54658e9d555ee0faa7a37827136 SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d - Yoga: 055f92ad73f8c8600a93f0e25ac0b2344c3b07e6 + Yoga: aa3df615739504eebb91925fc9c58b4922ea9a08 PODFILE CHECKSUM: 837b933596e1616ff02cc206bb17dee4f611fdbc -COCOAPODS: 1.14.0 +COCOAPODS: 1.15.2 diff --git a/expo-plugin/index.js b/expo-plugin/index.js new file mode 100644 index 000000000..d438f5427 --- /dev/null +++ b/expo-plugin/index.js @@ -0,0 +1,10 @@ +// Minimal Expo config plugin for Instabug React Native +// This currently acts as a no-op because deployment target is enforced via expo-build-properties +// Added to ensure Expo can resolve the plugin during managed workflows and prebuild + +/** @type {import('@expo/config-plugins').ConfigPlugin} */ +function withInstabug(config) { + return config; +} + +module.exports = withInstabug; diff --git a/package.json b/package.json index 61af51056..f19be6882 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "main": "dist/index", "types": "dist/index.d.ts", "react-native": "src/index.ts", + "expo": "./expo-plugin/index.js", "bin": { "instabug": "bin/index.js" }, From c750e201bf6505b9d7da43bf6c1d5c2fc29514c7 Mon Sep 17 00:00:00 2001 From: Andrew Amin Date: Mon, 25 Aug 2025 19:16:04 +0300 Subject: [PATCH 2/2] chore: update CLI README.md --- cli/README.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/cli/README.md b/cli/README.md index 3ec8c7679..44e6a98c7 100644 --- a/cli/README.md +++ b/cli/README.md @@ -40,8 +40,8 @@ npx instabug init --token YOUR_APP_TOKEN --entry App.tsx # 2) Ensure build properties plugin is installed in your project npm i -D expo-build-properties -# 3) Run on iOS (set CI to auto-accept alternate port if 8081 is busy) -CI=1 npx expo start --ios +# 3) Run the app (set CI to auto-accept alternate port if 8081 is busy) +CI=1 npx expo start --ios | android ``` - Notes: @@ -57,12 +57,6 @@ CI=1 npx expo start --ios npx instabug --help ``` -- When developing this repo locally, you can invoke the built CLI directly: - -```bash -node bin/index.js --help -``` - ### Commands #### init