@livx.cc/appwrap 0.25.0 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livx.cc/appwrap",
3
- "version": "0.25.0",
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",
@@ -166,6 +166,18 @@ export function registerPushHandlers(): void {
166
166
  if (isAndroid) return androidDeleteToken();
167
167
  });
168
168
 
169
+ // Dev/demo: ask the backend to push THIS device on demand. Goes native (NS Http) → no app:// CORS
170
+ // wall. Requires push.registrationUrl + a registered token; the backend honors `test:true`.
171
+ bridge.register('push.sendTest', () => {
172
+ const url = SHELL_CONFIG.pushRegistrationUrl;
173
+ if (!url) return Promise.reject(Object.assign(new Error('push.registrationUrl not configured'), { code: 'UNSUPPORTED' }));
174
+ if (!cachedToken) return Promise.reject(Object.assign(new Error('no device token yet — register first'), { code: 'NOT_READY' }));
175
+ return Http.request({
176
+ url, method: 'POST', headers: { 'Content-Type': 'application/json' },
177
+ content: JSON.stringify({ token: cachedToken, platform: isIOS ? 'ios' : 'android', test: true }),
178
+ }).then((res) => ({ status: res.statusCode }));
179
+ });
180
+
169
181
  // Debug: auto-acquire + log the FCM token at boot so it's readable HEADLESSLY (adb logcat / appwrap
170
182
  // logs) without a UI tap — the dev loop needs the token to send a test push. Android only (iOS push
171
183
  // is off on personal teams); no-op when push isn't configured for this platform.
@@ -230,8 +242,11 @@ function androidGetToken(): Promise<PushToken> {
230
242
  const task = fcm().getToken();
231
243
  task.addOnCompleteListener(new OnCompleteListener({
232
244
  onComplete(t: any) {
233
- if (t.isSuccessful()) resolve({ platform: 'fcm', token: String(t.getResult()) });
234
- else reject(Object.assign(new Error('FCM token fetch failed'), { code: 'NATIVE_ERROR' }));
245
+ if (t.isSuccessful()) {
246
+ const tok = String(t.getResult());
247
+ registerTokenWithBackend('android', tok); // parity with iOS onApnsToken
248
+ resolve({ platform: 'fcm', token: tok });
249
+ } else reject(Object.assign(new Error('FCM token fetch failed'), { code: 'NATIVE_ERROR' }));
235
250
  },
236
251
  }));
237
252
  } catch (e: any) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "appwrap-shell",
3
3
  "main": "app/app.ts",
4
- "version": "0.28.0",
4
+ "version": "0.29.0",
5
5
  "private": true,
6
6
  "scripts": {
7
7
  "dev:ios": "ns run ios",
package/src/cli.ts CHANGED
@@ -409,7 +409,7 @@ function stampIOSDisplayName(outDir: string, cfg: AppwrapConfig, req: NativeReqs
409
409
  }
410
410
  if (cfg.urlScheme) {
411
411
  extras.push(
412
- ` <key>CFBundleURLTypes</key>\n <array>\n <dict>\n <key>CFBundleURLName</key>\n <string>${cfg.id}</string>\n <key>CFBundleURLSchemes</key>\n <array>\n <string>${cfg.urlScheme}</string>\n </array>\n </dict>\n </array>`
412
+ ` <key>CFBundleURLTypes</key>\n <array>\n <dict>\n <key>CFBundleTypeRole</key>\n <string>Editor</string>\n <key>CFBundleURLName</key>\n <string>${cfg.id}</string>\n <key>CFBundleURLSchemes</key>\n <array>\n <string>${cfg.urlScheme}</string>\n </array>\n </dict>\n </array>`
413
413
  );
414
414
  }
415
415
  if (extras.length) {
@@ -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`);