@absolutejs/absolute 0.20.0-beta.32 → 0.20.0-beta.33

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/dist/cli/index.js CHANGED
@@ -1290,11 +1290,16 @@ import {
1290
1290
  sep,
1291
1291
  win32
1292
1292
  } from "path";
1293
- var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, DEV_JOURNAL_FORMAT = 1, NATIVE_CACHE_FORMAT = 1, HASH_RADIX = 16, EXECUTABLE_MODE_MASK = 73, NATIVE_PUBLIC_PATH_SEGMENTS = 5, CAPACITOR_PROJECT_DIRECTORY_PATTERN, ANDROID_TIMING_PHASES, androidTimingSummary = (timings) => ANDROID_TIMING_PHASES.map(([phase, label]) => {
1293
+ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, DEV_JOURNAL_FORMAT = 1, NATIVE_CACHE_FORMAT = 1, HASH_RADIX = 16, EXECUTABLE_MODE_MASK = 73, NATIVE_PUBLIC_PATH_SEGMENTS = 5, CAPACITOR_PROJECT_DIRECTORY_PATTERN, ANDROID_TIMING_PHASES, androidTimingSummary = (timings, physicalDevice = false) => ANDROID_TIMING_PHASES.map(([phase, label]) => {
1294
1294
  const duration = timings[phase];
1295
1295
  if (duration === undefined || label === undefined)
1296
1296
  return null;
1297
- return `${label} ${getDurationString(duration)}`;
1297
+ let phaseLabel = label;
1298
+ if (physicalDevice && phase === "booting")
1299
+ phaseLabel = "device selection";
1300
+ if (physicalDevice && phase === "forwarding")
1301
+ phaseLabel = "LAN transport";
1302
+ return `${phaseLabel} ${getDurationString(duration)}`;
1298
1303
  }).filter((value) => value !== null).join(", "), pathExists2 = async (path) => {
1299
1304
  try {
1300
1305
  await access3(path);
@@ -1454,8 +1459,10 @@ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, DEV_JOURNAL_F
1454
1459
  const root = join7(projectRoot, ".absolutejs", "mobile", "dev-session");
1455
1460
  return {
1456
1461
  backup: join7(root, "capacitor.config.backup.json"),
1462
+ caBackup: join7(root, "absolutejs_dev_ca.backup.pem"),
1457
1463
  journal: join7(root, "journal.json"),
1458
1464
  manifestBackup: join7(root, "AndroidManifest.backup.xml"),
1465
+ networkConfigBackup: join7(root, "absolutejs_dev_network_security.backup.xml"),
1459
1466
  root
1460
1467
  };
1461
1468
  }, isInside = (root, path) => {
@@ -1565,19 +1572,31 @@ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, DEV_JOURNAL_F
1565
1572
  const manifestBackupPath = Reflect.get(value, "manifestBackupPath");
1566
1573
  const nativeConfigPath = Reflect.get(value, "nativeConfigPath");
1567
1574
  const nativeManifestPath = Reflect.get(value, "nativeManifestPath");
1575
+ const projectedFiles = Reflect.get(value, "projectedFiles");
1568
1576
  if (typeof backupPath !== "string" || format !== DEV_JOURNAL_FORMAT || typeof nativeConfigPath !== "string") {
1569
1577
  return null;
1570
1578
  }
1571
1579
  if ((manifestBackupPath !== undefined || nativeManifestPath !== undefined) && (typeof manifestBackupPath !== "string" || typeof nativeManifestPath !== "string")) {
1572
1580
  return null;
1573
1581
  }
1582
+ if (projectedFiles !== undefined && (!Array.isArray(projectedFiles) || !projectedFiles.every((file) => isRecord(file) && typeof file.path === "string" && (file.backupPath === undefined || typeof file.backupPath === "string")))) {
1583
+ return null;
1584
+ }
1574
1585
  return {
1575
1586
  backupPath,
1576
1587
  format,
1577
1588
  manifestBackupPath,
1578
1589
  nativeConfigPath,
1579
- nativeManifestPath
1590
+ nativeManifestPath,
1591
+ projectedFiles
1580
1592
  };
1593
+ }, restoreAndroidProjectedFile = async (file) => {
1594
+ if (!file.backupPath || !await pathExists2(file.backupPath)) {
1595
+ await rm(file.path, { force: true });
1596
+ return;
1597
+ }
1598
+ await mkdir(dirname3(file.path), { recursive: true });
1599
+ await copyFile(file.backupPath, file.path);
1581
1600
  }, repairAbsoluteAndroidDevSession = async (projectRoot) => {
1582
1601
  const paths = journalPaths(projectRoot);
1583
1602
  if (!await pathExists2(paths.journal)) {
@@ -1585,7 +1604,7 @@ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, DEV_JOURNAL_F
1585
1604
  return false;
1586
1605
  }
1587
1606
  const journal = await readFile2(paths.journal, "utf8").then((source) => parseJournal(JSON.parse(source))).catch(() => null);
1588
- if (!journal || !isInside(projectRoot, journal.nativeConfigPath) || !isInside(paths.root, journal.backupPath) || journal.nativeManifestPath !== undefined && !isInside(projectRoot, journal.nativeManifestPath) || journal.manifestBackupPath !== undefined && !isInside(paths.root, journal.manifestBackupPath)) {
1607
+ if (!journal || !isInside(projectRoot, journal.nativeConfigPath) || !isInside(paths.root, journal.backupPath) || journal.nativeManifestPath !== undefined && !isInside(projectRoot, journal.nativeManifestPath) || journal.manifestBackupPath !== undefined && !isInside(paths.root, journal.manifestBackupPath) || journal.projectedFiles?.some((file) => !isInside(projectRoot, file.path) || file.backupPath !== undefined && !isInside(paths.root, file.backupPath))) {
1589
1608
  throw new Error(`Refusing unsafe or invalid mobile dev journal at ${paths.journal}.`);
1590
1609
  }
1591
1610
  if (await pathExists2(journal.backupPath)) {
@@ -1596,17 +1615,51 @@ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, DEV_JOURNAL_F
1596
1615
  await mkdir(dirname3(journal.nativeManifestPath), { recursive: true });
1597
1616
  await copyFile(journal.manifestBackupPath, journal.nativeManifestPath);
1598
1617
  }
1618
+ await Promise.all((journal.projectedFiles ?? []).map(restoreAndroidProjectedFile));
1599
1619
  await rm(paths.root, { force: true, recursive: true });
1600
1620
  return true;
1601
- }, androidDevelopmentManifest = (source, cleartext) => {
1621
+ }, setAndroidApplicationAttribute = (source, attribute, value) => {
1622
+ const pattern = new RegExp(`android:${attribute}=["'][^"']*["']`, "u");
1623
+ if (pattern.test(source))
1624
+ return source.replace(pattern, `android:${attribute}="${value}"`);
1625
+ return source.replace("<application", `<application
1626
+ android:${attribute}="${value}"`);
1627
+ }, androidDevelopmentManifest = (source, cleartext, developmentNetworkConfig) => {
1628
+ let projected = source;
1629
+ if (developmentNetworkConfig) {
1630
+ projected = setAndroidApplicationAttribute(projected, "networkSecurityConfig", `@xml/${developmentNetworkConfig}`);
1631
+ }
1602
1632
  if (!cleartext)
1603
- return source;
1633
+ return projected;
1604
1634
  if (/android:usesCleartextTraffic=["'][^"']*["']/u.test(source)) {
1605
- return source.replace(/android:usesCleartextTraffic=["'][^"']*["']/u, 'android:usesCleartextTraffic="true"');
1635
+ return projected.replace(/android:usesCleartextTraffic=["'][^"']*["']/u, 'android:usesCleartextTraffic="true"');
1606
1636
  }
1607
- return source.replace("<application", `<application
1608
- android:usesCleartextTraffic="true"`);
1609
- }, writeDevConfig = async (projectRoot, nativeConfigPath, nativeManifestPath, port, https, entry, embeddedBundle) => {
1637
+ return setAndroidApplicationAttribute(projected, "usesCleartextTraffic", "true");
1638
+ }, withAndroidDebugCertificateAuthority = (source) => {
1639
+ if (source.includes("@raw/absolutejs_dev_ca"))
1640
+ return source;
1641
+ const debugOverrides = /<debug-overrides(?:\s[^>]*)?>([\s\S]*?)<\/debug-overrides>/u;
1642
+ const match = source.match(debugOverrides);
1643
+ if (match) {
1644
+ const [block] = match;
1645
+ const trustAnchors = /<trust-anchors(?:\s[^>]*)?>/u;
1646
+ const projectedBlock = trustAnchors.test(block) ? block.replace(trustAnchors, `$&
1647
+ <certificates src="@raw/absolutejs_dev_ca" />`) : block.replace("</debug-overrides>", ` <trust-anchors>
1648
+ <certificates src="@raw/absolutejs_dev_ca" />
1649
+ </trust-anchors>
1650
+ </debug-overrides>`);
1651
+ return source.replace(block, projectedBlock);
1652
+ }
1653
+ if (!source.includes("</network-security-config>")) {
1654
+ throw new Error("Android Network Security Configuration is invalid.");
1655
+ }
1656
+ return source.replace("</network-security-config>", ` <debug-overrides>
1657
+ <trust-anchors>
1658
+ <certificates src="@raw/absolutejs_dev_ca" />
1659
+ </trust-anchors>
1660
+ </debug-overrides>
1661
+ </network-security-config>`);
1662
+ }, writeDevConfig = async (projectRoot, nativeConfigPath, nativeManifestPath, port, https, entry, embeddedBundle, serverHost, certificateAuthorityPath) => {
1610
1663
  const paths = journalPaths(projectRoot);
1611
1664
  await repairAbsoluteAndroidDevSession(projectRoot);
1612
1665
  const source = await readFile2(nativeConfigPath, "utf8");
@@ -1618,12 +1671,28 @@ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, DEV_JOURNAL_F
1618
1671
  await mkdir(paths.root, { recursive: true });
1619
1672
  await writeFile2(paths.backup, source, { flag: "wx" });
1620
1673
  await writeFile2(paths.manifestBackup, manifestSource, { flag: "wx" });
1674
+ const resourceRoot = join7(dirname3(nativeManifestPath), "res");
1675
+ const caPath = join7(resourceRoot, "raw", "absolutejs_dev_ca.pem");
1676
+ const existingNetworkConfig = manifestSource.match(/android:networkSecurityConfig=["']@xml\/([a-z0-9_]+)["']/u)?.[1];
1677
+ const networkConfigName = existingNetworkConfig ?? "absolutejs_dev_network_security";
1678
+ const networkConfigPath = join7(resourceRoot, "xml", `${networkConfigName}.xml`);
1679
+ const backupProjectedFile = async ([path, backupPath]) => {
1680
+ if (!await pathExists2(path))
1681
+ return { path };
1682
+ await copyFile(path, backupPath);
1683
+ return { backupPath, path };
1684
+ };
1685
+ const projectedFiles = https && certificateAuthorityPath ? await Promise.all([
1686
+ [caPath, paths.caBackup],
1687
+ [networkConfigPath, paths.networkConfigBackup]
1688
+ ].map(backupProjectedFile)) : [];
1621
1689
  const journal = {
1622
1690
  backupPath: paths.backup,
1623
1691
  format: DEV_JOURNAL_FORMAT,
1624
1692
  manifestBackupPath: paths.manifestBackup,
1625
1693
  nativeConfigPath,
1626
- nativeManifestPath
1694
+ nativeManifestPath,
1695
+ projectedFiles
1627
1696
  };
1628
1697
  await writeFile2(paths.journal, `${JSON.stringify(journal, null, "\t")}
1629
1698
  `, {
@@ -1631,7 +1700,7 @@ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, DEV_JOURNAL_F
1631
1700
  });
1632
1701
  if (!embeddedBundle) {
1633
1702
  const currentServer = parsed.server;
1634
- const developmentUrl = new URL(`${https ? "https" : "http"}://localhost:${port}${entry}`);
1703
+ const developmentUrl = new URL(`${https ? "https" : "http"}://${serverHost}:${port}${entry}`);
1635
1704
  developmentUrl.searchParams.set("__absolute_target", "capacitor-android");
1636
1705
  parsed.server = {
1637
1706
  ...typeof currentServer === "object" && currentServer !== null ? currentServer : {},
@@ -1641,7 +1710,19 @@ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, DEV_JOURNAL_F
1641
1710
  }
1642
1711
  await writeFile2(nativeConfigPath, `${JSON.stringify(parsed, null, "\t")}
1643
1712
  `);
1644
- await writeFile2(nativeManifestPath, androidDevelopmentManifest(manifestSource, !https));
1713
+ if (https && certificateAuthorityPath) {
1714
+ await Promise.all([
1715
+ mkdir(dirname3(caPath), { recursive: true }),
1716
+ mkdir(dirname3(networkConfigPath), { recursive: true })
1717
+ ]);
1718
+ await copyFile(certificateAuthorityPath, caPath);
1719
+ const existingNetworkConfigSource = existingNetworkConfig ? await readFile2(networkConfigPath, "utf8") : `<?xml version="1.0" encoding="utf-8"?>
1720
+ <network-security-config>
1721
+ </network-security-config>
1722
+ `;
1723
+ await writeFile2(networkConfigPath, withAndroidDebugCertificateAuthority(existingNetworkConfigSource));
1724
+ }
1725
+ await writeFile2(nativeManifestPath, androidDevelopmentManifest(manifestSource, !https, https && certificateAuthorityPath !== undefined ? networkConfigName : undefined));
1645
1726
  }, parseAdbDevices = (output) => output.split(/\r?\n/).slice(1).map((line) => line.trim().split(/\s+/, 2)).filter((parts) => parts.length === 2 && parts[1] === "device").map(([serial]) => serial), isManagedEmulatorSerial = (serial, adb, capture, env) => {
1646
1727
  if (!serial.startsWith("emulator-"))
1647
1728
  return false;
@@ -1652,6 +1733,11 @@ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, DEV_JOURNAL_F
1652
1733
  if (devices.exitCode !== 0)
1653
1734
  return;
1654
1735
  return parseAdbDevices(devices.stdout).find((serial) => isManagedEmulatorSerial(serial, adb, capture, env));
1736
+ }, requireConnectedAndroidDevice = (adb, serial, capture, env) => {
1737
+ const devices = capture([adb, "devices"], { env });
1738
+ if (devices.exitCode !== 0 || !parseAdbDevices(devices.stdout).includes(serial)) {
1739
+ throw new Error(`Android device ${serial} is not connected and authorized. Confirm the USB debugging prompt, then retry.`);
1740
+ }
1655
1741
  }, completedBootSerial = (project, capture, env) => {
1656
1742
  const serial = managedEmulatorSerial(project.adb, capture, env);
1657
1743
  if (!serial)
@@ -1663,15 +1749,13 @@ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, DEV_JOURNAL_F
1663
1749
  return booted.exitCode === 0 && booted.stdout.trim() === "1" ? serial : undefined;
1664
1750
  }, waitForManagedEmulator = async (project, capture, sleep, env, signal, preferredSerial) => {
1665
1751
  const deadline = Date.now() + ANDROID_BOOT_TIMEOUT_MS;
1666
- let firstSerial = preferredSerial;
1667
1752
  const poll = async () => {
1668
1753
  throwIfAborted(signal);
1669
- const serial = firstSerial ? preferredCompletedBootSerial(project, firstSerial, capture, env) : completedBootSerial(project, capture, env);
1670
- firstSerial = undefined;
1754
+ const serial = preferredSerial ? preferredCompletedBootSerial(project, preferredSerial, capture, env) : completedBootSerial(project, capture, env);
1671
1755
  if (serial)
1672
1756
  return serial;
1673
1757
  if (Date.now() >= deadline) {
1674
- throw new Error(`Android emulator ${ABSOLUTE_ANDROID_AVD_NAME} did not finish booting within ${ANDROID_BOOT_TIMEOUT_MS / ANDROID_BOOT_POLL_MS}s.`);
1758
+ throw new Error(`Android ${preferredSerial ? `target ${preferredSerial}` : `emulator ${ABSOLUTE_ANDROID_AVD_NAME}`} did not finish booting within ${ANDROID_BOOT_TIMEOUT_MS / ANDROID_BOOT_POLL_MS}s.`);
1675
1759
  }
1676
1760
  await sleep(ANDROID_BOOT_POLL_MS);
1677
1761
  return poll();
@@ -1821,6 +1905,9 @@ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, DEV_JOURNAL_F
1821
1905
  await persistInstalledAndroidNativeCache(options, updated);
1822
1906
  return { nativeCacheHit: false, uid: updated?.uid };
1823
1907
  }, startManagedEmulatorIfNeeded = (project, capture, spawn, log, env) => {
1908
+ if (!project.emulator) {
1909
+ throw new Error("Android Emulator is unavailable. Select a physical device or run `absolute mobile doctor android --fix`.");
1910
+ }
1824
1911
  const serial = managedEmulatorSerial(project.adb, capture, env);
1825
1912
  if (serial)
1826
1913
  return { serial, started: false };
@@ -1838,7 +1925,7 @@ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, DEV_JOURNAL_F
1838
1925
  }, logHttpsCertificateRequirement = (https, log) => {
1839
1926
  if (https !== true)
1840
1927
  return;
1841
- log("Android HMR is using HTTPS; the emulator must trust the local development certificate authority.");
1928
+ log("Android HMR is using HTTPS; AbsoluteJS is projecting the local development CA into this debug app only.");
1842
1929
  }, removeAdbReverse = async (project, serial, port, run, env) => {
1843
1930
  if (!serial)
1844
1931
  return;
@@ -1937,13 +2024,19 @@ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, DEV_JOURNAL_F
1937
2024
  const host = detectAbsoluteMobileHost();
1938
2025
  const androidRoot = process.env.ANDROID_HOME ?? process.env.ANDROID_SDK_ROOT ?? absoluteManagedAndroidSdkRoot(host);
1939
2026
  const checks = await inspectAbsoluteMobileToolchain({ androidRoot, host });
1940
- const failed = checks.filter((check) => check.platform === "android" && (check.status === "fail" || check.status === "warn"));
2027
+ const deviceOnlyChecks = new Set([
2028
+ "android.avd",
2029
+ "android.avdmanager",
2030
+ "android.emulator",
2031
+ "android.virtualization"
2032
+ ]);
2033
+ const failed = checks.filter((check) => check.platform === "android" && (check.status === "fail" || check.status === "warn") && (options.target !== "device" || !deviceOnlyChecks.has(check.id)));
1941
2034
  if (failed.length > 0) {
1942
- throw new Error(`Android emulation is not ready: ${failed.map(({ label }) => label).join(", ")}.`);
2035
+ throw new Error(`Android ${options.target === "device" ? "device development" : "emulation"} is not ready: ${failed.map(({ label }) => label).join(", ")}.`);
1943
2036
  }
1944
2037
  const adb = checks.find((check) => check.id === "android.adb")?.path;
1945
2038
  const emulator = checks.find((check) => check.id === "android.emulator")?.path;
1946
- if (!adb || !emulator) {
2039
+ if (!adb || options.target !== "device" && !emulator) {
1947
2040
  throw new Error("Android SDK tools disappeared after readiness checks.");
1948
2041
  }
1949
2042
  const cap = join7(projectRoot, "node_modules", ".bin", host === "windows" ? "cap.cmd" : "cap");
@@ -1974,7 +2067,7 @@ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, DEV_JOURNAL_F
1974
2067
  androidRoot,
1975
2068
  cap,
1976
2069
  config,
1977
- emulator,
2070
+ ...emulator ? { emulator } : {},
1978
2071
  host,
1979
2072
  nativeDirectory,
1980
2073
  projectRoot
@@ -2030,7 +2123,11 @@ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, DEV_JOURNAL_F
2030
2123
  });
2031
2124
  };
2032
2125
  try {
2033
- await writeDevConfig(project.projectRoot, nativeConfigPath, nativeManifestPath, options.port, options.https === true, project.config.entry, options.embeddedBundle === true);
2126
+ const physicalDevice = options.deviceSerial !== undefined;
2127
+ if (options.https && !options.certificateAuthorityPath) {
2128
+ throw new Error("Android HTTPS development requires the AbsoluteJS development CA certificate.");
2129
+ }
2130
+ await writeDevConfig(project.projectRoot, nativeConfigPath, nativeManifestPath, options.port, options.https === true, project.config.entry, options.embeddedBundle === true, options.serverHost ?? "localhost", options.certificateAuthorityPath);
2034
2131
  throwIfAborted(options.signal);
2035
2132
  logHttpsCertificateRequirement(options.https, log);
2036
2133
  const fingerprintStartedAt = performance.now();
@@ -2041,20 +2138,23 @@ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, DEV_JOURNAL_F
2041
2138
  return fingerprint;
2042
2139
  });
2043
2140
  transition("booting");
2044
- const emulator = startManagedEmulatorIfNeeded(project, capture, spawn, log, env);
2141
+ if (options.deviceSerial)
2142
+ requireConnectedAndroidDevice(project.adb, options.deviceSerial, capture, env);
2143
+ const emulator = options.deviceSerial ? { serial: options.deviceSerial, started: false } : startManagedEmulatorIfNeeded(project, capture, spawn, log, env);
2045
2144
  const { started: startedEmulator } = emulator;
2046
2145
  transition("connecting");
2047
2146
  const serial = await waitForManagedEmulator(project, capture, sleep, env, options.signal, emulator.serial);
2048
2147
  connectedSerial = serial;
2049
2148
  transition("forwarding");
2050
- await requireSuccess([
2051
- project.adb,
2052
- "-s",
2053
- serial,
2054
- "reverse",
2055
- `tcp:${options.port}`,
2056
- `tcp:${options.port}`
2057
- ], "ADB reverse port forwarding", run, { env, signal: options.signal });
2149
+ if (!physicalDevice)
2150
+ await requireSuccess([
2151
+ project.adb,
2152
+ "-s",
2153
+ serial,
2154
+ "reverse",
2155
+ `tcp:${options.port}`,
2156
+ `tcp:${options.port}`
2157
+ ], "ADB reverse port forwarding", run, { env, signal: options.signal });
2058
2158
  throwIfAborted(options.signal);
2059
2159
  const nativeFingerprint = await nativeFingerprintPromise;
2060
2160
  throwIfAborted(options.signal);
@@ -2085,8 +2185,8 @@ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, DEV_JOURNAL_F
2085
2185
  nativeLogs = attachAndroidNativeLogs(project, serial, capture, env, options, uid);
2086
2186
  transition("ready");
2087
2187
  phaseDurations.total = performance.now() - startupStartedAt;
2088
- log(options.embeddedBundle === true ? `Android emulator connected (${serial}) with the embedded bundle and backend on port ${options.port} in ${getDurationString(phaseDurations.total)} (${nativeCacheHit ? "native cache hit" : "native build installed"}).` : `Android emulator connected (${serial}) with HMR on port ${options.port} in ${getDurationString(phaseDurations.total)} (${nativeCacheHit ? "native cache hit" : "native build installed"}).`);
2089
- log(`Android startup: ${androidTimingSummary(phaseDurations)}.`);
2188
+ log(options.embeddedBundle === true ? `Android ${physicalDevice ? "device" : "emulator"} connected (${serial}) with the embedded bundle and backend on port ${options.port} in ${getDurationString(phaseDurations.total)} (${nativeCacheHit ? "native cache hit" : "native build installed"}).` : `Android ${physicalDevice ? "device" : "emulator"} connected (${serial}) with HMR on port ${options.port} in ${getDurationString(phaseDurations.total)} (${nativeCacheHit ? "native cache hit" : "native build installed"}).`);
2189
+ log(`Android startup: ${androidTimingSummary(phaseDurations, physicalDevice)}.`);
2090
2190
  let closed = false;
2091
2191
  const close = async () => {
2092
2192
  if (closed)
@@ -2094,7 +2194,8 @@ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, DEV_JOURNAL_F
2094
2194
  closed = true;
2095
2195
  transition("closing");
2096
2196
  await closeNativeLogs();
2097
- await removeAdbReverse(project, serial, options.port, run, env);
2197
+ if (!physicalDevice)
2198
+ await removeAdbReverse(project, serial, options.port, run, env);
2098
2199
  await repairAbsoluteAndroidDevSession(project.projectRoot);
2099
2200
  transition("closed");
2100
2201
  };
@@ -2133,7 +2234,8 @@ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, DEV_JOURNAL_F
2133
2234
  } catch (error) {
2134
2235
  transition("failed");
2135
2236
  await closeNativeLogs();
2136
- await removeAdbReverse(project, connectedSerial, options.port, run, env);
2237
+ if (!options.deviceSerial)
2238
+ await removeAdbReverse(project, connectedSerial, options.port, run, env);
2137
2239
  await repairAbsoluteAndroidDevSession(project.projectRoot);
2138
2240
  throw error;
2139
2241
  }
@@ -2848,6 +2950,21 @@ var ABSOLUTE_IOS_SIMULATOR_NAME = "AbsoluteJS iPhone", BOOT_TIMEOUT_MS = 180000,
2848
2950
  const exitCode = await run(command, options);
2849
2951
  if (exitCode !== 0)
2850
2952
  throw new Error(`${label} failed with status ${exitCode}.`);
2953
+ }, trustIosSimulatorDevelopmentCa = async (options, udid, run, log) => {
2954
+ if (!options.https)
2955
+ return;
2956
+ if (!options.certificateAuthorityPath) {
2957
+ throw new Error("iOS Simulator HTTPS requires the AbsoluteJS development CA certificate.");
2958
+ }
2959
+ await requireSuccess2([
2960
+ options.project.xcrun,
2961
+ "simctl",
2962
+ "keychain",
2963
+ udid,
2964
+ "add-root-cert",
2965
+ options.certificateAuthorityPath
2966
+ ], "iOS Simulator development CA trust", run, { signal: options.signal });
2967
+ log("Installed the AbsoluteJS development CA into this iOS Simulator trust store.");
2851
2968
  }, requireCapturedSuccess = (result, label) => {
2852
2969
  if (result.exitCode !== 0) {
2853
2970
  throw new Error(`${label} failed: ${result.stderr.trim() || result.stdout.trim() || `status ${result.exitCode}`}`);
@@ -3322,6 +3439,7 @@ var ABSOLUTE_IOS_SIMULATOR_NAME = "AbsoluteJS iPhone", BOOT_TIMEOUT_MS = 180000,
3322
3439
  transition("connecting");
3323
3440
  await waitForBootedSimulator(project, device.udid, capture, sleep, options.signal);
3324
3441
  await requireSuccess2([project.xcrun, "simctl", "bootstatus", device.udid, "-b"], "iOS simulator boot readiness", run, { signal: options.signal });
3442
+ await trustIosSimulatorDevelopmentCa(options, device.udid, run, log);
3325
3443
  const fingerprint = await fingerprintPromise;
3326
3444
  transition("checking-native");
3327
3445
  const nativeCacheHit = await ensureIosDebugApp({
@@ -3806,6 +3924,7 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
3806
3924
  await syncProject(options.project);
3807
3925
  const syncDuration = performance.now() - syncStartedAt;
3808
3926
  const encodedConfig = Buffer.from(JSON.stringify(portableMobileConfig(options.project))).toString("base64url");
3927
+ const encodedCertificateAuthority = options.certificateAuthorityPath ? (await readFile6(options.certificateAuthorityPath)).toString("base64url") : undefined;
3809
3928
  const remoteCommand = [
3810
3929
  `cd ${shellQuote(options.project.remoteProjectRoot)}`,
3811
3930
  "&&",
@@ -3816,6 +3935,10 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
3816
3935
  String(options.port),
3817
3936
  "--mobile-config",
3818
3937
  shellQuote(encodedConfig),
3938
+ ...encodedCertificateAuthority ? [
3939
+ "--certificate-authority",
3940
+ shellQuote(encodedCertificateAuthority)
3941
+ ] : [],
3819
3942
  ...options.https ? ["--https"] : []
3820
3943
  ].join(" ");
3821
3944
  const command = [
@@ -3999,9 +4122,11 @@ var init_remoteMacProtocol = __esm(() => {
3999
4122
  var exports_devCert = {};
4000
4123
  __export(exports_devCert, {
4001
4124
  setupMkcert: () => setupMkcert,
4125
+ normalizeDevCertificateHosts: () => normalizeDevCertificateHosts,
4002
4126
  loadDevCert: () => loadDevCert,
4003
4127
  hasMkcert: () => hasMkcert,
4004
4128
  hasCert: () => hasCert,
4129
+ getDevCertificateAuthorityPath: () => getDevCertificateAuthorityPath,
4005
4130
  ensureDevCert: () => ensureDevCert
4006
4131
  });
4007
4132
  import {
@@ -4011,20 +4136,31 @@ import {
4011
4136
  readFileSync as readFileSync7,
4012
4137
  rmSync
4013
4138
  } from "fs";
4139
+ import { X509Certificate } from "crypto";
4140
+ import { isIP } from "net";
4014
4141
  import { platform as platform2 } from "os";
4015
4142
  import { join as join12 } from "path";
4016
- var CERT_DIR, CERT_PATH, KEY_PATH, CERT_VALIDITY_DAYS = 365, devLog = (msg) => console.log(`\x1B[2m${new Date().toLocaleTimeString()}\x1B[0m \x1B[36m[dev]\x1B[0m ${msg}`), devWarn = (msg) => console.log(`\x1B[2m${new Date().toLocaleTimeString()}\x1B[0m \x1B[33m[dev]\x1B[0m \x1B[33m${msg}\x1B[0m`), certFilesExist = () => existsSync4(CERT_PATH) && existsSync4(KEY_PATH), isCertExpired = () => {
4143
+ var CERT_DIR, CERT_PATH, KEY_PATH, CERT_VALIDITY_DAYS = 365, DEFAULT_CERTIFICATE_HOSTS, CERTIFICATE_HOSTNAME_PATTERN, devLog = (msg) => console.log(`\x1B[2m${new Date().toLocaleTimeString()}\x1B[0m \x1B[36m[dev]\x1B[0m ${msg}`), devWarn = (msg) => console.log(`\x1B[2m${new Date().toLocaleTimeString()}\x1B[0m \x1B[33m[dev]\x1B[0m \x1B[33m${msg}\x1B[0m`), certFilesExist = () => existsSync4(CERT_PATH) && existsSync4(KEY_PATH), normalizeDevCertificateHosts = (hosts = []) => {
4144
+ const normalized = new Set(DEFAULT_CERTIFICATE_HOSTS);
4145
+ for (const host of hosts) {
4146
+ const value = host.trim().toLowerCase();
4147
+ if (!value || value === "0.0.0.0" || value === "::")
4148
+ continue;
4149
+ if (isIP(value) === 0 && !CERTIFICATE_HOSTNAME_PATTERN.test(value)) {
4150
+ throw new TypeError(`Invalid development certificate host: ${host}`);
4151
+ }
4152
+ normalized.add(value);
4153
+ }
4154
+ return [...normalized];
4155
+ }, certificateIsUsable = (hosts) => {
4017
4156
  try {
4018
4157
  const certPem = readFileSync7(CERT_PATH, "utf-8");
4019
- const proc = Bun.spawnSync(["openssl", "x509", "-enddate", "-noout"], {
4020
- stdin: new TextEncoder().encode(certPem)
4021
- });
4022
- const output = new TextDecoder().decode(proc.stdout).trim();
4023
- const dateStr = output.replace("notAfter=", "");
4024
- const expiryDate = new Date(dateStr);
4025
- return expiryDate.getTime() < Date.now();
4158
+ const certificate = new X509Certificate(certPem);
4159
+ if (new Date(certificate.validTo).getTime() <= Date.now())
4160
+ return false;
4161
+ return normalizeDevCertificateHosts(hosts).every((host) => isIP(host) ? certificate.checkIP(host) !== undefined : certificate.checkHost(host) !== undefined);
4026
4162
  } catch {
4027
- return true;
4163
+ return false;
4028
4164
  }
4029
4165
  }, hasMkcert = () => {
4030
4166
  try {
@@ -4036,22 +4172,21 @@ var CERT_DIR, CERT_PATH, KEY_PATH, CERT_VALIDITY_DAYS = 365, devLog = (msg) => c
4036
4172
  } catch {
4037
4173
  return false;
4038
4174
  }
4039
- }, generateWithMkcert = () => {
4175
+ }, generateWithMkcert = (hosts = []) => {
4040
4176
  const result = Bun.spawnSync([
4041
4177
  "mkcert",
4042
4178
  "-cert-file",
4043
4179
  CERT_PATH,
4044
4180
  "-key-file",
4045
4181
  KEY_PATH,
4046
- "localhost",
4047
- "127.0.0.1",
4048
- "::1"
4182
+ ...normalizeDevCertificateHosts(hosts)
4049
4183
  ], { stderr: "pipe", stdout: "pipe" });
4050
4184
  if (result.exitCode !== 0) {
4051
4185
  const err = new TextDecoder().decode(result.stderr);
4052
4186
  throw new Error(`mkcert failed: ${err}`);
4053
4187
  }
4054
- }, generateSelfSigned = () => {
4188
+ }, generateSelfSigned = (hosts = []) => {
4189
+ const subjectAlternativeNames = normalizeDevCertificateHosts(hosts).map((host) => `${isIP(host) ? "IP" : "DNS"}:${host}`).join(",");
4055
4190
  const proc = Bun.spawnSync([
4056
4191
  "openssl",
4057
4192
  "req",
@@ -4070,36 +4205,36 @@ var CERT_DIR, CERT_PATH, KEY_PATH, CERT_VALIDITY_DAYS = 365, devLog = (msg) => c
4070
4205
  "-subj",
4071
4206
  "/CN=localhost",
4072
4207
  "-addext",
4073
- "subjectAltName=DNS:localhost,IP:127.0.0.1,IP:::1"
4208
+ `subjectAltName=${subjectAlternativeNames}`
4074
4209
  ], { stderr: "pipe", stdout: "pipe" });
4075
4210
  if (proc.exitCode !== 0) {
4076
4211
  const err = new TextDecoder().decode(proc.stderr);
4077
4212
  throw new Error(`openssl failed: ${err}`);
4078
4213
  }
4079
4214
  devLog("Using self-signed certificate \u2014 browser will show a one-time warning");
4080
- }, generateCert = () => {
4215
+ }, generateCert = (hosts = []) => {
4081
4216
  if (hasMkcert()) {
4082
- generateWithMkcert();
4217
+ generateWithMkcert(hosts);
4083
4218
  } else {
4084
- generateSelfSigned();
4219
+ generateSelfSigned(hosts);
4085
4220
  }
4086
- }, ensureDevCert = () => {
4221
+ }, ensureDevCert = (hosts = []) => {
4087
4222
  mkdirSync4(CERT_DIR, { recursive: true });
4088
- if (hasCert()) {
4223
+ if (hasCert(hosts)) {
4089
4224
  return { cert: CERT_PATH, key: KEY_PATH };
4090
4225
  }
4091
4226
  if (certFilesExist()) {
4092
- devLog("Certificate expired, regenerating...");
4227
+ devLog("Certificate is expired or missing a required host, regenerating...");
4093
4228
  }
4094
4229
  try {
4095
- generateCert();
4230
+ generateCert(hosts);
4096
4231
  } catch (err) {
4097
4232
  devWarn(`Failed to generate certificate: ${err instanceof Error ? err.message : err}`);
4098
4233
  return null;
4099
4234
  }
4100
4235
  return { cert: CERT_PATH, key: KEY_PATH };
4101
- }, hasCert = () => certFilesExist() && !isCertExpired(), loadDevCert = () => {
4102
- const paths = ensureDevCert();
4236
+ }, hasCert = (hosts = []) => certFilesExist() && certificateIsUsable(hosts), loadDevCert = (hosts = []) => {
4237
+ const paths = ensureDevCert(hosts);
4103
4238
  if (!paths)
4104
4239
  return null;
4105
4240
  try {
@@ -4215,7 +4350,15 @@ var CERT_DIR, CERT_PATH, KEY_PATH, CERT_VALIDITY_DAYS = 365, devLog = (msg) => c
4215
4350
  } catch {
4216
4351
  return null;
4217
4352
  }
4218
- }, mkcertCaRoot = () => runCapture(["mkcert", "-CAROOT"]), toWindowsPath = (linuxPath) => runCapture(["wslpath", "-w", linuxPath]), windowsTempDir = () => {
4353
+ }, mkcertCaRoot = () => runCapture(["mkcert", "-CAROOT"]), getDevCertificateAuthorityPath = () => {
4354
+ const caRoot = hasMkcert() ? mkcertCaRoot() : null;
4355
+ const rootCertificate = caRoot ? join12(caRoot, "rootCA.pem") : null;
4356
+ if (rootCertificate && existsSync4(rootCertificate))
4357
+ return rootCertificate;
4358
+ if (certFilesExist())
4359
+ return CERT_PATH;
4360
+ return null;
4361
+ }, toWindowsPath = (linuxPath) => runCapture(["wslpath", "-w", linuxPath]), windowsTempDir = () => {
4219
4362
  const winTemp = runCapture(["cmd.exe", "/c", "echo %TEMP%"]);
4220
4363
  if (!winTemp)
4221
4364
  return null;
@@ -4249,7 +4392,7 @@ var CERT_DIR, CERT_PATH, KEY_PATH, CERT_VALIDITY_DAYS = 365, devLog = (msg) => c
4249
4392
  ], { stderr: "pipe", stdout: "pipe" });
4250
4393
  rmSync(staged, { force: true });
4251
4394
  return result.exitCode === 0;
4252
- }, setupMkcert = () => {
4395
+ }, setupMkcert = (hosts = []) => {
4253
4396
  if (!ensureMkcert())
4254
4397
  return false;
4255
4398
  const installResult = Bun.spawnSync(["mkcert", "-install"], {
@@ -4276,7 +4419,7 @@ var CERT_DIR, CERT_PATH, KEY_PATH, CERT_VALIDITY_DAYS = 365, devLog = (msg) => c
4276
4419
  rmSync(CERT_PATH, { force: true });
4277
4420
  rmSync(KEY_PATH, { force: true });
4278
4421
  mkdirSync4(CERT_DIR, { recursive: true });
4279
- generateWithMkcert();
4422
+ generateWithMkcert(hosts);
4280
4423
  console.log("");
4281
4424
  devLog("mkcert installed \u2014 HTTPS certificates are now locally trusted");
4282
4425
  return true;
@@ -4285,6 +4428,8 @@ var init_devCert = __esm(() => {
4285
4428
  CERT_DIR = join12(process.cwd(), ".absolutejs");
4286
4429
  CERT_PATH = join12(CERT_DIR, "cert.pem");
4287
4430
  KEY_PATH = join12(CERT_DIR, "key.pem");
4431
+ DEFAULT_CERTIFICATE_HOSTS = ["localhost", "127.0.0.1", "::1"];
4432
+ CERTIFICATE_HOSTNAME_PATTERN = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?))*$/u;
4288
4433
  });
4289
4434
 
4290
4435
  // src/cli/scripts/eslintChunked.ts
@@ -17591,7 +17736,7 @@ var DEFAULT_ROUTE_TIMEOUT_MS = 30000, DEFAULT_HMR_TIMEOUT_MS = 30000, routeExpre
17591
17736
 
17592
17737
  // src/mobile/releaseDoctor.ts
17593
17738
  import { access as access8, readFile as readFile15, readdir as readdir4 } from "fs/promises";
17594
- import { extname as extname8, join as join49, relative as relative24 } from "path";
17739
+ import { dirname as dirname29, extname as extname8, join as join49, relative as relative24 } from "path";
17595
17740
  var HMR_ASSET_PATTERN, RELEASE_ASSET_EXTENSIONS, pathExists5 = async (path) => {
17596
17741
  try {
17597
17742
  await access8(path);
@@ -17650,7 +17795,13 @@ var HMR_ASSET_PATTERN, RELEASE_ASSET_EXTENSIONS, pathExists5 = async (path) => {
17650
17795
  return fail5("android.cleartext", "The Android manifest is missing.", manifestPath, "Run `absolute mobile sync android` before release validation.");
17651
17796
  }
17652
17797
  const source = await readFile15(manifestPath, "utf8");
17653
- return /android:usesCleartextTraffic=["']true["']/u.test(source) ? fail5("android.cleartext", "Android explicitly permits cleartext traffic.", manifestPath, 'Remove usesCleartextTraffic="true" from the release manifest.') : pass("android.cleartext", "Android does not explicitly permit cleartext traffic.", manifestPath);
17798
+ const cleartext = /android:usesCleartextTraffic=["']true["']/u.test(source);
17799
+ const networkConfigName = source.match(/android:networkSecurityConfig=["']@xml\/([a-z0-9_]+)["']/u)?.[1];
17800
+ const networkConfigPath = networkConfigName ? join49(dirname29(manifestPath), "res", "xml", `${networkConfigName}.xml`) : undefined;
17801
+ const developmentTrustReference = /android:networkSecurityConfig=["']@xml\/absolutejs_dev_network_security["']/u.test(source);
17802
+ const developmentTrustContents = networkConfigPath ? await readFile15(networkConfigPath, "utf8").then((value) => value.includes("@raw/absolutejs_dev_ca")).catch(() => false) : false;
17803
+ const developmentTrust = developmentTrustReference || developmentTrustContents;
17804
+ return cleartext || developmentTrust ? fail5("android.cleartext", developmentTrust ? "Android still references the AbsoluteJS development certificate authority." : "Android explicitly permits cleartext traffic.", manifestPath, "Run `absolute mobile sync android`; do not ship development transport or trust overrides.") : pass("android.cleartext", "Android does not explicitly permit cleartext traffic.", manifestPath);
17654
17805
  }, hmrAssetsReleaseCheck = async (publicRoot) => {
17655
17806
  const hmrAsset = await findHmrAsset(publicRoot);
17656
17807
  return hmrAsset ? fail5("android.hmr-assets", "A packaged Android asset contains the development HMR client.", hmrAsset, "Rebuild the production mobile bundle and run Capacitor sync again.") : pass("android.hmr-assets", "Packaged Android assets contain no development HMR markers.", publicRoot);
@@ -17809,7 +17960,7 @@ import {
17809
17960
  stat as stat2,
17810
17961
  writeFile as writeFile14
17811
17962
  } from "fs/promises";
17812
- import { dirname as dirname29, isAbsolute as isAbsolute7, join as join50, relative as relative25, resolve as resolve39, sep as sep6 } from "path";
17963
+ import { dirname as dirname30, isAbsolute as isAbsolute7, join as join50, relative as relative25, resolve as resolve39, sep as sep6 } from "path";
17813
17964
  var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord13 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), requireManifest2 = (value) => {
17814
17965
  if (!isRecord13(value) || typeof value.appBuild !== "string" || typeof value.appId !== "string" || typeof value.runtime !== "string") {
17815
17966
  throw new TypeError("Invalid embedded AbsoluteJS mobile manifest.");
@@ -17882,8 +18033,8 @@ var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord13 = (value) => typeof value ==
17882
18033
  }
17883
18034
  return { artifactPath: destination, metadata: existing, releaseRoot };
17884
18035
  }
17885
- await mkdir12(dirname29(releaseRoot), { recursive: true });
17886
- const staging = await mkdtemp5(join50(dirname29(releaseRoot), ".android-stage-"));
18036
+ await mkdir12(dirname30(releaseRoot), { recursive: true });
18037
+ const staging = await mkdtemp5(join50(dirname30(releaseRoot), ".android-stage-"));
17887
18038
  try {
17888
18039
  await copyFile5(artifactPath, join50(staging, artifactName));
17889
18040
  const complete = {
@@ -20678,6 +20829,24 @@ var resolveDevPort = async (requestedPort, options = {}) => {
20678
20829
  ` + `Set \`dev.port\` to a different value in absolute.config.ts (or via the ABSOLUTE_PORT env var), or extend \`dev.portRange\`.`);
20679
20830
  };
20680
20831
 
20832
+ // src/utils/networking.ts
20833
+ import os from "os";
20834
+ var getAllNetworkIPs = () => {
20835
+ const interfaces = os.networkInterfaces();
20836
+ const addresses = Object.values(interfaces).flat().filter((iface) => iface !== undefined);
20837
+ const ipv4Addresses = [];
20838
+ addresses.filter((addr) => !addr.internal && addr.family === "IPv4").forEach((addr) => ipv4Addresses.push(addr.address));
20839
+ return ipv4Addresses;
20840
+ };
20841
+ var getLocalIPAddress = () => {
20842
+ const allIPs = getAllNetworkIPs();
20843
+ if (allIPs.length > 0 && allIPs[0]) {
20844
+ return allIPs[0];
20845
+ }
20846
+ console.warn("No IP address found, falling back to localhost");
20847
+ return "localhost";
20848
+ };
20849
+
20681
20850
  // src/cli/scripts/dev.ts
20682
20851
  init_config();
20683
20852
  init_nativeAuth();
@@ -20982,25 +21151,41 @@ var confirmPrompt = (message, defaultYes = true) => {
20982
21151
  process.stdin.on("data", onData);
20983
21152
  return promise;
20984
21153
  };
20985
- var setupCertWithPrompt = async (ensureDevCert2, setupMkcert2) => {
21154
+ var setupCertWithPrompt = async (ensureDevCert2, setupMkcert2, hosts) => {
20986
21155
  const install = await confirmPrompt("Install mkcert for trusted HTTPS? (no browser warning)");
20987
21156
  if (install) {
20988
- setupMkcert2();
21157
+ setupMkcert2(hosts);
20989
21158
  } else {
20990
- ensureDevCert2();
21159
+ ensureDevCert2(hosts);
20991
21160
  }
20992
21161
  };
20993
- var setupHttpsCert = async () => {
20994
- const { hasCert: hasCert2, hasMkcert: hasMkcert2, ensureDevCert: ensureDevCert2, setupMkcert: setupMkcert2 } = await Promise.resolve().then(() => (init_devCert(), exports_devCert));
20995
- if (hasCert2()) {
20996
- ensureDevCert2();
20997
- return;
21162
+ var setupHttpsCert = async (hosts = []) => {
21163
+ const {
21164
+ getDevCertificateAuthorityPath: getDevCertificateAuthorityPath2,
21165
+ hasCert: hasCert2,
21166
+ hasMkcert: hasMkcert2,
21167
+ ensureDevCert: ensureDevCert2,
21168
+ setupMkcert: setupMkcert2
21169
+ } = await Promise.resolve().then(() => (init_devCert(), exports_devCert));
21170
+ if (hasCert2(hosts)) {
21171
+ ensureDevCert2(hosts);
21172
+ return getDevCertificateAuthorityPath2();
20998
21173
  }
20999
21174
  if (hasMkcert2()) {
21000
- ensureDevCert2();
21001
- return;
21175
+ ensureDevCert2(hosts);
21176
+ return getDevCertificateAuthorityPath2();
21002
21177
  }
21003
- await setupCertWithPrompt(ensureDevCert2, setupMkcert2);
21178
+ await setupCertWithPrompt(ensureDevCert2, setupMkcert2, hosts);
21179
+ return getDevCertificateAuthorityPath2();
21180
+ };
21181
+ var mobileReachableHost = (host) => {
21182
+ if (host !== "0.0.0.0" && host !== "::")
21183
+ return host;
21184
+ const address = getLocalIPAddress();
21185
+ if (address === "localhost") {
21186
+ throw new Error("No LAN address is available for the selected physical device. Connect this computer to the device network or set dev.host explicitly.");
21187
+ }
21188
+ return address;
21004
21189
  };
21005
21190
  var resolveDevConfig = (configDev) => {
21006
21191
  const relay = env.ABSOLUTE_TUNNEL_RELAY ?? configDev?.tunnel?.relay;
@@ -21014,10 +21199,19 @@ var resolveDevConfig = (configDev) => {
21014
21199
  ...relay && token ? { tunnel: { relay, token } } : {}
21015
21200
  };
21016
21201
  };
21017
- var androidToolchainReady = (checks) => checks.every((check) => check.platform !== "android" || check.status !== "fail" && check.status !== "warn");
21202
+ var androidToolchainReady = (checks, target = "emulator") => {
21203
+ const deviceOnlyChecks = new Set([
21204
+ "android.avd",
21205
+ "android.avdmanager",
21206
+ "android.emulator",
21207
+ "android.virtualization"
21208
+ ]);
21209
+ return checks.every((check) => check.platform !== "android" || target === "device" && deviceOnlyChecks.has(check.id) || check.status !== "fail" && check.status !== "warn");
21210
+ };
21018
21211
  var iosToolchainReady = (checks) => checks.every((check) => check.platform !== "ios" || check.status !== "fail" && check.status !== "warn");
21019
21212
  var dev = async (serverEntry, configPath2, options = {}) => {
21020
21213
  let httpsEnabled = false;
21214
+ let devCertificateAuthorityPath = null;
21021
21215
  let resolvedDev;
21022
21216
  let buildDirectory = resolve8(process.cwd(), "build");
21023
21217
  let mobileConfig;
@@ -21031,12 +21225,21 @@ var dev = async (serverEntry, configPath2, options = {}) => {
21031
21225
  if (config?.buildDirectory) {
21032
21226
  buildDirectory = resolve8(process.cwd(), config.buildDirectory);
21033
21227
  }
21034
- if (httpsEnabled)
21035
- await setupHttpsCert();
21036
21228
  } catch {
21037
21229
  resolvedDev = resolveDevConfig(undefined);
21038
21230
  httpsEnabled = resolvedDev.https;
21039
21231
  }
21232
+ if (options.androidDevice && ["localhost", "127.0.0.1", "::1"].includes(resolvedDev.host)) {
21233
+ resolvedDev.host = "0.0.0.0";
21234
+ }
21235
+ if (httpsEnabled)
21236
+ devCertificateAuthorityPath = await setupHttpsCert(options.androidDevice ? [mobileReachableHost(resolvedDev.host)] : [resolvedDev.host]);
21237
+ if (options.androidDevice && !mobileConfig) {
21238
+ throw new TypeError("--android-device requires an absolute.config.ts mobile configuration.");
21239
+ }
21240
+ if (options.androidDevice && mobileConfig?.platforms && !mobileConfig.platforms.includes("android")) {
21241
+ throw new TypeError("--android-device requires android in mobile.platforms.");
21242
+ }
21040
21243
  let androidDevProject = null;
21041
21244
  let iosDevProject = null;
21042
21245
  const mobileInteractive = options.mobile !== false && process.env.ABSOLUTE_NO_MOBILE !== "1" && process.stdin.isTTY === true && process.stdout.isTTY === true;
@@ -21044,14 +21247,15 @@ var dev = async (serverEntry, configPath2, options = {}) => {
21044
21247
  try {
21045
21248
  const normalized = normalizeAbsoluteMobileConfig(mobileConfig, process.cwd());
21046
21249
  if (normalized.platforms.includes("android")) {
21047
- let ready = androidToolchainReady(await inspectAbsoluteMobileToolchain());
21250
+ const androidTarget = options.androidDevice ? "device" : "emulator";
21251
+ let ready = androidToolchainReady(await inspectAbsoluteMobileToolchain(), androidTarget);
21048
21252
  if (!ready) {
21049
- const install = await confirmPrompt("Android emulation is not configured. Install it now?");
21253
+ const install = await confirmPrompt("Android development is not configured. Install the tested toolchain now?");
21050
21254
  if (install) {
21051
21255
  await fixAbsoluteMobileEmulatorToolchain("android");
21052
- ready = androidToolchainReady(await inspectAbsoluteMobileToolchain());
21256
+ ready = androidToolchainReady(await inspectAbsoluteMobileToolchain(), androidTarget);
21053
21257
  } else {
21054
- console.log(cliTag("\x1B[33m", "Mobile emulator skipped. Run `absolute mobile doctor android --fix` when ready."));
21258
+ console.log(cliTag("\x1B[33m", "Android target skipped. Run `absolute mobile doctor android --fix` when ready."));
21055
21259
  }
21056
21260
  }
21057
21261
  if (ready) {
@@ -21063,7 +21267,8 @@ var dev = async (serverEntry, configPath2, options = {}) => {
21063
21267
  if (existsSync5(nativeDirectory) || createNativeProject) {
21064
21268
  androidDevProject = await prepareAbsoluteAndroidDevProject(normalized, {
21065
21269
  createNativeProject,
21066
- projectRoot: process.cwd()
21270
+ projectRoot: process.cwd(),
21271
+ target: androidTarget
21067
21272
  });
21068
21273
  }
21069
21274
  }
@@ -21145,7 +21350,8 @@ var dev = async (serverEntry, configPath2, options = {}) => {
21145
21350
  "dev",
21146
21351
  serverEntry,
21147
21352
  ...configPath2 ? ["--config", configPath2] : [],
21148
- ...options.mobile === false ? ["--no-mobile"] : []
21353
+ ...options.mobile === false ? ["--no-mobile"] : [],
21354
+ ...options.androidDevice ? ["--android-device", options.androidDevice] : []
21149
21355
  ].filter((part) => part.length > 0);
21150
21356
  registerInstance({
21151
21357
  command: relaunchCommand,
@@ -21201,6 +21407,7 @@ var dev = async (serverEntry, configPath2, options = {}) => {
21201
21407
  platform: "android",
21202
21408
  provider: "capacitor",
21203
21409
  startedEmulator: session.startedEmulator,
21410
+ target: options.androidDevice ? "device" : "emulator",
21204
21411
  timings: session.timings
21205
21412
  });
21206
21413
  if (session.timings.building === undefined)
@@ -21215,9 +21422,12 @@ var dev = async (serverEntry, configPath2, options = {}) => {
21215
21422
  });
21216
21423
  };
21217
21424
  const openAndroidDevSession = (androidProject) => startAbsoluteAndroidDevSession({
21425
+ certificateAuthorityPath: devCertificateAuthorityPath ?? undefined,
21426
+ deviceSerial: options.androidDevice,
21218
21427
  https: httpsEnabled,
21219
21428
  port,
21220
21429
  project: androidProject,
21430
+ serverHost: options.androidDevice ? mobileReachableHost(resolvedDev.host) : "localhost",
21221
21431
  signal: androidDevAbort.signal,
21222
21432
  log: (message) => printNativeOutput(cliTag("\x1B[36m", message)),
21223
21433
  nativeLog: (entry) => printNativeOutput(androidLogTag(entry)),
@@ -21328,6 +21538,7 @@ var dev = async (serverEntry, configPath2, options = {}) => {
21328
21538
  };
21329
21539
  const openIosDevSession = (iosProject) => {
21330
21540
  const sessionOptions = {
21541
+ certificateAuthorityPath: devCertificateAuthorityPath ?? undefined,
21331
21542
  https: httpsEnabled,
21332
21543
  port,
21333
21544
  signal: iosDevAbort.signal,
@@ -21488,6 +21699,9 @@ var dev = async (serverEntry, configPath2, options = {}) => {
21488
21699
  return;
21489
21700
  }
21490
21701
  const refreshedDevConfig = resolveDevConfig(cfg?.dev);
21702
+ if (options.androidDevice && ["localhost", "127.0.0.1", "::1"].includes(refreshedDevConfig.host)) {
21703
+ refreshedDevConfig.host = "0.0.0.0";
21704
+ }
21491
21705
  const desiredBuildDir = cfg?.buildDirectory ? resolve8(process.cwd(), cfg.buildDirectory) : resolve8(process.cwd(), "build");
21492
21706
  if (desiredBuildDir !== buildDirectory && desiredBuildDir !== lastBuildDirectoryWarned) {
21493
21707
  lastBuildDirectoryWarned = desiredBuildDir;
@@ -21570,6 +21784,7 @@ var dev = async (serverEntry, configPath2, options = {}) => {
21570
21784
  env: {
21571
21785
  ...process.env,
21572
21786
  ...readDotenvFiles(),
21787
+ ABSOLUTE_HOST: resolvedDev.host,
21573
21788
  ABSOLUTE_INSTANCE_MANAGED: "1",
21574
21789
  ABSOLUTE_PORT: String(port),
21575
21790
  ABSOLUTE_SERVER_ENTRY: resolve8(serverEntry),
@@ -23766,9 +23981,17 @@ var stripNamedArgs = (...flags) => args.filter((_, idx) => flags.every((flag) =>
23766
23981
  if (command === "dev") {
23767
23982
  sendTelemetryEvent("cli:command", { command });
23768
23983
  const configPath2 = parseNamedArg("--config");
23769
- const positionalArgs2 = stripNamedArgs("--config").filter((arg) => arg !== "--no-mobile");
23984
+ const androidDevice = parseNamedArg("--android-device");
23985
+ if (args.includes("--android-device") && !androidDevice) {
23986
+ throw new TypeError("--android-device requires an ADB device serial.");
23987
+ }
23988
+ if (androidDevice && args.includes("--no-mobile")) {
23989
+ throw new TypeError("--android-device cannot be combined with --no-mobile.");
23990
+ }
23991
+ const positionalArgs2 = stripNamedArgs("--config", "--android-device").filter((arg) => arg !== "--no-mobile");
23770
23992
  const serverEntry = positionalArgs2[0] ?? DEFAULT_SERVER_ENTRY;
23771
23993
  await dev(serverEntry, configPath2, {
23994
+ androidDevice,
23772
23995
  mobile: !args.includes("--no-mobile")
23773
23996
  });
23774
23997
  } else if (command === "start") {
@@ -23927,7 +24150,7 @@ if (command === "dev") {
23927
24150
  console.error(message);
23928
24151
  console.error("Usage: absolute <command>");
23929
24152
  console.error("Commands:");
23930
- console.error(" dev [entry] [--no-mobile] Start web and configured mobile development");
24153
+ console.error(" dev [entry] [--no-mobile] [--android-device serial] Start web and configured mobile development");
23931
24154
  console.error(" workspace dev [--no-tui] Start multi-service workspace dev");
23932
24155
  console.error(" build [--outdir dir] [--profile] Build production assets");
23933
24156
  console.error(" prepare [entry] [--outdir dir] Build production assets and server without launching");