@livx.cc/appwrap 0.25.1 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli.ts +40 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livx.cc/appwrap",
3
- "version": "0.25.1",
3
+ "version": "0.26.0",
4
4
  "description": "Wrap any PWA into a native app with native capabilities (appwrap runtime + @livx.cc/native-kit).",
5
5
  "license": "MIT",
6
6
  "author": "Elya Livshitz",
package/src/cli.ts CHANGED
@@ -1181,6 +1181,7 @@ async function deploy(cwd: string, flags: Record<string, string>, positionals: s
1181
1181
  const ipaPath = join(ipaDir, ipa);
1182
1182
 
1183
1183
  console.log(`▶ installing ${ipa} → ${device.name} [${device.transport}]`);
1184
+ let installedViaUsbmux = false;
1184
1185
  try {
1185
1186
  // Capture (not inherit) so we can recognize specific failures; echo it for visibility.
1186
1187
  // Wrapped so a LOCKED device waits-and-retries instead of hard-failing (the common annoyance).
@@ -1204,17 +1205,34 @@ async function deploy(cwd: string, flags: Record<string, string>, positionals: s
1204
1205
  ` xcrun devicectl device uninstall app --device ${device.id} <bundleId>\n` +
1205
1206
  ' (A paid Apple Developer account removes this limit.)'
1206
1207
  );
1208
+ process.exit(1);
1209
+ } else if (/Command timeout|got stuck|could not be reached|Unable to connect to device/.test(log)) {
1210
+ // devicectl/CoreDevice is stuck (a connection hang, NOT a lock). usbmux (ideviceinstaller) is a
1211
+ // separate stack that usually still works — auto-fall-back instead of chasing a phantom "unlock".
1212
+ console.error('\n⚠ devicectl/CoreDevice is stuck (connection hang, not a lock) — falling back to ideviceinstaller (usbmux)…');
1213
+ if (usbmuxInstall(ipaPath)) {
1214
+ installedViaUsbmux = true;
1215
+ } else {
1216
+ console.error(
1217
+ '✖ usbmux fallback unavailable.\n' +
1218
+ ' → Re-plug the USB cable (re-establishes the CoreDevice tunnel) and re-run, OR\n' +
1219
+ ' `brew install ideviceinstaller` for a usbmux install path.\n' +
1220
+ ` The built .ipa is ready: ${ipaPath}`
1221
+ );
1222
+ process.exit(1);
1223
+ }
1207
1224
  } else {
1208
1225
  console.error(
1209
1226
  '✖ Install failed (device still locked after waiting, or only on Wi-Fi).\n' +
1210
1227
  ' → Unlock the phone (and plug in USB for a reliable connection), then re-run.\n' +
1211
1228
  ` The built .ipa is ready: ${ipaPath}`
1212
1229
  );
1230
+ process.exit(1);
1213
1231
  }
1214
- process.exit(1);
1215
1232
  }
1216
1233
 
1217
- if (!('no-launch' in flags)) {
1234
+ // devicectl process-launch is also stuck when we fell back to usbmux — skip it; the user taps the icon.
1235
+ if (!('no-launch' in flags) && !installedViaUsbmux) {
1218
1236
  console.log(`▶ launching ${cfg.id}`);
1219
1237
  try {
1220
1238
  withUnlockRetry('Launch', () =>
@@ -1224,7 +1242,23 @@ async function deploy(cwd: string, flags: Record<string, string>, positionals: s
1224
1242
  console.error('⚠ Launch failed (still locked after waiting). The app is installed — unlock and tap it, or re-run.');
1225
1243
  }
1226
1244
  }
1227
- console.log(`✓ Deployed to ${device.name}.`);
1245
+ console.log(installedViaUsbmux
1246
+ ? `✓ Installed to ${device.name} via usbmux (devicectl was stuck). Tap the app icon to open it.`
1247
+ : `✓ Deployed to ${device.name}.`);
1248
+ }
1249
+
1250
+ /** Install an .ipa over usbmux via ideviceinstaller — a separate stack from devicectl/CoreDevice, so it
1251
+ * works when the CoreDevice tunnel is stuck. Returns false if ideviceinstaller is absent or the install
1252
+ * fails (the caller then prints the re-plug / brew-install remedy). */
1253
+ function usbmuxInstall(ipaPath: string): boolean {
1254
+ try {
1255
+ const out = execFileSync('ideviceinstaller', ['install', ipaPath], { encoding: 'utf8', stdio: ['inherit', 'pipe', 'pipe'] });
1256
+ process.stdout.write(out);
1257
+ return /Complete|Installed/i.test(out);
1258
+ } catch (e: any) {
1259
+ process.stderr.write(`${e?.stdout ?? ''}${e?.stderr ?? ''}`);
1260
+ return false;
1261
+ }
1228
1262
  }
1229
1263
 
1230
1264
  /** Run a devicectl op; if it fails because the device is LOCKED (or transiently unavailable), prompt
@@ -1238,6 +1272,9 @@ function withUnlockRetry<T>(label: string, run: () => T, tries = 40, delayMs = 3
1238
1272
  } catch (e: any) {
1239
1273
  const log = `${e?.stdout ?? ''}${e?.stderr ?? ''}`;
1240
1274
  if (/maximum number of installed apps|MIInstallerErrorDomain error 13|ApplicationVerificationFailed/.test(log)) throw e;
1275
+ // devicectl/CoreDevice tunnel STUCK (it switches to a [wired] path and hangs) is NOT a lock —
1276
+ // retrying as "waiting for unlock" is pointless + misleading. Re-throw so the caller can fall back.
1277
+ if (/Command timeout|got stuck|could not be reached|Unable to connect to device/.test(log)) throw e;
1241
1278
  if (i >= tries) throw e;
1242
1279
  if (i === 0) process.stdout.write(`\n🔒 ${label}: device unavailable — unlock your iPhone. Waiting (auto-retries every ${delayMs / 1000}s, up to ${Math.round((tries * delayMs) / 1000)}s)…\n`);
1243
1280
  else process.stdout.write(` …waiting for unlock (${i}/${tries})\n`);