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

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
  }
@@ -2742,17 +2844,109 @@ var init_iosRelease = __esm(() => {
2742
2844
  ]);
2743
2845
  });
2744
2846
 
2847
+ // src/mobile/iosPhysicalDeviceTransport.ts
2848
+ import { randomUUID as randomUUID2, X509Certificate } from "crypto";
2849
+ import { createServer as createServer3 } from "http";
2850
+ import {
2851
+ connect as connectTcp,
2852
+ createServer as createTcpServer,
2853
+ isIP
2854
+ } from "net";
2855
+ import { readFile as readFile5 } from "fs/promises";
2856
+ var closeServer = (server) => new Promise((resolve7, reject) => {
2857
+ server.close((error) => {
2858
+ if (error)
2859
+ reject(error);
2860
+ else
2861
+ resolve7();
2862
+ });
2863
+ }), listen = (server, port) => new Promise((resolve7, reject) => {
2864
+ server.once("error", reject);
2865
+ server.listen(port, "0.0.0.0", () => {
2866
+ server.off("error", reject);
2867
+ const address = server.address();
2868
+ if (!address || typeof address === "string") {
2869
+ reject(new Error("Could not determine the iOS device helper port."));
2870
+ return;
2871
+ }
2872
+ resolve7(address.port);
2873
+ });
2874
+ }), findEphemeralPort = async () => {
2875
+ const probe = createTcpServer();
2876
+ const port = await new Promise((resolve7, reject) => {
2877
+ probe.once("error", reject);
2878
+ probe.listen(0, "127.0.0.1", () => {
2879
+ const address = probe.address();
2880
+ if (!address || typeof address === "string") {
2881
+ reject(new Error("Could not allocate the iOS CA enrollment port."));
2882
+ return;
2883
+ }
2884
+ resolve7(address.port);
2885
+ });
2886
+ });
2887
+ await closeServer(probe);
2888
+ return port;
2889
+ }, normalizeAbsoluteIosDeviceHost = (value) => {
2890
+ const normalized = value.trim();
2891
+ if (!normalized || normalized.length > 253 || /[\0\s/?#]/u.test(normalized))
2892
+ throw new TypeError("Physical iOS development requires a valid LAN host.");
2893
+ return normalized;
2894
+ }, normalizeAbsoluteIosDeviceIdentifier = (value) => {
2895
+ const normalized = value.trim();
2896
+ if (!normalized || normalized.length > 256 || /[\0\r\n]/u.test(normalized))
2897
+ throw new TypeError("--ios-device requires a valid Xcode device identifier or name.");
2898
+ return normalized;
2899
+ }, urlForHost = (protocol, host, port) => {
2900
+ const url = new URL(`${protocol}://localhost:${port}`);
2901
+ const normalizedHost = normalizeAbsoluteIosDeviceHost(host);
2902
+ url.hostname = isIP(normalizedHost) === 6 ? `[${normalizedHost}]` : normalizedHost;
2903
+ return url;
2904
+ }, startAbsoluteIosCaEnrollmentServer = async (options) => {
2905
+ const certificate = new X509Certificate(await readFile5(options.certificateAuthorityPath));
2906
+ const certificateBytes = certificate.raw;
2907
+ const token = randomUUID2().replaceAll("-", "");
2908
+ const certificatePath = `/${token}/absolutejs-development-ca.cer`;
2909
+ const server = createServer3((request, response) => {
2910
+ if (request.method !== "GET" || request.url !== certificatePath) {
2911
+ response.writeHead(404, {
2912
+ "Cache-Control": "no-store",
2913
+ "Content-Type": "text/plain; charset=utf-8"
2914
+ });
2915
+ response.end("Not found.");
2916
+ return;
2917
+ }
2918
+ response.writeHead(200, {
2919
+ "Cache-Control": "no-store",
2920
+ "Content-Disposition": 'attachment; filename="absolutejs-development-ca.cer"',
2921
+ "Content-Length": String(certificateBytes.byteLength),
2922
+ "Content-Type": "application/x-x509-ca-cert",
2923
+ "X-Content-Type-Options": "nosniff"
2924
+ });
2925
+ response.end(certificateBytes);
2926
+ });
2927
+ const port = await findEphemeralPort();
2928
+ await listen(server, port);
2929
+ const url = urlForHost("http", options.displayHost, port);
2930
+ url.pathname = certificatePath;
2931
+ return {
2932
+ url: url.href,
2933
+ close: () => closeServer(server)
2934
+ };
2935
+ };
2936
+ var init_iosPhysicalDeviceTransport = () => {};
2937
+
2745
2938
  // src/mobile/iosSimulatorController.ts
2746
- import { createHash as createHash4, randomUUID as randomUUID2 } from "crypto";
2939
+ import { createHash as createHash4, randomUUID as randomUUID3 } from "crypto";
2747
2940
  import {
2748
2941
  access as access5,
2749
2942
  copyFile as copyFile3,
2750
2943
  mkdir as mkdir4,
2751
- readFile as readFile5,
2944
+ readFile as readFile6,
2752
2945
  rename as rename4,
2753
2946
  rm as rm4,
2754
2947
  writeFile as writeFile4
2755
2948
  } from "fs/promises";
2949
+ import { isIP as isIP2 } from "net";
2756
2950
  import { dirname as dirname5, isAbsolute as isAbsolute3, join as join10, relative as relative4, resolve as resolve7, sep as sep3 } from "path";
2757
2951
  var ABSOLUTE_IOS_SIMULATOR_NAME = "AbsoluteJS iPhone", BOOT_TIMEOUT_MS = 180000, BOOT_POLL_MS = 1000, DEV_JOURNAL_FORMAT2 = 1, NATIVE_CACHE_FORMAT2 = 1, isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), pathExists4 = async (path) => {
2758
2952
  try {
@@ -2848,7 +3042,39 @@ var ABSOLUTE_IOS_SIMULATOR_NAME = "AbsoluteJS iPhone", BOOT_TIMEOUT_MS = 180000,
2848
3042
  const exitCode = await run(command, options);
2849
3043
  if (exitCode !== 0)
2850
3044
  throw new Error(`${label} failed with status ${exitCode}.`);
2851
- }, requireCapturedSuccess = (result, label) => {
3045
+ }, trustIosSimulatorDevelopmentCa = async (options, udid, run, log) => {
3046
+ if (!options.https)
3047
+ return;
3048
+ if (!options.certificateAuthorityPath) {
3049
+ throw new Error("iOS Simulator HTTPS requires the AbsoluteJS development CA certificate.");
3050
+ }
3051
+ await requireSuccess2([
3052
+ options.project.xcrun,
3053
+ "simctl",
3054
+ "keychain",
3055
+ udid,
3056
+ "add-root-cert",
3057
+ options.certificateAuthorityPath
3058
+ ], "iOS Simulator development CA trust", run, { signal: options.signal });
3059
+ log("Installed the AbsoluteJS development CA into this iOS Simulator trust store.");
3060
+ }, iosLaunchCommand = (project, udid, physical) => physical ? [
3061
+ project.xcrun,
3062
+ "devicectl",
3063
+ "device",
3064
+ "process",
3065
+ "launch",
3066
+ "--terminate-existing",
3067
+ "--device",
3068
+ udid,
3069
+ project.config.appId
3070
+ ] : [
3071
+ project.xcrun,
3072
+ "simctl",
3073
+ "launch",
3074
+ "--terminate-running-process",
3075
+ udid,
3076
+ project.config.appId
3077
+ ], requireCapturedSuccess = (result, label) => {
2852
3078
  if (result.exitCode !== 0) {
2853
3079
  throw new Error(`${label} failed: ${result.stderr.trim() || result.stdout.trim() || `status ${result.exitCode}`}`);
2854
3080
  }
@@ -2971,7 +3197,7 @@ var ABSOLUTE_IOS_SIMULATOR_NAME = "AbsoluteJS iPhone", BOOT_TIMEOUT_MS = 180000,
2971
3197
  await rm4(paths.root, { force: true, recursive: true });
2972
3198
  return false;
2973
3199
  }
2974
- const journal = await readFile5(paths.journal, "utf8").then((source) => parseJournal2(JSON.parse(source))).catch(() => null);
3200
+ const journal = await readFile6(paths.journal, "utf8").then((source) => parseJournal2(JSON.parse(source))).catch(() => null);
2975
3201
  if (!journal || !isInside2(projectRoot, journal.nativeConfigPath) || !isInside2(projectRoot, journal.infoPath) || !isInside2(paths.root, journal.configBackupPath) || !isInside2(paths.root, journal.infoBackupPath)) {
2976
3202
  throw new Error(`Refusing unsafe or invalid iOS dev journal at ${paths.journal}.`);
2977
3203
  }
@@ -3000,14 +3226,14 @@ var ABSOLUTE_IOS_SIMULATOR_NAME = "AbsoluteJS iPhone", BOOT_TIMEOUT_MS = 180000,
3000
3226
  <key>NSAllowsArbitraryLoads</key>
3001
3227
  <true/>
3002
3228
  </dict>`);
3003
- }, writeDevProjection = async (project, port, https) => {
3229
+ }, writeDevProjection = async (project, port, https, serverHost = "localhost") => {
3004
3230
  const paths = journalPaths2(project.projectRoot);
3005
3231
  await repairAbsoluteIosDevSession(project.projectRoot);
3006
3232
  const nativeConfigPath = join10(project.nativeDirectory, "App", "App", "capacitor.config.json");
3007
3233
  const infoPath = join10(project.nativeDirectory, "App", "App", "Info.plist");
3008
3234
  const [configSource, infoSource] = await Promise.all([
3009
- readFile5(nativeConfigPath, "utf8"),
3010
- readFile5(infoPath, "utf8")
3235
+ readFile6(nativeConfigPath, "utf8"),
3236
+ readFile6(infoPath, "utf8")
3011
3237
  ]);
3012
3238
  const parsed = JSON.parse(configSource);
3013
3239
  if (!isRecord3(parsed))
@@ -3029,6 +3255,7 @@ var ABSOLUTE_IOS_SIMULATOR_NAME = "AbsoluteJS iPhone", BOOT_TIMEOUT_MS = 180000,
3029
3255
  flag: "wx"
3030
3256
  });
3031
3257
  const developmentUrl = new URL(`${https ? "https" : "http"}://localhost:${port}${project.config.entry}`);
3258
+ developmentUrl.hostname = isIP2(serverHost) === 6 ? `[${serverHost}]` : serverHost;
3032
3259
  developmentUrl.searchParams.set("__absolute_target", "capacitor-ios");
3033
3260
  const existingServer = parsed.server;
3034
3261
  parsed.server = {
@@ -3056,9 +3283,9 @@ var ABSOLUTE_IOS_SIMULATOR_NAME = "AbsoluteJS iPhone", BOOT_TIMEOUT_MS = 180000,
3056
3283
  String(identity)
3057
3284
  ]))
3058
3285
  };
3059
- }, readNativeCache2 = (projectRoot) => readFile5(nativeCachePath2(projectRoot), "utf8").then((source) => parseNativeCache2(JSON.parse(source))).catch(() => null), writeNativeCache2 = async (projectRoot, cache) => {
3286
+ }, readNativeCache2 = (projectRoot) => readFile6(nativeCachePath2(projectRoot), "utf8").then((source) => parseNativeCache2(JSON.parse(source))).catch(() => null), writeNativeCache2 = async (projectRoot, cache) => {
3060
3287
  const destination = nativeCachePath2(projectRoot);
3061
- const temporary = `${destination}.${process.pid}.${randomUUID2()}.tmp`;
3288
+ const temporary = `${destination}.${process.pid}.${randomUUID3()}.tmp`;
3062
3289
  await mkdir4(dirname5(destination), { recursive: true });
3063
3290
  try {
3064
3291
  await writeFile4(temporary, `${JSON.stringify(cache, null, "\t")}
@@ -3140,6 +3367,29 @@ var ABSOLUTE_IOS_SIMULATOR_NAME = "AbsoluteJS iPhone", BOOT_TIMEOUT_MS = 180000,
3140
3367
  "app"
3141
3368
  ]);
3142
3369
  return result.exitCode === 0 && result.stdout.trim() ? result.stdout.trim() : undefined;
3370
+ }, validatePhysicalIosDevice = (project, identifier, capture) => {
3371
+ const result = capture([
3372
+ project.xcrun,
3373
+ "devicectl",
3374
+ "device",
3375
+ "info",
3376
+ "details",
3377
+ "--device",
3378
+ identifier
3379
+ ]);
3380
+ if (result.exitCode !== 0)
3381
+ throw new Error(`Physical iOS device ${JSON.stringify(identifier)} is unavailable: ${result.stderr.trim() || result.stdout.trim() || "pair it in Xcode Device Hub, trust this Mac, unlock it, and enable Developer Mode."}`);
3382
+ }, physicalIosAppIsInstalled = (project, identifier, capture) => {
3383
+ const result = capture([
3384
+ project.xcrun,
3385
+ "devicectl",
3386
+ "device",
3387
+ "info",
3388
+ "apps",
3389
+ "--device",
3390
+ identifier
3391
+ ]);
3392
+ return result.exitCode === 0 && result.stdout.includes(project.config.appId);
3143
3393
  }, buildIosDebugApp = async (project, udid, fingerprint, run, signal) => {
3144
3394
  const derivedDataPath = join10(project.projectRoot, ".absolutejs", "mobile", "ios-derived-data", createHash4("sha256").update(project.config.appId).digest("hex").slice(0, 16));
3145
3395
  await mkdir4(derivedDataPath, { recursive: true });
@@ -3161,6 +3411,59 @@ var ABSOLUTE_IOS_SIMULATOR_NAME = "AbsoluteJS iPhone", BOOT_TIMEOUT_MS = 180000,
3161
3411
  if (!await pathExists4(appPath))
3162
3412
  throw new Error(`Xcode did not produce the simulator app at ${appPath}.`);
3163
3413
  return appPath;
3414
+ }, buildPhysicalIosDebugApp = async (project, identifier, run, signal) => {
3415
+ const derivedDataPath = join10(project.projectRoot, ".absolutejs", "mobile", "ios-derived-data", createHash4("sha256").update(project.config.appId).digest("hex").slice(0, 16));
3416
+ await mkdir4(derivedDataPath, { recursive: true });
3417
+ await requireSuccess2([
3418
+ project.xcodebuild,
3419
+ "-workspace",
3420
+ join10(project.nativeDirectory, "App", "App.xcworkspace"),
3421
+ "-scheme",
3422
+ "App",
3423
+ "-configuration",
3424
+ "Debug",
3425
+ "-destination",
3426
+ `platform=iOS,id=${identifier}`,
3427
+ "-derivedDataPath",
3428
+ derivedDataPath,
3429
+ "-allowProvisioningUpdates",
3430
+ "build"
3431
+ ], "iOS physical-device build (configure automatic signing and a Development Team in Xcode if this is the first run)", run, { cwd: project.nativeDirectory, signal });
3432
+ const appPath = join10(derivedDataPath, "Build", "Products", "Debug-iphoneos", "App.app");
3433
+ if (!await pathExists4(appPath))
3434
+ throw new Error(`Xcode did not produce the physical-device app at ${appPath}.`);
3435
+ return appPath;
3436
+ }, ensurePhysicalIosDebugApp = async (options) => {
3437
+ const cacheHit = options.cache?.appId === options.project.config.appId && options.cache.fingerprint === options.fingerprint && options.cache.installations[options.identifier] === options.fingerprint && physicalIosAppIsInstalled(options.project, options.identifier, options.capture);
3438
+ if (cacheHit) {
3439
+ options.log(`iOS native app is unchanged on the selected physical device; skipped Xcode build and install.`);
3440
+ return true;
3441
+ }
3442
+ options.log("iOS native inputs changed or the physical-device install is stale; rebuilding.");
3443
+ options.transition("building");
3444
+ const appPath = await buildPhysicalIosDebugApp(options.project, options.identifier, options.run, options.signal);
3445
+ throwIfAborted2(options.signal);
3446
+ options.transition("installing");
3447
+ await requireSuccess2([
3448
+ options.project.xcrun,
3449
+ "devicectl",
3450
+ "device",
3451
+ "install",
3452
+ "app",
3453
+ "--device",
3454
+ options.identifier,
3455
+ appPath
3456
+ ], "iOS physical-device app installation", options.run, { signal: options.signal });
3457
+ await writeNativeCache2(options.project.projectRoot, {
3458
+ appId: options.project.config.appId,
3459
+ fingerprint: options.fingerprint,
3460
+ format: NATIVE_CACHE_FORMAT2,
3461
+ installations: {
3462
+ ...options.cache?.appId === options.project.config.appId ? options.cache.installations : {},
3463
+ [options.identifier]: options.fingerprint
3464
+ }
3465
+ }).catch((error) => options.log(`iOS native cache could not be saved: ${error instanceof Error ? error.message : String(error)}`));
3466
+ return false;
3164
3467
  }, ensureIosDebugApp = async (options) => {
3165
3468
  const installed = installedAppIdentity(options.project, options.udid, options.capture);
3166
3469
  const cacheHit = options.cache?.appId === options.project.config.appId && options.cache.fingerprint === options.fingerprint && installed !== undefined && options.cache.installations[options.udid] === installed;
@@ -3200,7 +3503,18 @@ var ABSOLUTE_IOS_SIMULATOR_NAME = "AbsoluteJS iPhone", BOOT_TIMEOUT_MS = 180000,
3200
3503
  if (!options.nativeLog)
3201
3504
  return null;
3202
3505
  const start = options.startNativeLogs ?? defaultStartNativeLogs;
3203
- return start([
3506
+ const command = options.deviceIdentifier ? [
3507
+ project.xcrun,
3508
+ "devicectl",
3509
+ "device",
3510
+ "process",
3511
+ "launch",
3512
+ "--console",
3513
+ "--terminate-existing",
3514
+ "--device",
3515
+ udid,
3516
+ project.config.appId
3517
+ ] : [
3204
3518
  project.xcrun,
3205
3519
  "simctl",
3206
3520
  "spawn",
@@ -3213,7 +3527,8 @@ var ABSOLUTE_IOS_SIMULATOR_NAME = "AbsoluteJS iPhone", BOOT_TIMEOUT_MS = 180000,
3213
3527
  "debug",
3214
3528
  "--predicate",
3215
3529
  'process == "App"'
3216
- ], { signal: options.signal }, (line) => {
3530
+ ];
3531
+ return start(command, { signal: options.signal }, (line) => {
3217
3532
  const entry = parseAbsoluteIosLogLine(line);
3218
3533
  if (entry)
3219
3534
  options.nativeLog?.(entry);
@@ -3223,12 +3538,13 @@ var ABSOLUTE_IOS_SIMULATOR_NAME = "AbsoluteJS iPhone", BOOT_TIMEOUT_MS = 180000,
3223
3538
  return duration === undefined ? null : `${label} ${getDurationString(duration)}`;
3224
3539
  }).filter((value) => value !== null).join(", "), prepareAbsoluteIosDevProject = async (config, options) => {
3225
3540
  if (detectAbsoluteMobileHost() !== "macos")
3226
- throw new Error("iOS simulation requires macOS and Xcode.");
3541
+ throw new Error("iOS development requires macOS and Xcode.");
3542
+ const target = options.target ?? "simulator";
3227
3543
  const projectRoot = resolve7(options.projectRoot);
3228
3544
  const checks = await inspectAbsoluteMobileToolchain({ host: "macos" });
3229
- const failed = checks.filter((check) => check.platform === "ios" && (check.status === "fail" || check.status === "warn"));
3545
+ const failed = checks.filter((check) => check.platform === "ios" && !(target === "device" && check.id === "ios.runtime") && (check.status === "fail" || check.status === "warn"));
3230
3546
  if (failed.length > 0)
3231
- throw new Error(`iOS simulation is not ready: ${failed.map(({ label }) => label).join(", ")}.`);
3547
+ throw new Error(`iOS ${target} development is not ready: ${failed.map(({ label }) => label).join(", ")}.`);
3232
3548
  const xcrun = checks.find((check) => check.id === "ios.xcrun")?.path;
3233
3549
  const xcodebuild = checks.find((check) => check.id === "ios.xcodebuild")?.path;
3234
3550
  if (!xcrun || !xcodebuild)
@@ -3258,8 +3574,53 @@ var ABSOLUTE_IOS_SIMULATOR_NAME = "AbsoluteJS iPhone", BOOT_TIMEOUT_MS = 180000,
3258
3574
  xcodebuild,
3259
3575
  xcrun
3260
3576
  };
3577
+ }, preparePhysicalIosTarget = async (options) => {
3578
+ options.transition("connecting");
3579
+ validatePhysicalIosDevice(options.project, options.deviceIdentifier, options.capture);
3580
+ if (!options.startOptions.https)
3581
+ return {
3582
+ caEnrollmentServer: null,
3583
+ startedSimulator: false,
3584
+ udid: options.deviceIdentifier
3585
+ };
3586
+ if (!options.startOptions.certificateAuthorityPath)
3587
+ throw new Error("Physical iOS HTTPS development requires the AbsoluteJS development CA certificate.");
3588
+ options.transition("enrolling-trust");
3589
+ const startEnrollment = options.startOptions.startCaEnrollmentServer ?? startAbsoluteIosCaEnrollmentServer;
3590
+ const caEnrollmentServer = await startEnrollment({
3591
+ certificateAuthorityPath: options.startOptions.certificateAuthorityPath,
3592
+ displayHost: options.serverHost
3593
+ });
3594
+ options.log(`On the iOS device, open ${caEnrollmentServer.url}, install the AbsoluteJS development CA profile, then enable it under Settings > General > About > Certificate Trust Settings. This public CA endpoint exists only for this dev session.`);
3595
+ return {
3596
+ caEnrollmentServer,
3597
+ startedSimulator: false,
3598
+ udid: options.deviceIdentifier
3599
+ };
3600
+ }, prepareIosSimulatorTarget = async (options) => {
3601
+ options.transition("booting");
3602
+ const managed = await ensureManagedSimulator(options.project, options.capture);
3603
+ const { device } = managed;
3604
+ const startedSimulator = managed.created || device.state !== "Booted";
3605
+ bootSimulator(options.project, device, options.capture);
3606
+ options.spawn([
3607
+ "open",
3608
+ "-a",
3609
+ "Simulator",
3610
+ "--args",
3611
+ "-CurrentDeviceUDID",
3612
+ device.udid
3613
+ ]);
3614
+ options.transition("connecting");
3615
+ await waitForBootedSimulator(options.project, device.udid, options.capture, options.sleep, options.startOptions.signal);
3616
+ await requireSuccess2([options.project.xcrun, "simctl", "bootstatus", device.udid, "-b"], "iOS simulator boot readiness", options.run, { signal: options.startOptions.signal });
3617
+ await trustIosSimulatorDevelopmentCa(options.startOptions, device.udid, options.run, options.log);
3618
+ return { caEnrollmentServer: null, startedSimulator, udid: device.udid };
3261
3619
  }, startAbsoluteIosDevSession = async (options) => {
3262
3620
  const { project } = options;
3621
+ const deviceIdentifier = options.deviceIdentifier ? normalizeAbsoluteIosDeviceIdentifier(options.deviceIdentifier) : undefined;
3622
+ const targetKind = deviceIdentifier ? "device" : "simulator";
3623
+ const serverHost = deviceIdentifier ? normalizeAbsoluteIosDeviceHost(options.serverHost ?? "") : "localhost";
3263
3624
  const capture = options.capture ?? defaultCapture3;
3264
3625
  const run = options.run ?? defaultRun3;
3265
3626
  const sleep = options.sleep ?? Bun.sleep;
@@ -3287,6 +3648,7 @@ var ABSOLUTE_IOS_SIMULATOR_NAME = "AbsoluteJS iPhone", BOOT_TIMEOUT_MS = 180000,
3287
3648
  options.onStateChange?.(next);
3288
3649
  };
3289
3650
  let nativeLogs = null;
3651
+ let caEnrollmentServer = null;
3290
3652
  const closeLogs = async () => {
3291
3653
  const stream = nativeLogs;
3292
3654
  nativeLogs = null;
@@ -3294,38 +3656,66 @@ var ABSOLUTE_IOS_SIMULATOR_NAME = "AbsoluteJS iPhone", BOOT_TIMEOUT_MS = 180000,
3294
3656
  return;
3295
3657
  });
3296
3658
  };
3659
+ const relaunchTarget = async (udid) => {
3660
+ if (deviceIdentifier && options.nativeLog) {
3661
+ await closeLogs();
3662
+ nativeLogs = attachNativeLogs(project, udid, options);
3663
+ return;
3664
+ }
3665
+ await requireSuccess2(iosLaunchCommand(project, udid, deviceIdentifier !== undefined), "iOS app relaunch", run, { signal: options.signal });
3666
+ };
3297
3667
  try {
3298
3668
  await repairAbsoluteIosDevSession(project.projectRoot);
3299
3669
  throwIfAborted2(options.signal);
3300
3670
  transition("syncing");
3301
3671
  await requireSuccess2([project.cap, "sync", "ios"], "Capacitor iOS synchronization", run, { cwd: project.projectRoot, signal: options.signal });
3302
3672
  transition("configuring");
3303
- await writeDevProjection(project, options.port, options.https === true);
3673
+ await writeDevProjection(project, options.port, options.https === true, serverHost);
3304
3674
  throwIfAborted2(options.signal);
3305
3675
  const fingerprintStartedAt = performance.now();
3306
3676
  const fingerprintPromise = fingerprintAbsoluteIosDevProject(project).then((fingerprint2) => {
3307
3677
  timings.fingerprinting = performance.now() - fingerprintStartedAt;
3308
3678
  return fingerprint2;
3309
3679
  });
3310
- transition("booting");
3311
- const { created, device } = await ensureManagedSimulator(project, capture);
3312
- const startedSimulator = created || device.state !== "Booted";
3313
- bootSimulator(project, device, capture);
3314
- spawn([
3315
- "open",
3316
- "-a",
3317
- "Simulator",
3318
- "--args",
3319
- "-CurrentDeviceUDID",
3320
- device.udid
3321
- ]);
3322
- transition("connecting");
3323
- await waitForBootedSimulator(project, device.udid, capture, sleep, options.signal);
3324
- await requireSuccess2([project.xcrun, "simctl", "bootstatus", device.udid, "-b"], "iOS simulator boot readiness", run, { signal: options.signal });
3680
+ const target = deviceIdentifier ? await preparePhysicalIosTarget({
3681
+ capture,
3682
+ deviceIdentifier,
3683
+ log,
3684
+ project,
3685
+ serverHost,
3686
+ startOptions: options,
3687
+ transition
3688
+ }) : await prepareIosSimulatorTarget({
3689
+ capture,
3690
+ log,
3691
+ project,
3692
+ run,
3693
+ sleep,
3694
+ spawn,
3695
+ startOptions: options,
3696
+ transition
3697
+ });
3698
+ const {
3699
+ caEnrollmentServer: targetCaEnrollmentServer,
3700
+ startedSimulator,
3701
+ udid
3702
+ } = target;
3703
+ caEnrollmentServer = targetCaEnrollmentServer;
3325
3704
  const fingerprint = await fingerprintPromise;
3326
3705
  transition("checking-native");
3327
- const nativeCacheHit = await ensureIosDebugApp({
3328
- cache: await readNativeCache2(project.projectRoot),
3706
+ const cache = await readNativeCache2(project.projectRoot);
3707
+ const nativeCacheHit = deviceIdentifier ? await ensurePhysicalIosDebugApp({
3708
+ cache,
3709
+ capture,
3710
+ fingerprint,
3711
+ identifier: deviceIdentifier,
3712
+ log,
3713
+ project,
3714
+ run,
3715
+ signal: options.signal,
3716
+ transition
3717
+ }) : await ensureIosDebugApp({
3718
+ cache,
3329
3719
  capture,
3330
3720
  fingerprint,
3331
3721
  log,
@@ -3333,24 +3723,18 @@ var ABSOLUTE_IOS_SIMULATOR_NAME = "AbsoluteJS iPhone", BOOT_TIMEOUT_MS = 180000,
3333
3723
  run,
3334
3724
  signal: options.signal,
3335
3725
  transition,
3336
- udid: device.udid
3726
+ udid
3337
3727
  });
3338
3728
  throwIfAborted2(options.signal);
3339
3729
  if (options.nativeLog)
3340
3730
  transition("streaming-logs");
3341
- nativeLogs = attachNativeLogs(project, device.udid, options);
3731
+ nativeLogs = attachNativeLogs(project, udid, options);
3342
3732
  transition("launching");
3343
- await requireSuccess2([
3344
- project.xcrun,
3345
- "simctl",
3346
- "launch",
3347
- "--terminate-running-process",
3348
- device.udid,
3349
- project.config.appId
3350
- ], "iOS app launch", run, { signal: options.signal });
3733
+ if (!deviceIdentifier || !nativeLogs)
3734
+ await requireSuccess2(iosLaunchCommand(project, udid, deviceIdentifier !== undefined), "iOS app launch", run, { signal: options.signal });
3351
3735
  transition("ready");
3352
3736
  timings.total = performance.now() - startedAt;
3353
- log(`iOS simulator connected (${device.udid}) with HMR on port ${options.port} in ${getDurationString(timings.total)} (${nativeCacheHit ? "native cache hit" : "native build installed"}).`);
3737
+ log(`iOS ${targetKind} connected with HMR on port ${options.port} in ${getDurationString(timings.total)} (${nativeCacheHit ? "native cache hit" : "native build installed"}).`);
3354
3738
  log(`iOS startup: ${timingSummary(timings)}.`);
3355
3739
  let closed = false;
3356
3740
  const close = async () => {
@@ -3359,6 +3743,10 @@ var ABSOLUTE_IOS_SIMULATOR_NAME = "AbsoluteJS iPhone", BOOT_TIMEOUT_MS = 180000,
3359
3743
  closed = true;
3360
3744
  transition("closing");
3361
3745
  await closeLogs();
3746
+ await caEnrollmentServer?.close().catch(() => {
3747
+ return;
3748
+ });
3749
+ caEnrollmentServer = null;
3362
3750
  await repairAbsoluteIosDevSession(project.projectRoot);
3363
3751
  transition("closed");
3364
3752
  };
@@ -3366,8 +3754,9 @@ var ABSOLUTE_IOS_SIMULATOR_NAME = "AbsoluteJS iPhone", BOOT_TIMEOUT_MS = 180000,
3366
3754
  close,
3367
3755
  nativeCacheHit,
3368
3756
  startedSimulator,
3757
+ targetKind,
3369
3758
  timings: { ...timings },
3370
- udid: device.udid,
3759
+ udid,
3371
3760
  rebuild: async () => {
3372
3761
  if (closed)
3373
3762
  throw new Error("iOS development session is closed.");
@@ -3380,22 +3769,17 @@ var ABSOLUTE_IOS_SIMULATOR_NAME = "AbsoluteJS iPhone", BOOT_TIMEOUT_MS = 180000,
3380
3769
  throw new Error("iOS development session is closed.");
3381
3770
  transition("launching");
3382
3771
  try {
3383
- await requireSuccess2([
3384
- project.xcrun,
3385
- "simctl",
3386
- "launch",
3387
- "--terminate-running-process",
3388
- device.udid,
3389
- project.config.appId
3390
- ], "iOS app relaunch", run, { signal: options.signal });
3772
+ await relaunchTarget(udid);
3391
3773
  transition("ready");
3392
- log(`iOS app relaunched on ${device.udid}.`);
3774
+ log(`iOS app relaunched on the selected ${targetKind}.`);
3393
3775
  } catch (error) {
3394
3776
  transition("failed");
3395
3777
  throw error;
3396
3778
  }
3397
3779
  },
3398
3780
  screenshot: async (destination) => {
3781
+ if (deviceIdentifier)
3782
+ throw new Error("Physical iOS screenshots are captured in Xcode Device Hub; the CLI never records a device screen automatically.");
3399
3783
  const resolved = resolve7(project.projectRoot, destination);
3400
3784
  if (!isInside2(project.projectRoot, resolved))
3401
3785
  throw new Error("iOS screenshot destination must remain inside the project.");
@@ -3404,7 +3788,7 @@ var ABSOLUTE_IOS_SIMULATOR_NAME = "AbsoluteJS iPhone", BOOT_TIMEOUT_MS = 180000,
3404
3788
  project.xcrun,
3405
3789
  "simctl",
3406
3790
  "io",
3407
- device.udid,
3791
+ udid,
3408
3792
  "screenshot",
3409
3793
  resolved
3410
3794
  ], "iOS simulator screenshot", run, { signal: options.signal });
@@ -3417,6 +3801,9 @@ var ABSOLUTE_IOS_SIMULATOR_NAME = "AbsoluteJS iPhone", BOOT_TIMEOUT_MS = 180000,
3417
3801
  } catch (error) {
3418
3802
  transition("failed");
3419
3803
  await closeLogs();
3804
+ await caEnrollmentServer?.close().catch(() => {
3805
+ return;
3806
+ });
3420
3807
  await repairAbsoluteIosDevSession(project.projectRoot);
3421
3808
  throw error;
3422
3809
  }
@@ -3425,6 +3812,7 @@ var init_iosSimulatorController = __esm(() => {
3425
3812
  init_emulatorDoctor();
3426
3813
  init_capacitorProject();
3427
3814
  init_iosRelease();
3815
+ init_iosPhysicalDeviceTransport();
3428
3816
  init_getDurationString();
3429
3817
  SECRET_VALUE = /((?:authorization|cookie|password|secret|token|oauth[_-]?code)\s*[:=]\s*)([^\s,;]+)/giu;
3430
3818
  BEARER_VALUE = new RegExp(String.raw`\bBearer\s+[A-Za-z0-9._~+/-]+=*`, "giu");
@@ -3436,6 +3824,7 @@ var init_iosSimulatorController = __esm(() => {
3436
3824
  ["fingerprinting", "fingerprint"],
3437
3825
  ["booting", "simulator"],
3438
3826
  ["connecting", "device ready"],
3827
+ ["enrolling-trust", "HTTPS trust"],
3439
3828
  ["checking-native", "app check"],
3440
3829
  ["building", "Xcode"],
3441
3830
  ["installing", "install"],
@@ -3448,9 +3837,10 @@ var init_iosSimulatorController = __esm(() => {
3448
3837
  var ABSOLUTE_REMOTE_MAC_EVENT_PREFIX = "ABSOLUTE_REMOTE_MAC\t", ABSOLUTE_REMOTE_MAC_PROTOCOL_VERSION = 1;
3449
3838
 
3450
3839
  // src/mobile/remoteMacProtocol.ts
3451
- import { createHash as createHash5, randomUUID as randomUUID3 } from "crypto";
3452
- import { chmod, mkdir as mkdir5, readFile as readFile6, rename as rename5, writeFile as writeFile5 } from "fs/promises";
3840
+ import { createHash as createHash5, randomUUID as randomUUID4 } from "crypto";
3841
+ import { chmod, mkdir as mkdir5, readFile as readFile7, rename as rename5, writeFile as writeFile5 } from "fs/promises";
3453
3842
  import { homedir as homedir4 } from "os";
3843
+ import { isIP as isIP3 } from "net";
3454
3844
  import {
3455
3845
  dirname as dirname6,
3456
3846
  isAbsolute as isAbsolute4,
@@ -3465,7 +3855,7 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
3465
3855
  profiles: {}
3466
3856
  }), loadStore = async (path = defaultProfilePath()) => {
3467
3857
  try {
3468
- const parsed = JSON.parse(await readFile6(path, "utf8"));
3858
+ const parsed = JSON.parse(await readFile7(path, "utf8"));
3469
3859
  if (parsed.format !== PROFILE_FORMAT || typeof parsed.profiles !== "object" || parsed.profiles === null || Array.isArray(parsed.profiles))
3470
3860
  throw new Error("Unsupported remote Mac profile format.");
3471
3861
  for (const [key, profile] of Object.entries(parsed.profiles)) {
@@ -3482,7 +3872,7 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
3482
3872
  }
3483
3873
  }, saveStore = async (store, path = defaultProfilePath()) => {
3484
3874
  await mkdir5(dirname6(path), { recursive: true });
3485
- const temporary = `${path}.${randomUUID3()}.tmp`;
3875
+ const temporary = `${path}.${randomUUID4()}.tmp`;
3486
3876
  await writeFile5(temporary, `${JSON.stringify(store, null, 2)}
3487
3877
  `, {
3488
3878
  mode: 384
@@ -3564,6 +3954,18 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
3564
3954
  if (!xcodeVersion?.startsWith("Xcode "))
3565
3955
  throw new Error("The remote Mac must have full Xcode installed and selected.");
3566
3956
  return { bunPath, home, os: operatingSystem, xcodeVersion };
3957
+ }, inspectAbsoluteRemoteMacLanHost = async (profile, transport) => {
3958
+ const capture = transport?.capture ?? defaultTransport.capture;
3959
+ const script = `default_interface="$(/sbin/route -n get default 2>/dev/null | /usr/bin/awk '/interface:/{print $2; exit}')"; for interface in "$default_interface" en0 en1; do [ -n "$interface" ] || continue; address="$(/usr/sbin/ipconfig getifaddr "$interface" 2>/dev/null || true)"; if [ -n "$address" ]; then printf '%s\\n' "$address"; exit 0; fi; done; exit 1`;
3960
+ const result = await capture([
3961
+ ...absoluteRemoteMacSshBase(profile),
3962
+ "/bin/sh -lc",
3963
+ shellQuote(script)
3964
+ ]);
3965
+ const host = requireRemoteSuccess(result, "Remote Mac LAN address discovery").trim();
3966
+ if (isIP3(host) === 0)
3967
+ throw new Error("The Remote Mac did not report a device-reachable LAN address.");
3968
+ return host;
3567
3969
  }, listAbsoluteRemoteMacProfiles = async (profilePath) => {
3568
3970
  const store = await loadStore(profilePath);
3569
3971
  return {
@@ -3630,7 +4032,7 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
3630
4032
  ]);
3631
4033
  if (verified.exitCode === 0)
3632
4034
  return { ...artifact, remotePath, uploaded: false };
3633
- const temporary = posix.join(directory, `.agent-${randomUUID3()}.tmp`);
4035
+ const temporary = posix.join(directory, `.agent-${randomUUID4()}.tmp`);
3634
4036
  const installScript = [
3635
4037
  "set -eu",
3636
4038
  "umask 077",
@@ -3717,7 +4119,7 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
3717
4119
  }), absoluteRemoteProjectSyncCommands = (project) => {
3718
4120
  const current = project.remoteProjectRoot;
3719
4121
  const parent = posix.dirname(current);
3720
- const staging = posix.join(parent, `.incoming-${randomUUID3()}`);
4122
+ const staging = posix.join(parent, `.incoming-${randomUUID4()}`);
3721
4123
  const previous = posix.join(parent, ".previous");
3722
4124
  const script = [
3723
4125
  "set -eu",
@@ -3806,6 +4208,13 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
3806
4208
  await syncProject(options.project);
3807
4209
  const syncDuration = performance.now() - syncStartedAt;
3808
4210
  const encodedConfig = Buffer.from(JSON.stringify(portableMobileConfig(options.project))).toString("base64url");
4211
+ const encodedCertificateAuthority = options.certificateAuthorityPath ? (await readFile7(options.certificateAuthorityPath)).toString("base64url") : undefined;
4212
+ const physicalDevice = options.deviceIdentifier !== undefined;
4213
+ let relayPort;
4214
+ if (physicalDevice)
4215
+ relayPort = options.port <= 49151 ? options.port + 16384 : options.port - 16384;
4216
+ if (physicalDevice && !options.serverHost)
4217
+ throw new Error("Remote physical iOS development requires the Remote Mac LAN host.");
3809
4218
  const remoteCommand = [
3810
4219
  `cd ${shellQuote(options.project.remoteProjectRoot)}`,
3811
4220
  "&&",
@@ -3816,14 +4225,26 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
3816
4225
  String(options.port),
3817
4226
  "--mobile-config",
3818
4227
  shellQuote(encodedConfig),
3819
- ...options.https ? ["--https"] : []
4228
+ ...encodedCertificateAuthority ? [
4229
+ "--certificate-authority",
4230
+ shellQuote(encodedCertificateAuthority)
4231
+ ] : [],
4232
+ ...options.https ? ["--https"] : [],
4233
+ ...options.deviceIdentifier ? [
4234
+ "--ios-device",
4235
+ shellQuote(options.deviceIdentifier),
4236
+ "--server-host",
4237
+ shellQuote(options.serverHost ?? ""),
4238
+ "--relay-port",
4239
+ String(relayPort)
4240
+ ] : []
3820
4241
  ].join(" ");
3821
4242
  const command = [
3822
4243
  ...absoluteRemoteMacSshBase(options.project.profile),
3823
4244
  "-o",
3824
4245
  "ExitOnForwardFailure=yes",
3825
4246
  "-R",
3826
- `${options.port}:127.0.0.1:${options.port}`,
4247
+ physicalDevice ? `127.0.0.1:${relayPort}:127.0.0.1:${options.port}` : `${options.port}:127.0.0.1:${options.port}`,
3827
4248
  "/bin/sh -lc",
3828
4249
  shellQuote(remoteCommand)
3829
4250
  ];
@@ -3911,7 +4332,7 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
3911
4332
  };
3912
4333
  options.log?.(`Remote Mac connected (${options.project.profile.name}); agent ${agent.uploaded ? "uploaded" : "cache hit"}, project synced, and iOS ready in ${totalDuration.toFixed(2)}ms.`);
3913
4334
  const request = (commandName) => {
3914
- const id = randomUUID3();
4335
+ const id = randomUUID4();
3915
4336
  const response = new Promise((resolve8, reject) => pending.set(id, { reject, resolve: resolve8 }));
3916
4337
  process2.stdin.write(`${JSON.stringify({ command: commandName, id, v: 1 })}
3917
4338
  `);
@@ -3944,6 +4365,7 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
3944
4365
  close,
3945
4366
  nativeCacheHit: currentReady.nativeCacheHit,
3946
4367
  startedSimulator: currentReady.startedSimulator,
4368
+ targetKind: currentReady.targetKind,
3947
4369
  timings: currentReady.timings,
3948
4370
  udid: currentReady.udid,
3949
4371
  rebuild: async () => {
@@ -3999,9 +4421,11 @@ var init_remoteMacProtocol = __esm(() => {
3999
4421
  var exports_devCert = {};
4000
4422
  __export(exports_devCert, {
4001
4423
  setupMkcert: () => setupMkcert,
4424
+ normalizeDevCertificateHosts: () => normalizeDevCertificateHosts,
4002
4425
  loadDevCert: () => loadDevCert,
4003
4426
  hasMkcert: () => hasMkcert,
4004
4427
  hasCert: () => hasCert,
4428
+ getDevCertificateAuthorityPath: () => getDevCertificateAuthorityPath,
4005
4429
  ensureDevCert: () => ensureDevCert
4006
4430
  });
4007
4431
  import {
@@ -4011,20 +4435,31 @@ import {
4011
4435
  readFileSync as readFileSync7,
4012
4436
  rmSync
4013
4437
  } from "fs";
4438
+ import { X509Certificate as X509Certificate2 } from "crypto";
4439
+ import { isIP as isIP4 } from "net";
4014
4440
  import { platform as platform2 } from "os";
4015
4441
  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 = () => {
4442
+ 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 = []) => {
4443
+ const normalized = new Set(DEFAULT_CERTIFICATE_HOSTS);
4444
+ for (const host of hosts) {
4445
+ const value = host.trim().toLowerCase();
4446
+ if (!value || value === "0.0.0.0" || value === "::")
4447
+ continue;
4448
+ if (isIP4(value) === 0 && !CERTIFICATE_HOSTNAME_PATTERN.test(value)) {
4449
+ throw new TypeError(`Invalid development certificate host: ${host}`);
4450
+ }
4451
+ normalized.add(value);
4452
+ }
4453
+ return [...normalized];
4454
+ }, certificateIsUsable = (hosts) => {
4017
4455
  try {
4018
4456
  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();
4457
+ const certificate = new X509Certificate2(certPem);
4458
+ if (new Date(certificate.validTo).getTime() <= Date.now())
4459
+ return false;
4460
+ return normalizeDevCertificateHosts(hosts).every((host) => isIP4(host) ? certificate.checkIP(host) !== undefined : certificate.checkHost(host) !== undefined);
4026
4461
  } catch {
4027
- return true;
4462
+ return false;
4028
4463
  }
4029
4464
  }, hasMkcert = () => {
4030
4465
  try {
@@ -4036,22 +4471,21 @@ var CERT_DIR, CERT_PATH, KEY_PATH, CERT_VALIDITY_DAYS = 365, devLog = (msg) => c
4036
4471
  } catch {
4037
4472
  return false;
4038
4473
  }
4039
- }, generateWithMkcert = () => {
4474
+ }, generateWithMkcert = (hosts = []) => {
4040
4475
  const result = Bun.spawnSync([
4041
4476
  "mkcert",
4042
4477
  "-cert-file",
4043
4478
  CERT_PATH,
4044
4479
  "-key-file",
4045
4480
  KEY_PATH,
4046
- "localhost",
4047
- "127.0.0.1",
4048
- "::1"
4481
+ ...normalizeDevCertificateHosts(hosts)
4049
4482
  ], { stderr: "pipe", stdout: "pipe" });
4050
4483
  if (result.exitCode !== 0) {
4051
4484
  const err = new TextDecoder().decode(result.stderr);
4052
4485
  throw new Error(`mkcert failed: ${err}`);
4053
4486
  }
4054
- }, generateSelfSigned = () => {
4487
+ }, generateSelfSigned = (hosts = []) => {
4488
+ const subjectAlternativeNames = normalizeDevCertificateHosts(hosts).map((host) => `${isIP4(host) ? "IP" : "DNS"}:${host}`).join(",");
4055
4489
  const proc = Bun.spawnSync([
4056
4490
  "openssl",
4057
4491
  "req",
@@ -4070,36 +4504,36 @@ var CERT_DIR, CERT_PATH, KEY_PATH, CERT_VALIDITY_DAYS = 365, devLog = (msg) => c
4070
4504
  "-subj",
4071
4505
  "/CN=localhost",
4072
4506
  "-addext",
4073
- "subjectAltName=DNS:localhost,IP:127.0.0.1,IP:::1"
4507
+ `subjectAltName=${subjectAlternativeNames}`
4074
4508
  ], { stderr: "pipe", stdout: "pipe" });
4075
4509
  if (proc.exitCode !== 0) {
4076
4510
  const err = new TextDecoder().decode(proc.stderr);
4077
4511
  throw new Error(`openssl failed: ${err}`);
4078
4512
  }
4079
4513
  devLog("Using self-signed certificate \u2014 browser will show a one-time warning");
4080
- }, generateCert = () => {
4514
+ }, generateCert = (hosts = []) => {
4081
4515
  if (hasMkcert()) {
4082
- generateWithMkcert();
4516
+ generateWithMkcert(hosts);
4083
4517
  } else {
4084
- generateSelfSigned();
4518
+ generateSelfSigned(hosts);
4085
4519
  }
4086
- }, ensureDevCert = () => {
4520
+ }, ensureDevCert = (hosts = []) => {
4087
4521
  mkdirSync4(CERT_DIR, { recursive: true });
4088
- if (hasCert()) {
4522
+ if (hasCert(hosts)) {
4089
4523
  return { cert: CERT_PATH, key: KEY_PATH };
4090
4524
  }
4091
4525
  if (certFilesExist()) {
4092
- devLog("Certificate expired, regenerating...");
4526
+ devLog("Certificate is expired or missing a required host, regenerating...");
4093
4527
  }
4094
4528
  try {
4095
- generateCert();
4529
+ generateCert(hosts);
4096
4530
  } catch (err) {
4097
4531
  devWarn(`Failed to generate certificate: ${err instanceof Error ? err.message : err}`);
4098
4532
  return null;
4099
4533
  }
4100
4534
  return { cert: CERT_PATH, key: KEY_PATH };
4101
- }, hasCert = () => certFilesExist() && !isCertExpired(), loadDevCert = () => {
4102
- const paths = ensureDevCert();
4535
+ }, hasCert = (hosts = []) => certFilesExist() && certificateIsUsable(hosts), loadDevCert = (hosts = []) => {
4536
+ const paths = ensureDevCert(hosts);
4103
4537
  if (!paths)
4104
4538
  return null;
4105
4539
  try {
@@ -4215,7 +4649,15 @@ var CERT_DIR, CERT_PATH, KEY_PATH, CERT_VALIDITY_DAYS = 365, devLog = (msg) => c
4215
4649
  } catch {
4216
4650
  return null;
4217
4651
  }
4218
- }, mkcertCaRoot = () => runCapture(["mkcert", "-CAROOT"]), toWindowsPath = (linuxPath) => runCapture(["wslpath", "-w", linuxPath]), windowsTempDir = () => {
4652
+ }, mkcertCaRoot = () => runCapture(["mkcert", "-CAROOT"]), getDevCertificateAuthorityPath = () => {
4653
+ const caRoot = hasMkcert() ? mkcertCaRoot() : null;
4654
+ const rootCertificate = caRoot ? join12(caRoot, "rootCA.pem") : null;
4655
+ if (rootCertificate && existsSync4(rootCertificate))
4656
+ return rootCertificate;
4657
+ if (certFilesExist())
4658
+ return CERT_PATH;
4659
+ return null;
4660
+ }, toWindowsPath = (linuxPath) => runCapture(["wslpath", "-w", linuxPath]), windowsTempDir = () => {
4219
4661
  const winTemp = runCapture(["cmd.exe", "/c", "echo %TEMP%"]);
4220
4662
  if (!winTemp)
4221
4663
  return null;
@@ -4249,7 +4691,7 @@ var CERT_DIR, CERT_PATH, KEY_PATH, CERT_VALIDITY_DAYS = 365, devLog = (msg) => c
4249
4691
  ], { stderr: "pipe", stdout: "pipe" });
4250
4692
  rmSync(staged, { force: true });
4251
4693
  return result.exitCode === 0;
4252
- }, setupMkcert = () => {
4694
+ }, setupMkcert = (hosts = []) => {
4253
4695
  if (!ensureMkcert())
4254
4696
  return false;
4255
4697
  const installResult = Bun.spawnSync(["mkcert", "-install"], {
@@ -4276,7 +4718,7 @@ var CERT_DIR, CERT_PATH, KEY_PATH, CERT_VALIDITY_DAYS = 365, devLog = (msg) => c
4276
4718
  rmSync(CERT_PATH, { force: true });
4277
4719
  rmSync(KEY_PATH, { force: true });
4278
4720
  mkdirSync4(CERT_DIR, { recursive: true });
4279
- generateWithMkcert();
4721
+ generateWithMkcert(hosts);
4280
4722
  console.log("");
4281
4723
  devLog("mkcert installed \u2014 HTTPS certificates are now locally trusted");
4282
4724
  return true;
@@ -4285,6 +4727,8 @@ var init_devCert = __esm(() => {
4285
4727
  CERT_DIR = join12(process.cwd(), ".absolutejs");
4286
4728
  CERT_PATH = join12(CERT_DIR, "cert.pem");
4287
4729
  KEY_PATH = join12(CERT_DIR, "key.pem");
4730
+ DEFAULT_CERTIFICATE_HOSTS = ["localhost", "127.0.0.1", "::1"];
4731
+ 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
4732
  });
4289
4733
 
4290
4734
  // src/cli/scripts/eslintChunked.ts
@@ -5689,7 +6133,7 @@ var normalizeSlug = (str) => str.trim().replace(/\s+/g, "-").replace(/[^A-Za-z0-
5689
6133
 
5690
6134
  // src/mobile/buildRelease.ts
5691
6135
  import { createHash as createHash8 } from "crypto";
5692
- import { mkdir as mkdir7, readFile as readFile7, writeFile as writeFile6 } from "fs/promises";
6136
+ import { mkdir as mkdir7, readFile as readFile8, writeFile as writeFile6 } from "fs/promises";
5693
6137
  import { basename as basename6, dirname as dirname9, extname as extname3, join as join16, relative as relative9, resolve as resolve13 } from "path";
5694
6138
  var sha256 = (bytes) => createHash8("sha256").update(bytes).digest("hex"), STATIC_SCRIPT_PATTERN, rewriteStaticScriptPaths = (source, manifest) => source.replace(STATIC_SCRIPT_PATTERN, (match, prefix, path, suffix) => {
5695
6139
  if (path.endsWith("/htmx.min.js"))
@@ -5711,7 +6155,7 @@ var sha256 = (bytes) => createHash8("sha256").update(bytes).digest("hex"), STATI
5711
6155
  }
5712
6156
  let resolvedAssetPath = resolveAssetPath(buildDirectory, assetPath);
5713
6157
  if (metadata.framework === "html" || metadata.framework === "htmx") {
5714
- const source = await readFile7(resolvedAssetPath, "utf8");
6158
+ const source = await readFile8(resolvedAssetPath, "utf8");
5715
6159
  const rewritten = rewriteStaticScriptPaths(source, manifest);
5716
6160
  const documentHash = sha256(new TextEncoder().encode(rewritten));
5717
6161
  resolvedAssetPath = join16(buildDirectory, ".absolutejs", "mobile-pages", `${documentHash}.html`);
@@ -5725,8 +6169,8 @@ var sha256 = (bytes) => createHash8("sha256").update(bytes).digest("hex"), STATI
5725
6169
  ].map((key) => manifest[key]).find((path) => typeof path === "string");
5726
6170
  const resolvedStylePath = styleAssetPath ? resolveAssetPath(buildDirectory, styleAssetPath) : undefined;
5727
6171
  const [bytes, styleBytes] = await Promise.all([
5728
- readFile7(resolvedAssetPath),
5729
- resolvedStylePath ? readFile7(resolvedStylePath) : undefined
6172
+ readFile8(resolvedAssetPath),
6173
+ resolvedStylePath ? readFile8(resolvedStylePath) : undefined
5730
6174
  ]);
5731
6175
  const bundlePath = `/${relative9(resolve13(buildDirectory), resolvedAssetPath).replaceAll("\\", "/")}`;
5732
6176
  const styleBundlePath = resolvedStylePath ? `/${relative9(resolve13(buildDirectory), resolvedStylePath).replaceAll("\\", "/")}` : undefined;
@@ -5745,7 +6189,7 @@ var sha256 = (bytes) => createHash8("sha256").update(bytes).digest("hex"), STATI
5745
6189
  }, buildAbsoluteMobileCompatibilityRelease = async (options) => {
5746
6190
  const [captured, producerBytes] = await Promise.all([
5747
6191
  captureAbsoluteMobileRouteGraph(options.app),
5748
- readFile7(options.producerPath)
6192
+ readFile8(options.producerPath)
5749
6193
  ]);
5750
6194
  if (captured.length === 0) {
5751
6195
  throw new TypeError("No instrumented AbsoluteJS mobile page routes were found in the finalized Elysia route graph.");
@@ -5925,7 +6369,7 @@ import {
5925
6369
  copyFile as copyFile4,
5926
6370
  mkdir as mkdir8,
5927
6371
  mkdtemp as mkdtemp3,
5928
- readFile as readFile8,
6372
+ readFile as readFile9,
5929
6373
  rename as rename6,
5930
6374
  rm as rm5,
5931
6375
  writeFile as writeFile7
@@ -5983,7 +6427,7 @@ var MANIFEST_FILE = "absolute-mobile-manifest.json", BOOTSTRAP_FILE = "absolute-
5983
6427
  const packageName = specifier.startsWith("@") ? segments.slice(0, 2).join("/") : segments[0] ?? "";
5984
6428
  const subpath = specifier.slice(packageName.length);
5985
6429
  const packageDirectory = join17(resolve14(projectRoot), "node_modules", packageName);
5986
- const manifest = JSON.parse(await readFile8(join17(packageDirectory, "package.json"), "utf8"));
6430
+ const manifest = JSON.parse(await readFile9(join17(packageDirectory, "package.json"), "utf8"));
5987
6431
  const exports = typeof manifest === "object" && manifest !== null ? Reflect.get(manifest, "exports") : undefined;
5988
6432
  const entry = typeof exports === "object" && exports !== null ? Reflect.get(exports, subpath ? `.${subpath}` : ".") : undefined;
5989
6433
  const target = importEntryTarget(entry);
@@ -6085,7 +6529,7 @@ void startAbsoluteMobileShell(${push ? `{ createAuth: (config, options) => creat
6085
6529
  ...localStylePath ? { localStylePath } : {}
6086
6530
  };
6087
6531
  }, absoluteClientImports = async (sourcePath, buildDirectory) => {
6088
- const source = await readFile8(sourcePath, "utf8");
6532
+ const source = await readFile9(sourcePath, "utf8");
6089
6533
  const extension = extname4(sourcePath).toLowerCase();
6090
6534
  let scriptLoader;
6091
6535
  if (extension === ".tsx")
@@ -6227,7 +6671,7 @@ import {
6227
6671
  access as access6,
6228
6672
  mkdir as mkdir9,
6229
6673
  mkdtemp as mkdtemp4,
6230
- readFile as readFile9,
6674
+ readFile as readFile10,
6231
6675
  rename as rename7,
6232
6676
  rm as rm6,
6233
6677
  writeFile as writeFile8
@@ -6327,7 +6771,7 @@ var ABSOLUTE_MOBILE_MATERIALIZED_BUNDLE_FORMAT = 1, CURRENT_BUNDLE_FILE = "curre
6327
6771
  }, readAbsoluteMobileMaterializedReleases = async (root) => {
6328
6772
  const resolvedRoot = resolvePath2(root);
6329
6773
  try {
6330
- const serialized = await readFile9(join18(resolvedRoot, CURRENT_BUNDLE_FILE), "utf8");
6774
+ const serialized = await readFile10(join18(resolvedRoot, CURRENT_BUNDLE_FILE), "utf8");
6331
6775
  const parsed = JSON.parse(serialized);
6332
6776
  const index = parseBundleIndex(parsed);
6333
6777
  const bundleRoot = join18(resolvedRoot, BUNDLES_DIRECTORY, index.bundleId);
@@ -7029,7 +7473,7 @@ var init_deviceCapabilities = __esm(() => {
7029
7473
  });
7030
7474
 
7031
7475
  // src/mobile/buildPipeline.ts
7032
- import { readFile as readFile10 } from "fs/promises";
7476
+ import { readFile as readFile11 } from "fs/promises";
7033
7477
  import { join as join21, resolve as resolve17 } from "path";
7034
7478
  import { pathToFileURL } from "url";
7035
7479
  var isElysiaApp = (value) => typeof value === "object" && value !== null && typeof Reflect.get(value, "compile") === "function" && Array.isArray(Reflect.get(value, "routes")), isStringRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((entry) => typeof entry === "string"), serverExportName = (loaded, app) => {
@@ -7063,7 +7507,7 @@ var isElysiaApp = (value) => typeof value === "object" && value !== null && type
7063
7507
  const mobile = normalizeAbsoluteMobileConfig(options.mobile, options.projectRoot);
7064
7508
  const root = join21(buildDirectory, ".absolutejs", "mobile-compatibility");
7065
7509
  const [manifestSource, previous] = await Promise.all([
7066
- readFile10(join21(buildDirectory, "manifest.json"), "utf8"),
7510
+ readFile11(join21(buildDirectory, "manifest.json"), "utf8"),
7067
7511
  readAbsoluteMobileMaterializedReleases(root)
7068
7512
  ]);
7069
7513
  const manifest = JSON.parse(manifestSource);
@@ -10769,8 +11213,8 @@ export { value };
10769
11213
  host2.getSourceFile = (fileName, languageVersion, onError, shouldCreate) => fileName === virtualPath ? ts6.createSourceFile(fileName, source, languageVersion, true) : getSourceFile(fileName, languageVersion, onError, shouldCreate);
10770
11214
  const fileExists = host2.fileExists.bind(host2);
10771
11215
  host2.fileExists = (fileName) => fileName === virtualPath ? true : fileExists(fileName);
10772
- const readFile11 = host2.readFile.bind(host2);
10773
- host2.readFile = (fileName) => fileName === virtualPath ? source : readFile11(fileName);
11216
+ const readFile12 = host2.readFile.bind(host2);
11217
+ host2.readFile = (fileName) => fileName === virtualPath ? source : readFile12(fileName);
10774
11218
  const program = ts6.createProgram([virtualPath], options, host2);
10775
11219
  const checker = program.getTypeChecker();
10776
11220
  const sourceFile = program.getSourceFile(virtualPath);
@@ -16393,10 +16837,10 @@ var init_compile = __esm(() => {
16393
16837
  });
16394
16838
 
16395
16839
  // src/mobile/nativeDeepLinks.ts
16396
- import { readFile as readFile11, rename as rename8, writeFile as writeFile9 } from "fs/promises";
16840
+ import { readFile as readFile12, rename as rename8, writeFile as writeFile9 } from "fs/promises";
16397
16841
  import { join as join46 } from "path";
16398
16842
  var START_MARKER = "<!-- absolutejs:deep-links:start -->", END_MARKER = "<!-- absolutejs:deep-links:end -->", IOS_ENTITLEMENTS = "App/AbsoluteJS.entitlements", NOT_FOUND = -1, escapeXml = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;"), writeChangedFile = async (path, source) => {
16399
- const current = await readFile11(path, "utf8");
16843
+ const current = await readFile12(path, "utf8");
16400
16844
  if (current === source)
16401
16845
  return false;
16402
16846
  const temporary = `${path}.${crypto.randomUUID()}.tmp`;
@@ -16444,7 +16888,7 @@ ${hosts}
16444
16888
  `;
16445
16889
  }, configureAndroid = async (config) => {
16446
16890
  const path = join46(config.nativeProjectDirectory, "android/app/src/main/AndroidManifest.xml");
16447
- const source = await readFile11(path, "utf8");
16891
+ const source = await readFile12(path, "utf8");
16448
16892
  const mainActivity = source.indexOf('android:name=".MainActivity"');
16449
16893
  if (mainActivity === NOT_FOUND) {
16450
16894
  throw new TypeError("Android MainActivity was not found.");
@@ -16468,7 +16912,7 @@ ${hosts}
16468
16912
  ${END_MARKER}
16469
16913
  `, configureIosInfo = async (config) => {
16470
16914
  const path = join46(config.nativeProjectDirectory, "ios/App/App/Info.plist");
16471
- const source = await readFile11(path, "utf8");
16915
+ const source = await readFile12(path, "utf8");
16472
16916
  const region = config.deepLinkScheme ? iosSchemeRegion(config.deepLinkScheme) : ` ${START_MARKER}
16473
16917
  ${END_MARKER}
16474
16918
  `;
@@ -16492,7 +16936,7 @@ ${domains}
16492
16936
  const path = join46(config.nativeProjectDirectory, "ios/App/AbsoluteJS.entitlements");
16493
16937
  let current = "";
16494
16938
  try {
16495
- current = await readFile11(path, "utf8");
16939
+ current = await readFile12(path, "utf8");
16496
16940
  } catch (error) {
16497
16941
  if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
16498
16942
  throw error;
@@ -16507,7 +16951,7 @@ ${domains}
16507
16951
  return true;
16508
16952
  }, configureIosProject = async (config) => {
16509
16953
  const path = join46(config.nativeProjectDirectory, "ios/App/App.xcodeproj/project.pbxproj");
16510
- const source = await readFile11(path, "utf8");
16954
+ const source = await readFile12(path, "utf8");
16511
16955
  const declarations = [
16512
16956
  ...source.matchAll(/CODE_SIGN_ENTITLEMENTS = ([^;]+);/g)
16513
16957
  ].map((match) => match[1]);
@@ -16545,10 +16989,10 @@ ${domains}
16545
16989
  var init_nativeDeepLinks = () => {};
16546
16990
 
16547
16991
  // src/mobile/nativeDeviceCapabilities.ts
16548
- import { readFile as readFile12, rename as rename9, writeFile as writeFile10 } from "fs/promises";
16992
+ import { readFile as readFile13, rename as rename9, writeFile as writeFile10 } from "fs/promises";
16549
16993
  import { join as join47 } from "path";
16550
16994
  var START_MARKER2 = "<!-- absolutejs:device-capabilities:start -->", END_MARKER2 = "<!-- absolutejs:device-capabilities:end -->", NOT_FOUND2 = -1, IOS_PRIVACY_FILE_REFERENCE = "A85D0C000000000000000001", IOS_PRIVACY_BUILD_FILE = "A85D0C000000000000000002", PUSH_START_MARKER = "absolutejs:push-notifications:start", PUSH_END_MARKER = "absolutejs:push-notifications:end", escapeXml2 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;"), writeChangedFile2 = async (path, source) => {
16551
- const current = await readFile12(path, "utf8");
16995
+ const current = await readFile13(path, "utf8");
16552
16996
  if (current === source)
16553
16997
  return false;
16554
16998
  const temporary = `${path}.${crypto.randomUUID()}.tmp`;
@@ -16566,7 +17010,7 @@ var START_MARKER2 = "<!-- absolutejs:device-capabilities:start -->", END_MARKER2
16566
17010
  return writeChangedFile2(path, source);
16567
17011
  }, optionalSource = async (path) => {
16568
17012
  try {
16569
- return await readFile12(path, "utf8");
17013
+ return await readFile13(path, "utf8");
16570
17014
  } catch (error) {
16571
17015
  if (typeof error === "object" && error !== null && Reflect.get(error, "code") === "ENOENT")
16572
17016
  return null;
@@ -16674,7 +17118,7 @@ ${entries}
16674
17118
  if (requirements.iosPrivacyAccessedApis.length === 0)
16675
17119
  return false;
16676
17120
  const projectPath = join47(config.nativeProjectDirectory, "ios/App/App.xcodeproj/project.pbxproj");
16677
- const project = await readFile12(projectPath, "utf8");
17121
+ const project = await readFile13(projectPath, "utf8");
16678
17122
  return writeChangedFile2(projectPath, addIosPrivacyProjectReference(project));
16679
17123
  }, addIosPrivacyProjectReference = (source) => {
16680
17124
  const fileMatch = source.match(/([A-F0-9]{24}) \/\* PrivacyInfo\.xcprivacy \*\/ = \{isa = PBXFileReference;/u);
@@ -16725,7 +17169,7 @@ ${next.slice(index)}`;
16725
17169
  return next;
16726
17170
  }, configureIos2 = async (config, plan) => {
16727
17171
  const path = join47(config.nativeProjectDirectory, "ios/App/App/Info.plist");
16728
- const source = await readFile12(path, "utf8");
17172
+ const source = await readFile13(path, "utf8");
16729
17173
  const requirements = absoluteDeviceNativeRequirements(plan);
16730
17174
  const existingSystemBars = source.match(/<key>UIViewControllerBasedStatusBarAppearance<\/key>\s*<(true|false)\s*\/>/u);
16731
17175
  const ownedStart = source.indexOf(START_MARKER2);
@@ -16815,7 +17259,7 @@ ${content}
16815
17259
  return `${source.slice(0, insertion)}${region}${source.slice(insertion)}`;
16816
17260
  }, configureAndroid2 = async (config, plan) => {
16817
17261
  const path = join47(config.nativeProjectDirectory, "android/app/src/main/AndroidManifest.xml");
16818
- const source = await readFile12(path, "utf8");
17262
+ const source = await readFile13(path, "utf8");
16819
17263
  const permissions = absoluteDeviceNativeRequirements(plan).androidPermissions;
16820
17264
  const content = permissions.map((permission) => ` <uses-permission android:name="${escapeXml2(permission)}" />`).join(`
16821
17265
  `);
@@ -16874,10 +17318,10 @@ var init_nativeDeviceCapabilities = __esm(() => {
16874
17318
  });
16875
17319
 
16876
17320
  // src/mobile/nativeBackgroundSync.ts
16877
- import { readFile as readFile13, rename as rename10, writeFile as writeFile11 } from "fs/promises";
17321
+ import { readFile as readFile14, rename as rename10, writeFile as writeFile11 } from "fs/promises";
16878
17322
  import { join as join48 } from "path";
16879
17323
  var writeChanged = async (path, source) => {
16880
- const current = await readFile13(path, "utf8");
17324
+ const current = await readFile14(path, "utf8");
16881
17325
  if (current === source)
16882
17326
  return false;
16883
17327
  const temporary = `${path}.${crypto.randomUUID()}.tmp`;
@@ -16959,10 +17403,10 @@ ${makeRegion(values)} </array>
16959
17403
  return { changed: false };
16960
17404
  const identifier = `${config.appId}.absolutejs.background-sync`;
16961
17405
  const infoPath = join48(config.nativeProjectDirectory, "ios/App/App/Info.plist");
16962
- const info2 = await readFile13(infoPath, "utf8");
17406
+ const info2 = await readFile14(infoPath, "utf8");
16963
17407
  const nextInfo = ensurePlistArrayValues(ensurePlistArrayValues(info2, "BGTaskSchedulerPermittedIdentifiers", [identifier], "background-sync-identifiers"), "UIBackgroundModes", ["fetch", "processing"], "background-sync-modes");
16964
17408
  const delegatePath = join48(config.nativeProjectDirectory, "ios/App/App/AppDelegate.swift");
16965
- let delegate = await readFile13(delegatePath, "utf8");
17409
+ let delegate = await readFile14(delegatePath, "utf8");
16966
17410
  if (!delegate.includes("import AbsoluteSyncCapacitor")) {
16967
17411
  const importIndex = delegate.lastIndexOf("import Capacitor");
16968
17412
  if (importIndex < 0)
@@ -16996,7 +17440,7 @@ var init_nativeBackgroundSync = __esm(() => {
16996
17440
  import {
16997
17441
  access as access7,
16998
17442
  mkdir as mkdir10,
16999
- readFile as readFile14,
17443
+ readFile as readFile15,
17000
17444
  rename as rename11,
17001
17445
  rm as rm7,
17002
17446
  writeFile as writeFile12
@@ -17054,7 +17498,7 @@ var OWNERSHIP_FILE = ".absolutejs-mobile-associations.json", HTTP_OK = 200, VERI
17054
17498
  }, writeAtomic = async (path, source) => {
17055
17499
  let current;
17056
17500
  try {
17057
- current = await readFile14(path, "utf8");
17501
+ current = await readFile15(path, "utf8");
17058
17502
  } catch (error) {
17059
17503
  if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
17060
17504
  throw error;
@@ -17077,7 +17521,7 @@ var OWNERSHIP_FILE = ".absolutejs-mobile-associations.json", HTTP_OK = 200, VERI
17077
17521
  const path = resolve37(root, OWNERSHIP_FILE);
17078
17522
  let ownership;
17079
17523
  try {
17080
- ownership = JSON.parse(await readFile14(path, "utf8"));
17524
+ ownership = JSON.parse(await readFile15(path, "utf8"));
17081
17525
  } catch {
17082
17526
  throw new TypeError(`Association output ${root} already exists and is not owned by AbsoluteJS.`);
17083
17527
  }
@@ -17590,8 +18034,8 @@ var DEFAULT_ROUTE_TIMEOUT_MS = 30000, DEFAULT_HMR_TIMEOUT_MS = 30000, routeExpre
17590
18034
  };
17591
18035
 
17592
18036
  // src/mobile/releaseDoctor.ts
17593
- 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";
18037
+ import { access as access8, readFile as readFile16, readdir as readdir4 } from "fs/promises";
18038
+ import { dirname as dirname29, extname as extname8, join as join49, relative as relative24 } from "path";
17595
18039
  var HMR_ASSET_PATTERN, RELEASE_ASSET_EXTENSIONS, pathExists5 = async (path) => {
17596
18040
  try {
17597
18041
  await access8(path);
@@ -17604,7 +18048,7 @@ var HMR_ASSET_PATTERN, RELEASE_ASSET_EXTENSIONS, pathExists5 = async (path) => {
17604
18048
  return findHmrAsset(path);
17605
18049
  if (!isFile2 || !RELEASE_ASSET_EXTENSIONS.has(extname8(path)))
17606
18050
  return;
17607
- const source = await readFile15(path, "utf8");
18051
+ const source = await readFile16(path, "utf8");
17608
18052
  return HMR_ASSET_PATTERN.test(source) ? path : undefined;
17609
18053
  }, findHmrAsset = async (root) => {
17610
18054
  if (!await pathExists5(root))
@@ -17643,14 +18087,20 @@ var HMR_ASSET_PATTERN, RELEASE_ASSET_EXTENSIONS, pathExists5 = async (path) => {
17643
18087
  if (!await pathExists5(nativeConfigPath)) {
17644
18088
  return fail5("android.capacitor-config", "The generated Android Capacitor config is missing.", nativeConfigPath, "Run `absolute mobile sync android` before release validation.");
17645
18089
  }
17646
- const unsafe = isUnsafeCapacitorConfig(await readFile15(nativeConfigPath, "utf8"));
18090
+ const unsafe = isUnsafeCapacitorConfig(await readFile16(nativeConfigPath, "utf8"));
17647
18091
  return unsafe ? fail5("android.capacitor-config", "Android Capacitor config contains a development server URL, cleartext transport, navigation allowlist, or invalid JSON.", nativeConfigPath, "Run `absolute mobile sync android`; do not ship development transport overrides.") : pass("android.capacitor-config", "Android Capacitor config contains no development transport overrides.", nativeConfigPath);
17648
18092
  }, manifestReleaseCheck = async (manifestPath) => {
17649
18093
  if (!await pathExists5(manifestPath)) {
17650
18094
  return fail5("android.cleartext", "The Android manifest is missing.", manifestPath, "Run `absolute mobile sync android` before release validation.");
17651
18095
  }
17652
- 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);
18096
+ const source = await readFile16(manifestPath, "utf8");
18097
+ const cleartext = /android:usesCleartextTraffic=["']true["']/u.test(source);
18098
+ const networkConfigName = source.match(/android:networkSecurityConfig=["']@xml\/([a-z0-9_]+)["']/u)?.[1];
18099
+ const networkConfigPath = networkConfigName ? join49(dirname29(manifestPath), "res", "xml", `${networkConfigName}.xml`) : undefined;
18100
+ const developmentTrustReference = /android:networkSecurityConfig=["']@xml\/absolutejs_dev_network_security["']/u.test(source);
18101
+ const developmentTrustContents = networkConfigPath ? await readFile16(networkConfigPath, "utf8").then((value) => value.includes("@raw/absolutejs_dev_ca")).catch(() => false) : false;
18102
+ const developmentTrust = developmentTrustReference || developmentTrustContents;
18103
+ 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
18104
  }, hmrAssetsReleaseCheck = async (publicRoot) => {
17655
18105
  const hmrAsset = await findHmrAsset(publicRoot);
17656
18106
  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);
@@ -17680,7 +18130,7 @@ var HMR_ASSET_PATTERN, RELEASE_ASSET_EXTENSIONS, pathExists5 = async (path) => {
17680
18130
  if (!config.platforms.includes("android") || permissions.length === 0)
17681
18131
  return;
17682
18132
  const path = join49(config.nativeProjectDirectory, "android/app/src/main/AndroidManifest.xml");
17683
- const source = await readFile15(path, "utf8");
18133
+ const source = await readFile16(path, "utf8");
17684
18134
  const missing = permissions.filter((permission) => !source.includes(`android:name="${permission}"`) && !source.includes(`android:name='${permission}'`));
17685
18135
  if (missing.length === 0)
17686
18136
  return;
@@ -17689,7 +18139,7 @@ var HMR_ASSET_PATTERN, RELEASE_ASSET_EXTENSIONS, pathExists5 = async (path) => {
17689
18139
  if (!config.platforms.includes("ios") || purposes.length === 0)
17690
18140
  return;
17691
18141
  const path = join49(config.nativeProjectDirectory, "ios/App/App/Info.plist");
17692
- const source = await readFile15(path, "utf8");
18142
+ const source = await readFile16(path, "utf8");
17693
18143
  const missing = purposes.filter((purpose) => !source.includes(`<key>${IOS_USAGE_KEYS[purpose]}</key>`));
17694
18144
  if (missing.length === 0)
17695
18145
  return;
@@ -17742,7 +18192,7 @@ var HMR_ASSET_PATTERN, RELEASE_ASSET_EXTENSIONS, pathExists5 = async (path) => {
17742
18192
  }
17743
18193
  if (!await pathExists5(nativeConfigPath)) {
17744
18194
  checks.push(fail5("ios.capacitor-config", "The generated iOS Capacitor config is missing.", nativeConfigPath, "Run `absolute mobile sync ios` before release validation."));
17745
- } else if (isUnsafeCapacitorConfig(await readFile15(nativeConfigPath, "utf8"))) {
18195
+ } else if (isUnsafeCapacitorConfig(await readFile16(nativeConfigPath, "utf8"))) {
17746
18196
  checks.push(fail5("ios.capacitor-config", "iOS Capacitor config contains a development server URL, cleartext transport, navigation allowlist, or invalid JSON.", nativeConfigPath, "Run `absolute mobile sync ios`; do not ship development transport overrides."));
17747
18197
  } else {
17748
18198
  checks.push(pass("ios.capacitor-config", "iOS Capacitor config contains no development transport overrides.", nativeConfigPath));
@@ -17750,7 +18200,7 @@ var HMR_ASSET_PATTERN, RELEASE_ASSET_EXTENSIONS, pathExists5 = async (path) => {
17750
18200
  if (!await pathExists5(infoPath)) {
17751
18201
  checks.push(fail5("ios.transport-security", "The iOS Info.plist is missing.", infoPath, "Run `absolute mobile sync ios` before release validation."));
17752
18202
  } else {
17753
- const info2 = await readFile15(infoPath, "utf8");
18203
+ const info2 = await readFile16(infoPath, "utf8");
17754
18204
  checks.push(/<key>NSAllowsArbitraryLoads<\/key>\s*<true\s*\/>/u.test(info2) ? fail5("ios.transport-security", "iOS App Transport Security permits arbitrary network loads.", infoPath, "Remove NSAllowsArbitraryLoads from the release Info.plist.") : pass("ios.transport-security", "iOS App Transport Security does not permit arbitrary loads.", infoPath));
17755
18205
  }
17756
18206
  const hmrAsset = await findHmrAsset(publicRoot);
@@ -17803,13 +18253,13 @@ import {
17803
18253
  copyFile as copyFile5,
17804
18254
  mkdir as mkdir12,
17805
18255
  mkdtemp as mkdtemp5,
17806
- readFile as readFile16,
18256
+ readFile as readFile17,
17807
18257
  rename as rename12,
17808
18258
  rm as rm8,
17809
18259
  stat as stat2,
17810
18260
  writeFile as writeFile14
17811
18261
  } 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";
18262
+ import { dirname as dirname30, isAbsolute as isAbsolute7, join as join50, relative as relative25, resolve as resolve39, sep as sep6 } from "path";
17813
18263
  var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord13 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), requireManifest2 = (value) => {
17814
18264
  if (!isRecord13(value) || typeof value.appBuild !== "string" || typeof value.appId !== "string" || typeof value.runtime !== "string") {
17815
18265
  throw new TypeError("Invalid embedded AbsoluteJS mobile manifest.");
@@ -17859,7 +18309,7 @@ var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord13 = (value) => typeof value ==
17859
18309
  artifactPath
17860
18310
  ]);
17861
18311
  return result.exitCode === 0 && /jar verified/iu.test(result.stdout);
17862
- }, sha256File2 = async (path) => createHash12("sha256").update(await readFile16(path)).digest("hex"), safeOutputDirectory2 = (projectRoot, requested) => {
18312
+ }, sha256File2 = async (path) => createHash12("sha256").update(await readFile17(path)).digest("hex"), safeOutputDirectory2 = (projectRoot, requested) => {
17863
18313
  const root = resolve39(projectRoot);
17864
18314
  const output = resolve39(root, requested ?? ".absolutejs/mobile/releases/android");
17865
18315
  const projectRelative = relative25(root, output);
@@ -17872,7 +18322,7 @@ var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord13 = (value) => typeof value ==
17872
18322
  const artifactName = "app-release.aab";
17873
18323
  const destination = join50(releaseRoot, artifactName);
17874
18324
  if (await pathExists6(releaseRoot)) {
17875
- const existing = requireManifestIdentity(JSON.parse(await readFile16(join50(releaseRoot, "release.json"), "utf8")), metadata);
18325
+ const existing = requireManifestIdentity(JSON.parse(await readFile17(join50(releaseRoot, "release.json"), "utf8")), metadata);
17876
18326
  const [installedBytes, installedSha256] = await Promise.all([
17877
18327
  stat2(destination).then(({ size }) => size),
17878
18328
  sha256File2(destination)
@@ -17882,8 +18332,8 @@ var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord13 = (value) => typeof value ==
17882
18332
  }
17883
18333
  return { artifactPath: destination, metadata: existing, releaseRoot };
17884
18334
  }
17885
- await mkdir12(dirname29(releaseRoot), { recursive: true });
17886
- const staging = await mkdtemp5(join50(dirname29(releaseRoot), ".android-stage-"));
18335
+ await mkdir12(dirname30(releaseRoot), { recursive: true });
18336
+ const staging = await mkdtemp5(join50(dirname30(releaseRoot), ".android-stage-"));
17887
18337
  try {
17888
18338
  await copyFile5(artifactPath, join50(staging, artifactName));
17889
18339
  const complete = {
@@ -17919,7 +18369,7 @@ var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord13 = (value) => typeof value ==
17919
18369
  const host2 = options.host ?? detectAbsoluteMobileHost();
17920
18370
  const androidRoot = options.androidRoot ?? process.env.ANDROID_HOME ?? process.env.ANDROID_SDK_ROOT ?? absoluteManagedAndroidSdkRoot(host2);
17921
18371
  const nativeDirectory = join50(options.config.nativeProjectDirectory, "android");
17922
- const manifest = requireManifest2(JSON.parse(await readFile16(join50(options.config.bundleDirectory, "absolute-mobile-manifest.json"), "utf8")));
18372
+ const manifest = requireManifest2(JSON.parse(await readFile17(join50(options.config.bundleDirectory, "absolute-mobile-manifest.json"), "utf8")));
17923
18373
  if (manifest.appId !== options.config.appId) {
17924
18374
  throw new TypeError("Embedded mobile manifest appId does not match mobile.appId.");
17925
18375
  }
@@ -17983,7 +18433,7 @@ var init_androidRelease = __esm(() => {
17983
18433
  });
17984
18434
 
17985
18435
  // src/mobile/iosConformance.ts
17986
- import { readFile as readFile17, stat as stat3 } from "fs/promises";
18436
+ import { readFile as readFile18, stat as stat3 } from "fs/promises";
17987
18437
  var HMR_LINE, parseAbsoluteIosHmrLog = (line) => {
17988
18438
  const match = HMR_LINE.exec(line);
17989
18439
  if (!match)
@@ -18018,7 +18468,7 @@ var HMR_LINE, parseAbsoluteIosHmrLog = (line) => {
18018
18468
  if (Date.now() > deadline)
18019
18469
  throw new Error(`No iOS native HMR acknowledgement was observed within ${timeoutMs}ms.`);
18020
18470
  options.signal?.throwIfAborted();
18021
- const contents = await readFile17(options.logPath).catch(() => Buffer.alloc(0));
18471
+ const contents = await readFile18(options.logPath).catch(() => Buffer.alloc(0));
18022
18472
  if (contents.byteLength < offset) {
18023
18473
  offset = 0;
18024
18474
  buffered = "";
@@ -18042,7 +18492,7 @@ var init_iosConformance = __esm(() => {
18042
18492
  });
18043
18493
 
18044
18494
  // src/mobile/nativeTestReport.ts
18045
- import { mkdir as mkdir13, readFile as readFile18, writeFile as writeFile15 } from "fs/promises";
18495
+ import { mkdir as mkdir13, readFile as readFile19, writeFile as writeFile15 } from "fs/promises";
18046
18496
  import { join as join51 } from "path";
18047
18497
  var secretPattern, bearerPattern, coordinatePattern, nativeCredentialPattern, sanitizeNativeReportText = (value) => value.replace(nativeCredentialPattern, "[REDACTED]").replace(bearerPattern, "Bearer [REDACTED]").replace(secretPattern, "$1$2[REDACTED]").replace(coordinatePattern, "$1$2[REDACTED]").replace(/(https?:\/\/[^\s?#]+)[?#][^\s]*/giu, "$1?[REDACTED]"), markdownCell = (value) => sanitizeNativeReportText(value).replaceAll("|", "\\|").replaceAll(`
18048
18498
  `, "<br>"), createAbsoluteNativeAutomatedChecks = (run) => {
@@ -18121,7 +18571,7 @@ var secretPattern, bearerPattern, coordinatePattern, nativeCredentialPattern, sa
18121
18571
  reportVersion: 1,
18122
18572
  run: options.run
18123
18573
  }), readPackageVersionForNativeReport = async (packageJsonPath) => {
18124
- const manifest = JSON.parse(await readFile18(packageJsonPath, "utf8"));
18574
+ const manifest = JSON.parse(await readFile19(packageJsonPath, "utf8"));
18125
18575
  if (typeof manifest !== "object" || manifest === null)
18126
18576
  return "unknown";
18127
18577
  const version2 = Reflect.get(manifest, "version");
@@ -18462,11 +18912,11 @@ var exports_mobile = {};
18462
18912
  __export(exports_mobile, {
18463
18913
  runMobile: () => runMobile
18464
18914
  });
18465
- import { access as access11, mkdir as mkdir14, readFile as readFile19, writeFile as writeFile16 } from "fs/promises";
18915
+ import { access as access11, mkdir as mkdir14, readFile as readFile20, writeFile as writeFile16 } from "fs/promises";
18466
18916
  import { join as join52, resolve as resolve41 } from "path";
18467
18917
  import { createInterface } from "readline/promises";
18468
18918
  var NOT_FOUND3 = -1, isRecord15 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), CAPACITOR_PACKAGES, CAPACITOR_PACKAGE_SPECS, CAPACITOR_SYNC_PACKAGE_SPECS, packageNameFromSpec = (spec) => spec.slice(0, spec.lastIndexOf("@")), directProjectPackages = async (projectRoot) => {
18469
- const manifest = JSON.parse(await readFile19(join52(projectRoot, "package.json"), "utf8"));
18919
+ const manifest = JSON.parse(await readFile20(join52(projectRoot, "package.json"), "utf8"));
18470
18920
  if (!isRecord15(manifest))
18471
18921
  throw new TypeError("Application package.json must contain an object.");
18472
18922
  const names = new Set;
@@ -18479,7 +18929,7 @@ var NOT_FOUND3 = -1, isRecord15 = (value) => typeof value === "object" && value
18479
18929
  return names;
18480
18930
  }, resolvedPackageVersion = async (projectRoot, packageName) => {
18481
18931
  try {
18482
- const manifest = JSON.parse(await readFile19(join52(projectRoot, "node_modules", packageName, "package.json"), "utf8"));
18932
+ const manifest = JSON.parse(await readFile20(join52(projectRoot, "node_modules", packageName, "package.json"), "utf8"));
18483
18933
  return isRecord15(manifest) && typeof manifest.version === "string" ? manifest.version : undefined;
18484
18934
  } catch {
18485
18935
  return;
@@ -20678,6 +21128,24 @@ var resolveDevPort = async (requestedPort, options = {}) => {
20678
21128
  ` + `Set \`dev.port\` to a different value in absolute.config.ts (or via the ABSOLUTE_PORT env var), or extend \`dev.portRange\`.`);
20679
21129
  };
20680
21130
 
21131
+ // src/utils/networking.ts
21132
+ import os from "os";
21133
+ var getAllNetworkIPs = () => {
21134
+ const interfaces = os.networkInterfaces();
21135
+ const addresses = Object.values(interfaces).flat().filter((iface) => iface !== undefined);
21136
+ const ipv4Addresses = [];
21137
+ addresses.filter((addr) => !addr.internal && addr.family === "IPv4").forEach((addr) => ipv4Addresses.push(addr.address));
21138
+ return ipv4Addresses;
21139
+ };
21140
+ var getLocalIPAddress = () => {
21141
+ const allIPs = getAllNetworkIPs();
21142
+ if (allIPs.length > 0 && allIPs[0]) {
21143
+ return allIPs[0];
21144
+ }
21145
+ console.warn("No IP address found, falling back to localhost");
21146
+ return "localhost";
21147
+ };
21148
+
20681
21149
  // src/cli/scripts/dev.ts
20682
21150
  init_config();
20683
21151
  init_nativeAuth();
@@ -20982,25 +21450,41 @@ var confirmPrompt = (message, defaultYes = true) => {
20982
21450
  process.stdin.on("data", onData);
20983
21451
  return promise;
20984
21452
  };
20985
- var setupCertWithPrompt = async (ensureDevCert2, setupMkcert2) => {
21453
+ var setupCertWithPrompt = async (ensureDevCert2, setupMkcert2, hosts) => {
20986
21454
  const install = await confirmPrompt("Install mkcert for trusted HTTPS? (no browser warning)");
20987
21455
  if (install) {
20988
- setupMkcert2();
21456
+ setupMkcert2(hosts);
20989
21457
  } else {
20990
- ensureDevCert2();
21458
+ ensureDevCert2(hosts);
20991
21459
  }
20992
21460
  };
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;
21461
+ var setupHttpsCert = async (hosts = []) => {
21462
+ const {
21463
+ getDevCertificateAuthorityPath: getDevCertificateAuthorityPath2,
21464
+ hasCert: hasCert2,
21465
+ hasMkcert: hasMkcert2,
21466
+ ensureDevCert: ensureDevCert2,
21467
+ setupMkcert: setupMkcert2
21468
+ } = await Promise.resolve().then(() => (init_devCert(), exports_devCert));
21469
+ if (hasCert2(hosts)) {
21470
+ ensureDevCert2(hosts);
21471
+ return getDevCertificateAuthorityPath2();
20998
21472
  }
20999
21473
  if (hasMkcert2()) {
21000
- ensureDevCert2();
21001
- return;
21474
+ ensureDevCert2(hosts);
21475
+ return getDevCertificateAuthorityPath2();
21002
21476
  }
21003
- await setupCertWithPrompt(ensureDevCert2, setupMkcert2);
21477
+ await setupCertWithPrompt(ensureDevCert2, setupMkcert2, hosts);
21478
+ return getDevCertificateAuthorityPath2();
21479
+ };
21480
+ var mobileReachableHost = (host) => {
21481
+ if (host !== "0.0.0.0" && host !== "::")
21482
+ return host;
21483
+ const address = getLocalIPAddress();
21484
+ if (address === "localhost") {
21485
+ 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.");
21486
+ }
21487
+ return address;
21004
21488
  };
21005
21489
  var resolveDevConfig = (configDev) => {
21006
21490
  const relay = env.ABSOLUTE_TUNNEL_RELAY ?? configDev?.tunnel?.relay;
@@ -21014,13 +21498,24 @@ var resolveDevConfig = (configDev) => {
21014
21498
  ...relay && token ? { tunnel: { relay, token } } : {}
21015
21499
  };
21016
21500
  };
21017
- var androidToolchainReady = (checks) => checks.every((check) => check.platform !== "android" || check.status !== "fail" && check.status !== "warn");
21018
- var iosToolchainReady = (checks) => checks.every((check) => check.platform !== "ios" || check.status !== "fail" && check.status !== "warn");
21501
+ var androidToolchainReady = (checks, target = "emulator") => {
21502
+ const deviceOnlyChecks = new Set([
21503
+ "android.avd",
21504
+ "android.avdmanager",
21505
+ "android.emulator",
21506
+ "android.virtualization"
21507
+ ]);
21508
+ return checks.every((check) => check.platform !== "android" || target === "device" && deviceOnlyChecks.has(check.id) || check.status !== "fail" && check.status !== "warn");
21509
+ };
21510
+ var iosToolchainReady = (checks, target = "simulator") => checks.every((check) => check.platform !== "ios" || target === "device" && check.id === "ios.runtime" || check.status !== "fail" && check.status !== "warn");
21019
21511
  var dev = async (serverEntry, configPath2, options = {}) => {
21020
21512
  let httpsEnabled = false;
21513
+ let devCertificateAuthorityPath = null;
21021
21514
  let resolvedDev;
21022
21515
  let buildDirectory = resolve8(process.cwd(), "build");
21023
21516
  let mobileConfig;
21517
+ let iosPhysicalServerHost;
21518
+ let selectedRemoteMacProfile;
21024
21519
  try {
21025
21520
  const config = await loadConfig(configPath2);
21026
21521
  mobileConfig = config?.mobile;
@@ -21031,12 +21526,39 @@ var dev = async (serverEntry, configPath2, options = {}) => {
21031
21526
  if (config?.buildDirectory) {
21032
21527
  buildDirectory = resolve8(process.cwd(), config.buildDirectory);
21033
21528
  }
21034
- if (httpsEnabled)
21035
- await setupHttpsCert();
21036
21529
  } catch {
21037
21530
  resolvedDev = resolveDevConfig(undefined);
21038
21531
  httpsEnabled = resolvedDev.https;
21039
21532
  }
21533
+ if ((options.androidDevice || options.iosDevice && detectAbsoluteMobileHost() === "macos") && ["localhost", "127.0.0.1", "::1"].includes(resolvedDev.host)) {
21534
+ resolvedDev.host = "0.0.0.0";
21535
+ }
21536
+ if ((options.androidDevice || options.iosDevice) && !mobileConfig) {
21537
+ throw new TypeError("Physical-device development requires an absolute.config.ts mobile configuration.");
21538
+ }
21539
+ if (options.androidDevice && mobileConfig?.platforms && !mobileConfig.platforms.includes("android")) {
21540
+ throw new TypeError("--android-device requires android in mobile.platforms.");
21541
+ }
21542
+ if (options.iosDevice && mobileConfig?.platforms && !mobileConfig.platforms.includes("ios")) {
21543
+ throw new TypeError("--ios-device requires ios in mobile.platforms.");
21544
+ }
21545
+ if (options.iosDevice) {
21546
+ if (detectAbsoluteMobileHost() === "macos")
21547
+ iosPhysicalServerHost = mobileReachableHost(resolvedDev.host);
21548
+ else {
21549
+ selectedRemoteMacProfile = await getAbsoluteRemoteMacProfile();
21550
+ if (!selectedRemoteMacProfile)
21551
+ throw new Error("--ios-device requires macOS or a paired Remote Mac.");
21552
+ iosPhysicalServerHost = await inspectAbsoluteRemoteMacLanHost(selectedRemoteMacProfile);
21553
+ }
21554
+ }
21555
+ if (httpsEnabled) {
21556
+ const certificateHosts = [
21557
+ ...options.androidDevice ? [mobileReachableHost(resolvedDev.host)] : [],
21558
+ ...iosPhysicalServerHost ? [iosPhysicalServerHost] : []
21559
+ ];
21560
+ devCertificateAuthorityPath = await setupHttpsCert(certificateHosts.length > 0 ? certificateHosts : [resolvedDev.host]);
21561
+ }
21040
21562
  let androidDevProject = null;
21041
21563
  let iosDevProject = null;
21042
21564
  const mobileInteractive = options.mobile !== false && process.env.ABSOLUTE_NO_MOBILE !== "1" && process.stdin.isTTY === true && process.stdout.isTTY === true;
@@ -21044,14 +21566,15 @@ var dev = async (serverEntry, configPath2, options = {}) => {
21044
21566
  try {
21045
21567
  const normalized = normalizeAbsoluteMobileConfig(mobileConfig, process.cwd());
21046
21568
  if (normalized.platforms.includes("android")) {
21047
- let ready = androidToolchainReady(await inspectAbsoluteMobileToolchain());
21569
+ const androidTarget = options.androidDevice ? "device" : "emulator";
21570
+ let ready = androidToolchainReady(await inspectAbsoluteMobileToolchain(), androidTarget);
21048
21571
  if (!ready) {
21049
- const install = await confirmPrompt("Android emulation is not configured. Install it now?");
21572
+ const install = await confirmPrompt("Android development is not configured. Install the tested toolchain now?");
21050
21573
  if (install) {
21051
21574
  await fixAbsoluteMobileEmulatorToolchain("android");
21052
- ready = androidToolchainReady(await inspectAbsoluteMobileToolchain());
21575
+ ready = androidToolchainReady(await inspectAbsoluteMobileToolchain(), androidTarget);
21053
21576
  } else {
21054
- console.log(cliTag("\x1B[33m", "Mobile emulator skipped. Run `absolute mobile doctor android --fix` when ready."));
21577
+ console.log(cliTag("\x1B[33m", "Android target skipped. Run `absolute mobile doctor android --fix` when ready."));
21055
21578
  }
21056
21579
  }
21057
21580
  if (ready) {
@@ -21063,16 +21586,17 @@ var dev = async (serverEntry, configPath2, options = {}) => {
21063
21586
  if (existsSync5(nativeDirectory) || createNativeProject) {
21064
21587
  androidDevProject = await prepareAbsoluteAndroidDevProject(normalized, {
21065
21588
  createNativeProject,
21066
- projectRoot: process.cwd()
21589
+ projectRoot: process.cwd(),
21590
+ target: androidTarget
21067
21591
  });
21068
21592
  }
21069
21593
  }
21070
21594
  }
21071
21595
  if (normalized.platforms.includes("ios")) {
21072
21596
  if (detectAbsoluteMobileHost() !== "macos") {
21073
- const remote = await getAbsoluteRemoteMacProfile();
21597
+ const remote = selectedRemoteMacProfile ?? await getAbsoluteRemoteMacProfile();
21074
21598
  if (!remote) {
21075
- console.log(cliTag("\x1B[33m", "iOS simulator skipped. Pair a Mac with `absolute mobile pair mac <name> <user@host>`."));
21599
+ console.log(cliTag("\x1B[33m", "iOS target skipped. Pair a Mac with `absolute mobile pair mac <name> <user@host>`."));
21076
21600
  } else {
21077
21601
  const nativeDirectory = join13(normalized.nativeProjectDirectory, "ios");
21078
21602
  if (!existsSync5(nativeDirectory)) {
@@ -21083,12 +21607,13 @@ var dev = async (serverEntry, configPath2, options = {}) => {
21083
21607
  }
21084
21608
  }
21085
21609
  } else {
21086
- let ready = iosToolchainReady(await inspectAbsoluteMobileToolchain());
21610
+ const iosTarget = options.iosDevice ? "device" : "simulator";
21611
+ let ready = iosToolchainReady(await inspectAbsoluteMobileToolchain(), iosTarget);
21087
21612
  if (!ready) {
21088
- const install = await confirmPrompt("iOS simulation is not configured. Install the missing simulator runtime now?");
21613
+ const install = await confirmPrompt(options.iosDevice ? "Physical iOS development is not configured. Open the guided Xcode setup now?" : "iOS simulation is not configured. Install the missing simulator runtime now?");
21089
21614
  if (install) {
21090
21615
  await fixAbsoluteMobileEmulatorToolchain("ios");
21091
- ready = iosToolchainReady(await inspectAbsoluteMobileToolchain());
21616
+ ready = iosToolchainReady(await inspectAbsoluteMobileToolchain(), iosTarget);
21092
21617
  } else {
21093
21618
  console.log(cliTag("\x1B[33m", "Mobile simulator skipped. Run `absolute mobile doctor ios --fix` when ready."));
21094
21619
  }
@@ -21102,7 +21627,8 @@ var dev = async (serverEntry, configPath2, options = {}) => {
21102
21627
  if (existsSync5(nativeDirectory) || createNativeProject) {
21103
21628
  iosDevProject = await prepareAbsoluteIosDevProject(normalized, {
21104
21629
  createNativeProject,
21105
- projectRoot: process.cwd()
21630
+ projectRoot: process.cwd(),
21631
+ target: iosTarget
21106
21632
  });
21107
21633
  }
21108
21634
  }
@@ -21145,7 +21671,9 @@ var dev = async (serverEntry, configPath2, options = {}) => {
21145
21671
  "dev",
21146
21672
  serverEntry,
21147
21673
  ...configPath2 ? ["--config", configPath2] : [],
21148
- ...options.mobile === false ? ["--no-mobile"] : []
21674
+ ...options.mobile === false ? ["--no-mobile"] : [],
21675
+ ...options.androidDevice ? ["--android-device", options.androidDevice] : [],
21676
+ ...options.iosDevice ? ["--ios-device", options.iosDevice] : []
21149
21677
  ].filter((part) => part.length > 0);
21150
21678
  registerInstance({
21151
21679
  command: relaunchCommand,
@@ -21201,6 +21729,7 @@ var dev = async (serverEntry, configPath2, options = {}) => {
21201
21729
  platform: "android",
21202
21730
  provider: "capacitor",
21203
21731
  startedEmulator: session.startedEmulator,
21732
+ target: options.androidDevice ? "device" : "emulator",
21204
21733
  timings: session.timings
21205
21734
  });
21206
21735
  if (session.timings.building === undefined)
@@ -21215,9 +21744,12 @@ var dev = async (serverEntry, configPath2, options = {}) => {
21215
21744
  });
21216
21745
  };
21217
21746
  const openAndroidDevSession = (androidProject) => startAbsoluteAndroidDevSession({
21747
+ certificateAuthorityPath: devCertificateAuthorityPath ?? undefined,
21748
+ deviceSerial: options.androidDevice,
21218
21749
  https: httpsEnabled,
21219
21750
  port,
21220
21751
  project: androidProject,
21752
+ serverHost: options.androidDevice ? mobileReachableHost(resolvedDev.host) : "localhost",
21221
21753
  signal: androidDevAbort.signal,
21222
21754
  log: (message) => printNativeOutput(cliTag("\x1B[36m", message)),
21223
21755
  nativeLog: (entry) => printNativeOutput(androidLogTag(entry)),
@@ -21313,6 +21845,7 @@ var dev = async (serverEntry, configPath2, options = {}) => {
21313
21845
  platform: "ios",
21314
21846
  provider: "capacitor",
21315
21847
  startedSimulator: session.startedSimulator,
21848
+ target: session.targetKind,
21316
21849
  timings: session.timings
21317
21850
  });
21318
21851
  if (session.timings.building === undefined)
@@ -21323,13 +21856,17 @@ var dev = async (serverEntry, configPath2, options = {}) => {
21323
21856
  installMs: session.timings.installing,
21324
21857
  platform: "ios",
21325
21858
  provider: "capacitor",
21326
- success: true
21859
+ success: true,
21860
+ target: session.targetKind
21327
21861
  });
21328
21862
  };
21329
21863
  const openIosDevSession = (iosProject) => {
21330
21864
  const sessionOptions = {
21865
+ certificateAuthorityPath: devCertificateAuthorityPath ?? undefined,
21866
+ deviceIdentifier: options.iosDevice,
21331
21867
  https: httpsEnabled,
21332
21868
  port,
21869
+ serverHost: iosPhysicalServerHost,
21333
21870
  signal: iosDevAbort.signal,
21334
21871
  log: (message) => printNativeOutput(cliTag("\x1B[35m", message)),
21335
21872
  nativeLog: (entry) => printNativeOutput(iosLogTag(entry)),
@@ -21343,7 +21880,7 @@ var dev = async (serverEntry, configPath2, options = {}) => {
21343
21880
  iosDevState = state;
21344
21881
  if (state === "ready" || state === "closed")
21345
21882
  return;
21346
- printNativeOutput(cliTag("\x1B[35m", `iOS simulator: ${state}.`));
21883
+ printNativeOutput(cliTag("\x1B[35m", `iOS ${options.iosDevice ? "device" : "simulator"}: ${state}.`));
21347
21884
  }
21348
21885
  };
21349
21886
  if (iosProject.remote)
@@ -21382,6 +21919,7 @@ var dev = async (serverEntry, configPath2, options = {}) => {
21382
21919
  provider: "capacitor",
21383
21920
  rootInputChanged: change.rootInputChanged,
21384
21921
  success: true,
21922
+ target: replacement.targetKind,
21385
21923
  timings: replacement.timings
21386
21924
  });
21387
21925
  },
@@ -21392,7 +21930,8 @@ var dev = async (serverEntry, configPath2, options = {}) => {
21392
21930
  host: iosTelemetryHost(iosProject),
21393
21931
  platform: "ios",
21394
21932
  provider: "capacitor",
21395
- success: false
21933
+ success: false,
21934
+ target: options.iosDevice ? "device" : "simulator"
21396
21935
  });
21397
21936
  }
21398
21937
  });
@@ -21419,9 +21958,10 @@ var dev = async (serverEntry, configPath2, options = {}) => {
21419
21958
  phase: iosDevState,
21420
21959
  platform: "ios",
21421
21960
  provider: "capacitor",
21961
+ target: options.iosDevice ? "device" : "simulator",
21422
21962
  timings: iosPhaseTimings
21423
21963
  });
21424
- console.error(cliTag("\x1B[31m", `iOS simulator failed: ${error instanceof Error ? error.message : String(error)}`));
21964
+ console.error(cliTag("\x1B[31m", `iOS ${options.iosDevice ? "device" : "simulator"} failed: ${error instanceof Error ? error.message : String(error)}`));
21425
21965
  }).finally(() => {
21426
21966
  iosDevStart = null;
21427
21967
  });
@@ -21488,6 +22028,9 @@ var dev = async (serverEntry, configPath2, options = {}) => {
21488
22028
  return;
21489
22029
  }
21490
22030
  const refreshedDevConfig = resolveDevConfig(cfg?.dev);
22031
+ if (options.androidDevice && ["localhost", "127.0.0.1", "::1"].includes(refreshedDevConfig.host)) {
22032
+ refreshedDevConfig.host = "0.0.0.0";
22033
+ }
21491
22034
  const desiredBuildDir = cfg?.buildDirectory ? resolve8(process.cwd(), cfg.buildDirectory) : resolve8(process.cwd(), "build");
21492
22035
  if (desiredBuildDir !== buildDirectory && desiredBuildDir !== lastBuildDirectoryWarned) {
21493
22036
  lastBuildDirectoryWarned = desiredBuildDir;
@@ -21570,6 +22113,7 @@ var dev = async (serverEntry, configPath2, options = {}) => {
21570
22113
  env: {
21571
22114
  ...process.env,
21572
22115
  ...readDotenvFiles(),
22116
+ ABSOLUTE_HOST: resolvedDev.host,
21573
22117
  ABSOLUTE_INSTANCE_MANAGED: "1",
21574
22118
  ABSOLUTE_PORT: String(port),
21575
22119
  ABSOLUTE_SERVER_ENTRY: resolve8(serverEntry),
@@ -23766,9 +24310,25 @@ var stripNamedArgs = (...flags) => args.filter((_, idx) => flags.every((flag) =>
23766
24310
  if (command === "dev") {
23767
24311
  sendTelemetryEvent("cli:command", { command });
23768
24312
  const configPath2 = parseNamedArg("--config");
23769
- const positionalArgs2 = stripNamedArgs("--config").filter((arg) => arg !== "--no-mobile");
24313
+ const androidDevice = parseNamedArg("--android-device");
24314
+ const iosDevice = parseNamedArg("--ios-device");
24315
+ if (args.includes("--android-device") && !androidDevice) {
24316
+ throw new TypeError("--android-device requires an ADB device serial.");
24317
+ }
24318
+ if (androidDevice && args.includes("--no-mobile")) {
24319
+ throw new TypeError("--android-device cannot be combined with --no-mobile.");
24320
+ }
24321
+ if (args.includes("--ios-device") && !iosDevice) {
24322
+ throw new TypeError("--ios-device requires an Xcode device identifier or name.");
24323
+ }
24324
+ if (iosDevice && args.includes("--no-mobile")) {
24325
+ throw new TypeError("--ios-device cannot be combined with --no-mobile.");
24326
+ }
24327
+ const positionalArgs2 = stripNamedArgs("--config", "--android-device", "--ios-device").filter((arg) => arg !== "--no-mobile");
23770
24328
  const serverEntry = positionalArgs2[0] ?? DEFAULT_SERVER_ENTRY;
23771
24329
  await dev(serverEntry, configPath2, {
24330
+ androidDevice,
24331
+ iosDevice,
23772
24332
  mobile: !args.includes("--no-mobile")
23773
24333
  });
23774
24334
  } else if (command === "start") {
@@ -23927,13 +24487,13 @@ if (command === "dev") {
23927
24487
  console.error(message);
23928
24488
  console.error("Usage: absolute <command>");
23929
24489
  console.error("Commands:");
23930
- console.error(" dev [entry] [--no-mobile] Start web and configured mobile development");
24490
+ console.error(" dev [entry] [--no-mobile] [--android-device serial] [--ios-device identifier] Start web and configured mobile development");
23931
24491
  console.error(" workspace dev [--no-tui] Start multi-service workspace dev");
23932
24492
  console.error(" build [--outdir dir] [--profile] Build production assets");
23933
24493
  console.error(" prepare [entry] [--outdir dir] Build production assets and server without launching");
23934
24494
  console.error(" start [entry] [--outdir dir] [--prebuilt] Start production server");
23935
24495
  console.error(" compile [entry] [--outdir dir] [--outfile path] Compile standalone executable");
23936
- console.error(" mobile <init|sync|pair|remotes|doctor|test> Manage Capacitor projects, local or remote simulators, guided setup, and deep links");
24496
+ console.error(" mobile <init|sync|pair|remotes|doctor|test> Manage Capacitor projects, simulators, physical devices, Remote Macs, guided setup, and deep links");
23937
24497
  console.error(" config [--port n] Open the unified config UI (ESLint, tsconfig, Prettier)");
23938
24498
  console.error(" db <backup|restore|seed> Backup/restore any Postgres DB (ORM-agnostic, upsert by PK) or run the seed script");
23939
24499
  console.error(" doctor [--fix] [--json] Diagnose the project (bun, type graph, config, framework dirs, env, port)");