@livx.cc/appwrap 0.40.1 → 0.41.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.
- package/package.json +1 -1
- package/runtime/package.json +1 -0
- package/src/cli.ts +216 -36
package/package.json
CHANGED
package/runtime/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -1268,23 +1268,26 @@ function openInspector(cfg: AppwrapConfig, flags: Record<string, string>, platfo
|
|
|
1268
1268
|
/** `appwrap dev <ios|android> [--sim] [--detached] [--debug] [--url <devserver>|--port <p>]` — the
|
|
1269
1269
|
* live-dev loop. Subsumes the old `run`/`debug` verbs AND the old `dev` (loader:server stamp).
|
|
1270
1270
|
*
|
|
1271
|
-
* Default target = the physical DEVICE
|
|
1272
|
-
*
|
|
1273
|
-
*
|
|
1274
|
-
*
|
|
1271
|
+
* Default target = the physical DEVICE.
|
|
1272
|
+
* • ANDROID: `ns run` livesync for true on-device HMR (incremental, no full reinstall) + a source
|
|
1273
|
+
* watcher that rebuilds the web & re-stages www on save so PWA edits flow into the livesync. The old
|
|
1274
|
+
* "Invalid version … Got type object" crash that made this look unfixable was just the shell
|
|
1275
|
+
* package.json failing to declare @nativescript/android → ns read the runtime version as null.
|
|
1276
|
+
* • iOS: the proven deploy + redeploy-on-save loop (NOT ns run — `ns run ios --device` hits the
|
|
1277
|
+
* personal-team signing/registration path `deploy ios` handles bespokely; pending device-verify).
|
|
1275
1278
|
*
|
|
1276
1279
|
* Flags:
|
|
1277
1280
|
* --sim → emulator/simulator via `ns run` (HMR is reliable there); `--debug` → `ns debug`.
|
|
1278
1281
|
* --url/--port→ stamp loader:'server' at that dev-server URL (web hot-reloads inside the WebView), deploy + attach.
|
|
1279
|
-
* --detached → deploy + exit (install & launch only; don't attach/watch).
|
|
1280
|
-
* --debug →
|
|
1282
|
+
* --detached → deploy + exit (install & launch only; don't attach/watch — the MIUI-safe `--user 0` install).
|
|
1283
|
+
* --debug → android: `ns debug` (inspector); iOS/url: open the WebView inspector then attach.
|
|
1281
1284
|
*/
|
|
1282
1285
|
async function dev(cwd: string, flags: Record<string, string>, positionals: string[]): Promise<void> {
|
|
1283
1286
|
const platform = positionals[0];
|
|
1284
1287
|
const sim = 'sim' in flags || positionals[1] === 'sim';
|
|
1285
1288
|
const wantDebug = 'debug' in flags;
|
|
1286
1289
|
if (platform !== 'ios' && platform !== 'android') {
|
|
1287
|
-
console.error('Usage: appwrap dev <ios|android> [--sim] [--detached] [--debug] [--url <devserver>|--port <p>]');
|
|
1290
|
+
console.error('Usage: appwrap dev <ios|android> [--sim] [--detached] [--debug] [--wifi] [--device <id|ip[:port]>] [--url <devserver>|--port <p>]');
|
|
1288
1291
|
process.exit(1);
|
|
1289
1292
|
}
|
|
1290
1293
|
const cfg = await loadConfig(cwd, flags);
|
|
@@ -1326,39 +1329,62 @@ async function dev(cwd: string, flags: Record<string, string>, positionals: stri
|
|
|
1326
1329
|
return;
|
|
1327
1330
|
}
|
|
1328
1331
|
|
|
1329
|
-
// ── device:
|
|
1332
|
+
// ── ANDROID device + bundled loader: ns run livesync = true on-device HMR (incremental, no reinstall) ──
|
|
1333
|
+
// Unblocked by declaring @nativescript/android in the shell package.json: ns reads the android runtime
|
|
1334
|
+
// version on the livesync path; when it's undeclared that lookup returns null → `semver.gt(null, …)` →
|
|
1335
|
+
// the "Invalid version … Got type object" crash that long made on-device livesync look unfixable.
|
|
1336
|
+
// ns watches native/app + native/www-src; since the PWA SOURCE lives OUTSIDE native/, we run a source
|
|
1337
|
+
// watcher alongside that rebuilds the web + re-stages www on save — ns's livesync then pushes it.
|
|
1338
|
+
// (Must spawn ns async, not execFileSync: a sync exec freezes the fs.watch loop.)
|
|
1339
|
+
// iOS is intentionally NOT on ns run here: `ns run ios --device` hits the personal-team device
|
|
1340
|
+
// registration/signing path that `deploy ios` handles bespokely — unverified, so iOS keeps the proven
|
|
1341
|
+
// deploy + redeploy-on-save loop below until it's device-verified.
|
|
1342
|
+
if (platform === 'android' && !devUrl && !('detached' in flags)) {
|
|
1343
|
+
const device = resolveDevice(outDir, 'android', flags);
|
|
1344
|
+
// MIUI/Xiaomi auto-denies a bare `adb install`; only `--user 0` works (deploy uses it). Pre-installing
|
|
1345
|
+
// via deploy establishes the package so `ns run`'s subsequent install lands as an allowed UPDATE
|
|
1346
|
+
// rather than a blocked fresh bare-install — the device-verified path. (~one extra fast install.)
|
|
1347
|
+
await deploy(cwd, { ...flags, 'no-launch': '' }, ['android'], cfg);
|
|
1348
|
+
const nsArgs = [wantDebug ? 'debug' : 'run', 'android', '--device', device.id];
|
|
1349
|
+
const env = prepareNsEnv(outDir, nsArgs);
|
|
1350
|
+
console.log(`\n▶ dev: ns ${nsArgs.join(' ')} (on-device HMR) + watching sources → re-stage on save. Ctrl-C to stop.`);
|
|
1351
|
+
// Own process group so Ctrl-C / kill reaps ns AND its grandchildren (gradle, adb logcat, webpack).
|
|
1352
|
+
const nsChild = spawn('ns', nsArgs, { cwd: outDir, stdio: 'inherit', detached: true, env });
|
|
1353
|
+
const stop = () => { try { if (nsChild.pid) process.kill(-nsChild.pid); } catch { /* already gone */ } };
|
|
1354
|
+
process.on('exit', stop);
|
|
1355
|
+
process.on('SIGINT', () => { stop(); process.exit(0); });
|
|
1356
|
+
nsChild.on('exit', (code) => process.exit(code ?? 0));
|
|
1357
|
+
await watchAndSync(cwd, flags, outDir, cfg); // rebuild + re-stage on save; ns livesync pushes it
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// ── iOS device (bundled), --url, or --detached: the proven one-shot deploy path ──
|
|
1330
1362
|
await deploy(cwd, flags, [platform], devUrl ? effectiveCfg : undefined);
|
|
1331
1363
|
// Follow-ups reuse the just-deployed device from last-device memory — drop an interactive `-d`.
|
|
1332
1364
|
const followFlags = { ...flags }; delete followFlags.d;
|
|
1333
|
-
|
|
1334
1365
|
if (wantDebug) openInspector(effectiveCfg, followFlags, platform, outDir);
|
|
1335
|
-
|
|
1336
1366
|
if ('detached' in flags) {
|
|
1337
1367
|
console.log('\n✓ --detached — installed & launched; not attaching/watching.');
|
|
1338
1368
|
return;
|
|
1339
1369
|
}
|
|
1340
|
-
|
|
1341
|
-
// Attach: stream the WebView console. With a bundled loader we ALSO watch sources → rebuild+reinstall
|
|
1342
|
-
// on save. With a dev-server loader (--url) the web hot-reloads from the server INSIDE the WebView, so
|
|
1343
|
-
// a native rebuild is pointless (and would re-stamp the bundled loader) — just stream the console.
|
|
1344
1370
|
if (devUrl) {
|
|
1371
|
+
// --url: the web hot-reloads from the dev server INSIDE the WebView; just stream the console.
|
|
1345
1372
|
console.log('\n▶ dev: web hot-reloads from the dev server inside the WebView; streaming the console. Ctrl-C to stop.');
|
|
1346
1373
|
await logs(cwd, followFlags, [platform]);
|
|
1347
1374
|
return;
|
|
1348
1375
|
}
|
|
1349
|
-
|
|
1376
|
+
// iOS bundled device: stream the WebView console + redeploy (rebuild+reinstall) on save. No ns livesync
|
|
1377
|
+
// on an iOS device yet (see above), so a full redeploy is the device-safe refresh path.
|
|
1378
|
+
console.log('\n▶ dev: streaming WebView console + watching sources (edit a file → rebuild+reinstall). Ctrl-C to stop.');
|
|
1350
1379
|
const logArgs = [import.meta.path, 'logs', platform];
|
|
1351
1380
|
if (followFlags.device) logArgs.push('--device', followFlags.device);
|
|
1352
1381
|
if (followFlags.out) logArgs.push('--out', followFlags.out);
|
|
1353
1382
|
if (followFlags.config) logArgs.push('--config', followFlags.config);
|
|
1354
|
-
// `detached: true`
|
|
1355
|
-
// the child is `bun … logs`, which itself spawns `adb logcat`; `logChild.kill()` would only reap the
|
|
1356
|
-
// `bun` and orphan the `adb logcat` grandchild when the signal hits the leader pid (e.g. `kill <pid>`
|
|
1357
|
-
// / a supervisor, not interactive Ctrl-C which signals the group). `process.kill(-pid)` reaps both.
|
|
1383
|
+
// `detached: true` → own process group so we reap the whole tree (bun → adb/idevicesyslog) on Ctrl-C.
|
|
1358
1384
|
const logChild = spawn('bun', logArgs, { stdio: 'inherit', detached: true });
|
|
1359
|
-
const
|
|
1360
|
-
process.on('exit',
|
|
1361
|
-
process.on('SIGINT', () => {
|
|
1385
|
+
const stopLog = () => { try { if (logChild.pid) process.kill(-logChild.pid); } catch { /* already gone */ } };
|
|
1386
|
+
process.on('exit', stopLog);
|
|
1387
|
+
process.on('SIGINT', () => { stopLog(); process.exit(0); });
|
|
1362
1388
|
await watchAndRedeploy(cwd, followFlags, platform);
|
|
1363
1389
|
}
|
|
1364
1390
|
|
|
@@ -1386,7 +1412,9 @@ function resolveAndroidSdk(): string | undefined {
|
|
|
1386
1412
|
return candidates.find((d) => existsSync(join(d, 'platform-tools')) || existsSync(join(d, 'platforms')));
|
|
1387
1413
|
}
|
|
1388
1414
|
|
|
1389
|
-
|
|
1415
|
+
/** Build the env for an `ns` invocation in `outDir` (auto-detect Android SDK, ensure bun-installed deps).
|
|
1416
|
+
* Shared by the blocking `runNs` (sim) and the non-blocking spawn in `dev` (device livesync). */
|
|
1417
|
+
function prepareNsEnv(outDir: string, args: string[]): NodeJS.ProcessEnv {
|
|
1390
1418
|
const env: NodeJS.ProcessEnv = { ...process.env };
|
|
1391
1419
|
// Android: inject a discovered SDK so `ns` finds it even when the user's shell never exported
|
|
1392
1420
|
// ANDROID_HOME (new terminal not sourced, conda base shell, etc.) — the common deploy blocker.
|
|
@@ -1405,6 +1433,11 @@ function runNs(outDir: string, args: string[]): void {
|
|
|
1405
1433
|
console.log(`▶ bun install (cwd: ${outDir})`);
|
|
1406
1434
|
execFileSync('bun', ['install'], { cwd: outDir, stdio: 'inherit', env });
|
|
1407
1435
|
}
|
|
1436
|
+
return env;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
function runNs(outDir: string, args: string[]): void {
|
|
1440
|
+
const env = prepareNsEnv(outDir, args);
|
|
1408
1441
|
console.log(`▶ ns ${args.join(' ')} (cwd: ${outDir})`);
|
|
1409
1442
|
execFileSync('ns', args, { cwd: outDir, stdio: 'inherit', env });
|
|
1410
1443
|
}
|
|
@@ -1427,22 +1460,58 @@ function readStampedLoader(outDir: string): { loader: string; serverUrl: string;
|
|
|
1427
1460
|
}
|
|
1428
1461
|
}
|
|
1429
1462
|
|
|
1430
|
-
/**
|
|
1431
|
-
* source
|
|
1432
|
-
*
|
|
1463
|
+
/** Watch loop for iOS `dev` (no on-device ns livesync yet): re-run the clean deploy path on a project
|
|
1464
|
+
* source change (debounced) — a full rebuild+reinstall, the device-safe refresh for iOS. Skips
|
|
1465
|
+
* generated/output dirs. macOS recursive fs.watch. (Android uses watchAndSync + ns livesync instead.) */
|
|
1433
1466
|
async function watchAndRedeploy(cwd: string, flags: Record<string, string>, platform: 'ios' | 'android'): Promise<void> {
|
|
1434
1467
|
const { watch } = await import('fs');
|
|
1435
|
-
const ignore = /(^|\/)(native|node_modules|dist|\.git|\.appwrap)(\/|$)/;
|
|
1468
|
+
const ignore = /(^|\/)(native|node_modules|dist|public|\.git|\.appwrap)(\/|$)/;
|
|
1436
1469
|
console.log(`\n👀 watching ${cwd} for changes → rebuild+reinstall on save (Ctrl-C to stop).`);
|
|
1437
1470
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1438
1471
|
let busy = false;
|
|
1472
|
+
let quietUntil = 0;
|
|
1439
1473
|
watch(cwd, { recursive: true }, (_evt, file) => {
|
|
1440
|
-
if (!file || ignore.test(String(file)) || busy) return;
|
|
1474
|
+
if (!file || ignore.test(String(file)) || busy || Date.now() < quietUntil) return;
|
|
1441
1475
|
clearTimeout(timer);
|
|
1442
1476
|
timer = setTimeout(async () => {
|
|
1443
1477
|
busy = true;
|
|
1444
1478
|
console.log(`\n🔁 change: ${file} → redeploying…`);
|
|
1445
1479
|
try { await deploy(cwd, flags, [platform]); } catch (e) { console.error(`⚠ redeploy failed: ${(e as Error).message}`); }
|
|
1480
|
+
quietUntil = Date.now() + 1500;
|
|
1481
|
+
busy = false;
|
|
1482
|
+
}, 600);
|
|
1483
|
+
});
|
|
1484
|
+
await new Promise<void>(() => { /* run until Ctrl-C */ });
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
/** Lean watch loop for Android `dev`: on a project source change, rebuild the web + RE-STAGE only
|
|
1488
|
+
* the web bundle (dist → native/www-src) — debounced. Deliberately NOT a full `regenerateCore`: that
|
|
1489
|
+
* re-copies App_Resources/package.json and makes ns do a full native rebuild+reinstall (defeating HMR
|
|
1490
|
+
* and tripping MIUI). Staging www-src only keeps the `ns run` livesync on the JS hot-push path. The
|
|
1491
|
+
* watcher sees the PROJECT dir, so it only catches PWA source edits (the shell template lives elsewhere).
|
|
1492
|
+
* Skips generated/output dirs. macOS recursive fs.watch. */
|
|
1493
|
+
async function watchAndSync(cwd: string, flags: Record<string, string>, outDir: string, cfg: AppwrapConfig): Promise<void> {
|
|
1494
|
+
const { watch } = await import('fs');
|
|
1495
|
+
// Skip generated/output trees. `dist` is the web build output; `public` is where many build steps
|
|
1496
|
+
// ALSO emit (e.g. a copied bundle / stamped index) — both must be ignored or the rebuild's own writes
|
|
1497
|
+
// re-trigger the watch in a loop. A post-rebuild cooldown is the generic backstop for any other
|
|
1498
|
+
// output dir we don't know about (the project's build target is project-specific).
|
|
1499
|
+
const ignore = /(^|\/)(native|node_modules|dist|public|\.git|\.appwrap)(\/|$)/;
|
|
1500
|
+
console.log(`👀 watching ${cwd} → rebuild + re-stage on save (ns livesync pushes it).`);
|
|
1501
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1502
|
+
let busy = false;
|
|
1503
|
+
let quietUntil = 0; // ignore events for a beat after a rebuild — its own file writes aren't user edits
|
|
1504
|
+
watch(cwd, { recursive: true }, (_evt, file) => {
|
|
1505
|
+
if (!file || ignore.test(String(file)) || busy || Date.now() < quietUntil) return;
|
|
1506
|
+
clearTimeout(timer);
|
|
1507
|
+
timer = setTimeout(() => {
|
|
1508
|
+
busy = true;
|
|
1509
|
+
console.log(`\n🔁 change: ${file} → rebuild web + re-stage www…`);
|
|
1510
|
+
try {
|
|
1511
|
+
buildWebIfBundled(cwd, cfg, flags);
|
|
1512
|
+
copyPwa(cwd, outDir, cfg); // stage dist → www-src only; ns livesync hot-pushes it (no reinstall)
|
|
1513
|
+
} catch (e) { console.error(`⚠ re-stage failed: ${(e as Error).message}`); }
|
|
1514
|
+
quietUntil = Date.now() + 1500;
|
|
1446
1515
|
busy = false;
|
|
1447
1516
|
}, 600);
|
|
1448
1517
|
});
|
|
@@ -1871,7 +1940,8 @@ function listDevices(platform: 'ios' | 'android'): DeviceInfo[] {
|
|
|
1871
1940
|
return listAndroidDevices(adb).map((serial) => {
|
|
1872
1941
|
let model = '';
|
|
1873
1942
|
try { model = execFileSync(adb, ['-s', serial, 'shell', 'getprop', 'ro.product.model'], { encoding: 'utf8' }).trim(); } catch { /* offline */ }
|
|
1874
|
-
|
|
1943
|
+
// A network adb serial is `host:port` (USB serials never contain ':') → label it wifi.
|
|
1944
|
+
return { id: serial, name: model || serial, model, transport: serial.includes(':') ? 'wifi' : 'usb' };
|
|
1875
1945
|
});
|
|
1876
1946
|
}
|
|
1877
1947
|
|
|
@@ -1893,10 +1963,21 @@ function pickInteractively(devices: DeviceInfo[]): DeviceInfo {
|
|
|
1893
1963
|
/** Resolve the target device for a platform command (the reusable core). Persists the choice under
|
|
1894
1964
|
* `outDir` so the next command (e.g. `run`→`logs`) reuses it. */
|
|
1895
1965
|
function resolveDevice(outDir: string, platform: 'ios' | 'android', flags: Record<string, string>): DeviceInfo {
|
|
1896
|
-
const
|
|
1966
|
+
const adb = platform === 'android' ? androidAdb() : '';
|
|
1967
|
+
|
|
1968
|
+
// --wifi (android): flip a USB device to wireless adb (or reconnect a remembered one), then target it.
|
|
1969
|
+
if (platform === 'android' && 'wifi' in flags) enableWifiAdb(adb, outDir, flags);
|
|
1970
|
+
|
|
1971
|
+
// --device <ip[:port]> (android): if it's a network target that isn't attached yet, `adb connect` it.
|
|
1972
|
+
if (platform === 'android' && flags.device && looksLikeAdbHost(flags.device)) {
|
|
1973
|
+
const target = withAdbPort(flags.device);
|
|
1974
|
+
if (!listAndroidDevices(adb).includes(target)) { adbConnect(adb, target); flags.device = target; }
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
let devices = listDevices(platform);
|
|
1897
1978
|
const noneMsg = platform === 'ios'
|
|
1898
1979
|
? '✖ No connected iOS device found. Plug in via USB (unlocked, "Trust") or pair over Wi-Fi.'
|
|
1899
|
-
: '✖ No authorized Android device
|
|
1980
|
+
: '✖ No authorized Android device.\n USB: plug in + accept "Allow USB debugging".\n Wireless: `appwrap dev android --wifi` (flip a USB device to wireless), enable the phone\'s "Wireless debugging" (auto-discovered via mDNS), or `--device <ip[:port]>` (an already-paired device).\n (check with `adb devices`)';
|
|
1900
1981
|
|
|
1901
1982
|
// --device <id|name> — exact (or unambiguous prefix) match against connected devices.
|
|
1902
1983
|
if (flags.device) {
|
|
@@ -1905,6 +1986,22 @@ function resolveDevice(outDir: string, platform: 'ios' | 'android', flags: Recor
|
|
|
1905
1986
|
writeLastDevice(outDir, platform, m.id);
|
|
1906
1987
|
return m;
|
|
1907
1988
|
}
|
|
1989
|
+
|
|
1990
|
+
// Android: nothing attached but a wireless device was remembered → auto-reconnect it (survives sleep /
|
|
1991
|
+
// USB-unplug, so plain `appwrap dev android` keeps working cordless after the first `--wifi`).
|
|
1992
|
+
if (platform === 'android' && !('d' in flags)) {
|
|
1993
|
+
const last = readLastDevice(outDir, 'android');
|
|
1994
|
+
if (last && last.includes(':') && !devices.find((d) => d.id === last) && adbConnect(adb, last)) devices = listDevices(platform);
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
// Android passive discovery (iOS parity): still nothing → pick up any mDNS-advertised wireless device
|
|
1998
|
+
// (tcpip / "Wireless debugging" on) and adb-connect it, so plain `dev android` finds it with no flag —
|
|
1999
|
+
// the same zero-config a network-paired iPhone gets from devicectl.
|
|
2000
|
+
if (platform === 'android' && devices.length === 0 && !flags.device) {
|
|
2001
|
+
const found = androidMdnsTargets(adb).filter((t) => !listAndroidDevices(adb).includes(t) && adbConnect(adb, t));
|
|
2002
|
+
if (found.length) devices = listDevices(platform);
|
|
2003
|
+
}
|
|
2004
|
+
|
|
1908
2005
|
if (devices.length === 0) { console.error(noneMsg); process.exit(1); }
|
|
1909
2006
|
|
|
1910
2007
|
// -d → always prompt. Otherwise prefer the remembered device, then the sole device.
|
|
@@ -1968,6 +2065,86 @@ function androidAdb(): string {
|
|
|
1968
2065
|
return 'adb';
|
|
1969
2066
|
}
|
|
1970
2067
|
|
|
2068
|
+
/** Synchronous sleep — the device-resolution path is all sync execFileSync, so we can't await. */
|
|
2069
|
+
function sleepSync(ms: number): void { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); }
|
|
2070
|
+
|
|
2071
|
+
/** A `--device` value that looks like a network target (ip / hostname[:port]) vs a USB serial — USB
|
|
2072
|
+
* adb serials are bare alphanumerics, never containing a '.' (ip/host) or ':' (host:port). */
|
|
2073
|
+
function looksLikeAdbHost(s: string): boolean { return s.includes('.') || s.includes(':'); }
|
|
2074
|
+
/** Normalize a wireless target to host:port (adb's default tcpip port is 5555). */
|
|
2075
|
+
function withAdbPort(host: string): string { return /:\d+$/.test(host) ? host : `${host}:5555`; }
|
|
2076
|
+
|
|
2077
|
+
/** `adb connect <target>` — true if connected (or already was). Prints the outcome. */
|
|
2078
|
+
function adbConnect(adb: string, target: string): boolean {
|
|
2079
|
+
try {
|
|
2080
|
+
const out = execFileSync(adb, ['connect', target], { encoding: 'utf8' }).trim();
|
|
2081
|
+
const ok = /connected to|already connected/i.test(out);
|
|
2082
|
+
console.log(ok ? `🔗 ${out}` : `⚠ adb connect ${target}: ${out}`);
|
|
2083
|
+
return ok;
|
|
2084
|
+
} catch (e) {
|
|
2085
|
+
console.error(`⚠ adb connect ${target} failed: ${execErrText(e).trim()}`);
|
|
2086
|
+
return false;
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
/** mDNS-discovered wireless adb targets (`ip:port`) — the passive path that matches iOS devicectl's
|
|
2091
|
+
* network listing. The device advertises once tcpip is on (`--wifi`) or Android-11+ "Wireless debugging"
|
|
2092
|
+
* is enabled, so `appwrap dev android` finds it with NO flag (parity with `dev ios` over the network). */
|
|
2093
|
+
function androidMdnsTargets(adb: string): string[] {
|
|
2094
|
+
try {
|
|
2095
|
+
return execFileSync(adb, ['mdns', 'services'], { encoding: 'utf8', timeout: 8000 })
|
|
2096
|
+
.split('\n')
|
|
2097
|
+
.map((l) => l.match(/(\d+\.\d+\.\d+\.\d+:\d+)\s*$/)?.[1])
|
|
2098
|
+
.filter((x): x is string => !!x);
|
|
2099
|
+
} catch { return []; }
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
/** Read a USB-connected device's Wi-Fi (wlan0) IPv4, or null if it isn't on Wi-Fi. */
|
|
2103
|
+
function androidWifiIp(adb: string, serial: string): string | null {
|
|
2104
|
+
for (const args of [
|
|
2105
|
+
['-s', serial, 'shell', 'ip', '-o', 'route', 'get', '1.1.1.1'], // "... src 192.168.1.50"
|
|
2106
|
+
['-s', serial, 'shell', 'ip', '-o', '-f', 'inet', 'addr', 'show', 'wlan0'], // "inet 192.168.1.50/24"
|
|
2107
|
+
]) {
|
|
2108
|
+
try {
|
|
2109
|
+
const m = execFileSync(adb, args, { encoding: 'utf8' }).match(/(?:src|inet)\s+(\d+\.\d+\.\d+\.\d+)/);
|
|
2110
|
+
if (m && !m[1].startsWith('127.')) return m[1];
|
|
2111
|
+
} catch { /* try next */ }
|
|
2112
|
+
}
|
|
2113
|
+
return null;
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
/** `--wifi`: flip a USB-connected device into TCP/IP mode and `adb connect` it over the LAN, so the user
|
|
2117
|
+
* can unplug and keep iterating cordless. If nothing's on USB but a wireless device was remembered, just
|
|
2118
|
+
* reconnect that. Sets `flags.device` to the wireless target (and clears `wifi`) so the rest of the
|
|
2119
|
+
* resolve/deploy path — and any later resolveDevice call — targets it without re-flipping. */
|
|
2120
|
+
function enableWifiAdb(adb: string, outDir: string, flags: Record<string, string>): void {
|
|
2121
|
+
const usb = listAndroidDevices(adb).filter((s) => !s.includes(':')); // USB-attached serials only
|
|
2122
|
+
if (usb.length === 0) {
|
|
2123
|
+
const last = readLastDevice(outDir, 'android');
|
|
2124
|
+
if (last && last.includes(':') && adbConnect(adb, last)) { flags.device = last; delete flags.wifi; return; }
|
|
2125
|
+
console.error('✖ --wifi needs a USB-connected device to flip to wireless (none found). Plug in once + accept "Allow USB debugging", or pass --device <ip[:port]> for an already-paired device.');
|
|
2126
|
+
process.exit(1);
|
|
2127
|
+
}
|
|
2128
|
+
const serial = flags.device && usb.includes(flags.device) ? flags.device : usb[0];
|
|
2129
|
+
if (usb.length > 1 && serial === usb[0] && !(flags.device && usb.includes(flags.device))) {
|
|
2130
|
+
console.log(` (multiple USB devices; flipping ${serial} — pass --device <serial> to choose another)`);
|
|
2131
|
+
}
|
|
2132
|
+
const ip = androidWifiIp(adb, serial);
|
|
2133
|
+
if (!ip) { console.error(`✖ Couldn't read ${serial}'s Wi-Fi IP — is it on Wi-Fi? (try: adb -s ${serial} shell ip route)`); process.exit(1); }
|
|
2134
|
+
console.log(`📶 ${serial}: enabling wireless adb on :5555…`);
|
|
2135
|
+
try { execFileSync(adb, ['-s', serial, 'tcpip', '5555'], { stdio: 'pipe' }); }
|
|
2136
|
+
catch (e) { console.error(`✖ adb tcpip failed: ${execErrText(e).trim()}`); process.exit(1); }
|
|
2137
|
+
const target = `${ip}:5555`;
|
|
2138
|
+
// tcpip restarts adbd on the device — connect with a few retries while it comes back up.
|
|
2139
|
+
let connected = false;
|
|
2140
|
+
for (let i = 0; i < 6 && !connected; i++) { sleepSync(700); connected = adbConnect(adb, target); }
|
|
2141
|
+
if (!connected) { console.error(`✖ Couldn't connect to ${target} after tcpip — same Wi-Fi network? firewall blocking :5555?`); process.exit(1); }
|
|
2142
|
+
writeLastDevice(outDir, 'android', target);
|
|
2143
|
+
console.log(`✓ Wireless adb ready → ${target}. You can unplug USB now.`);
|
|
2144
|
+
flags.device = target;
|
|
2145
|
+
delete flags.wifi;
|
|
2146
|
+
}
|
|
2147
|
+
|
|
1971
2148
|
/** Authorized (`device` state) adb serials. Skips `unauthorized`/`offline`. */
|
|
1972
2149
|
function listAndroidDevices(adb: string): string[] {
|
|
1973
2150
|
try {
|
|
@@ -2408,11 +2585,14 @@ async function main(): Promise<void> {
|
|
|
2408
2585
|
default:
|
|
2409
2586
|
console.log('Usage: appwrap <init|sync|dev|build|deploy|publish|logs> [--config <path>] [--out native]\n' +
|
|
2410
2587
|
' config: appwrap.config.ts (preferred) → .js → appwrap.json\n' +
|
|
2411
|
-
' Device selection (dev/deploy/logs/publish): --device <id|name> | -d (pick from a list) | else last-used / sole device.\n
|
|
2412
|
-
'
|
|
2413
|
-
'
|
|
2414
|
-
' --
|
|
2415
|
-
'
|
|
2588
|
+
' Device selection (dev/deploy/logs/publish): --device <id|name|ip[:port]> | -d (pick from a list) | else last-used / sole device.\n' +
|
|
2589
|
+
' Android wireless: --wifi flips a USB device to wireless adb (unplug + keep going); thereafter the\n' +
|
|
2590
|
+
' device is auto-discovered via mDNS — plain `dev android` finds it with NO flag (iOS parity).\n' +
|
|
2591
|
+
' --device <ip[:port]> `adb connect`s an already-paired one.\n\n' +
|
|
2592
|
+
' dev <ios|android> [--sim] [--detached] [--debug] [--wifi] [--url <devserver>|--port <p>]\n' +
|
|
2593
|
+
' live-dev: ANDROID device → ns run livesync (true on-device HMR) + re-stage on save;\n' +
|
|
2594
|
+
' iOS device → deploy + rebuild/reinstall on save. --sim = ns run/HMR on emulator;\n' +
|
|
2595
|
+
' --url/--port = web HMR from a dev server inside the WebView; --detached = install & exit.\n' +
|
|
2416
2596
|
' deploy <ios|android> [--no-launch] [--no-web-build] [-f] (clean ship-once: build → install → launch → exit)\n' +
|
|
2417
2597
|
' publish <ios|android> [prod] (beta: TestFlight / Play internal. prod: App Store / Play production)\n' +
|
|
2418
2598
|
' build <ios|android> [--release] [--aab] (store artifact only — no install/upload)\n' +
|