@livx.cc/appwrap 0.39.12 → 0.39.13

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli.ts +90 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livx.cc/appwrap",
3
- "version": "0.39.12",
3
+ "version": "0.39.13",
4
4
  "description": "Wrap any PWA into a native app with native capabilities (appwrap runtime + @livx.cc/native-kit).",
5
5
  "license": "MIT",
6
6
  "author": "Elya Livshitz",
package/src/cli.ts CHANGED
@@ -1776,11 +1776,99 @@ function pickDevice(devices: DeviceInfo[], explicitId?: string): DeviceInfo {
1776
1776
  * distribution signing) — for testing on your own device. Run the PWA build first (or via the script).
1777
1777
  * iOS has a bespoke Debug-IPA path (below); Android delegates to `run` (NativeScript builds + installs
1778
1778
  * + launches), so the `deploy <platform>` surface is symmetric across both. */
1779
+ /** Resolve adb: $ANDROID_HOME/$ANDROID_SDK_ROOT platform-tools, else `adb` on PATH. */
1780
+ function androidAdb(): string {
1781
+ const home = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
1782
+ if (home) {
1783
+ const p = join(home, 'platform-tools', 'adb');
1784
+ if (existsSync(p)) return p;
1785
+ }
1786
+ return 'adb';
1787
+ }
1788
+
1789
+ /** Authorized (`device` state) adb serials. Skips `unauthorized`/`offline`. */
1790
+ function listAndroidDevices(adb: string): string[] {
1791
+ try {
1792
+ return execFileSync(adb, ['devices'], { encoding: 'utf8' })
1793
+ .split('\n').slice(1)
1794
+ .filter((l) => /\tdevice\b/.test(l))
1795
+ .map((l) => l.split('\t')[0].trim())
1796
+ .filter(Boolean);
1797
+ } catch {
1798
+ return [];
1799
+ }
1800
+ }
1801
+
1802
+ /** Pick the target Android device (fail fast, like iOS's pickDevice): explicit --device, else the
1803
+ * single connected one; clear errors for none / multiple / unauthorized. */
1804
+ function pickAndroidDevice(adb: string, flag?: string): string {
1805
+ const devices = listAndroidDevices(adb);
1806
+ if (flag) {
1807
+ if (devices.includes(flag)) return flag;
1808
+ console.error(`✖ Device "${flag}" not connected/authorized. Authorized: ${devices.join(', ') || '(none)'}`);
1809
+ process.exit(1);
1810
+ }
1811
+ if (devices.length === 0) {
1812
+ console.error('✖ No authorized Android device. Connect via USB + accept the "Allow USB debugging" prompt (check with `adb devices`).');
1813
+ process.exit(1);
1814
+ }
1815
+ if (devices.length > 1) {
1816
+ console.error(`✖ Multiple devices — pass --device <id>: ${devices.join(', ')}`);
1817
+ process.exit(1);
1818
+ }
1819
+ return devices[0];
1820
+ }
1821
+
1822
+ /** `appwrap deploy android` — ONE-SHOT device deploy, the Android twin of `deploy ios`: sync + debug
1823
+ * config → `ns build android` (debug APK) → `adb install -r` to the device → launch (unless --no-launch)
1824
+ * → exit. Unlike `run android` (ns watch-mode), it doesn't stay attached. NOTE: like `deploy ios`, it
1825
+ * ships the CURRENT `dist/` — build the web first (`bun run build`, or use the `bun run android` script). */
1826
+ async function deployAndroid(cwd: string, flags: Record<string, string>): Promise<void> {
1827
+ const cfg = await loadConfig(cwd, flags);
1828
+ const outDir = resolve(cwd, flags.out ?? 'native');
1829
+ if (!existsSync(outDir)) {
1830
+ console.error(`✖ Wrapper not found at ${outDir} — run \`appwrap init\` first`);
1831
+ process.exit(1);
1832
+ }
1833
+ const adb = androidAdb();
1834
+ const device = pickAndroidDevice(adb, flags.device || undefined); // fail fast before the build
1835
+
1836
+ await sync(cwd, flags); // re-stamp config + copy latest PWA dist
1837
+ stampShellConfig(outDir, { ...cfg, debug: true }); // debug: keep-awake + WebView inspector (parity with deploy ios)
1838
+
1839
+ console.log('▶ ns build android (debug)');
1840
+ runNs(outDir, ['build', 'android']); // bun-installs deps if needed, then gradle build
1841
+ const apk = join(outDir, 'platforms/android/app/build/outputs/apk/debug/app-debug.apk');
1842
+ if (!existsSync(apk)) { console.error(`✖ No APK produced at ${apk}`); process.exit(1); }
1843
+
1844
+ console.log(`▶ installing → ${device}`);
1845
+ try {
1846
+ execFileSync(adb, ['-s', device, 'install', '-r', apk], { stdio: 'inherit' });
1847
+ } catch (e: unknown) {
1848
+ const log = execErrText(e);
1849
+ if (/INSTALL_FAILED_USER_RESTRICTED|user is restricted/i.test(log)) {
1850
+ console.error('\n✖ Install blocked by the device. On MIUI/Xiaomi: Developer options → enable "Install via USB" (may need mobile data + a SIM, no VPN).');
1851
+ }
1852
+ process.exit(1);
1853
+ }
1854
+
1855
+ if (!('no-launch' in flags)) {
1856
+ console.log(`▶ launching ${cfg.id}`);
1857
+ try {
1858
+ execFileSync(adb, ['-s', device, 'shell', 'monkey', '-p', cfg.id, '-c', 'android.intent.category.LAUNCHER', '1'], { stdio: 'ignore' });
1859
+ } catch {
1860
+ console.error('⚠ Launch failed — the app is installed; tap its icon.');
1861
+ }
1862
+ }
1863
+ console.log(`✓ Deployed to ${device}.`);
1864
+ }
1865
+
1779
1866
  async function deploy(cwd: string, flags: Record<string, string>, positionals: string[]): Promise<void> {
1780
1867
  const platform = positionals[0];
1781
1868
  if (platform === 'android') {
1782
- // Aligned surface: `deploy android` == `run android` (Android's run already does build+install+launch).
1783
- return run(cwd, flags, positionals);
1869
+ // Parity with `deploy ios`: a clean ONE-SHOT build → install-to-device → launch → exit (NOT `ns run`,
1870
+ // which is watch-mode + hangs on some devices). Mirrors the iOS path with the adb toolchain.
1871
+ return deployAndroid(cwd, flags);
1784
1872
  }
1785
1873
  if (platform !== 'ios') {
1786
1874
  console.error('Usage: appwrap deploy <ios|android> [--device <id|name>] [--no-launch]');