@tamer4lynx/cli 0.0.9 → 0.0.12

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 (3) hide show
  1. package/README.md +45 -30
  2. package/dist/index.js +1911 -1356
  3. package/package.json +2 -4
package/dist/index.js CHANGED
@@ -7,16 +7,14 @@ process.on("warning", (w) => {
7
7
  });
8
8
 
9
9
  // index.ts
10
- import fs23 from "fs";
11
- import path24 from "path";
10
+ import fs24 from "fs";
11
+ import path25 from "path";
12
+ import { fileURLToPath } from "url";
12
13
  import { program } from "commander";
13
14
 
14
- // package.json
15
- var version = "0.0.9";
16
-
17
15
  // src/android/create.ts
18
- import fs3 from "fs";
19
- import path3 from "path";
16
+ import fs4 from "fs";
17
+ import path4 from "path";
20
18
  import os2 from "os";
21
19
 
22
20
  // src/android/getGradle.ts
@@ -297,6 +295,55 @@ function loadHostConfig(cwd = process.cwd()) {
297
295
  if (!cfg) throw new Error("tamer.config.json not found in the project root.");
298
296
  return cfg;
299
297
  }
298
+ function formatAdaptiveInsetValue(v, fallback) {
299
+ if (v === void 0) return fallback;
300
+ if (typeof v === "number") {
301
+ if (!Number.isFinite(v) || v < 0 || v > 50) return fallback;
302
+ return `${v}%`;
303
+ }
304
+ const s = String(v).trim();
305
+ if (s.endsWith("%") || s.endsWith("dp")) return s;
306
+ if (/^\d+(\.\d+)?$/.test(s)) return `${s}%`;
307
+ return fallback;
308
+ }
309
+ function resolveAdaptiveForegroundLayoutFromConfig(ad) {
310
+ const hasLayoutOpt = ad.foregroundScale != null || ad.foregroundPadding != null;
311
+ if (!hasLayoutOpt) return void 0;
312
+ let scale = ad.foregroundScale;
313
+ if (scale != null && typeof scale === "number") {
314
+ if (!Number.isFinite(scale)) scale = void 0;
315
+ else scale = Math.min(1, Math.max(0.05, scale));
316
+ }
317
+ let padding;
318
+ if (ad.foregroundPadding != null) {
319
+ const pad = ad.foregroundPadding;
320
+ if (typeof pad === "number" || typeof pad === "string") {
321
+ const u = formatAdaptiveInsetValue(pad, "0%");
322
+ padding = { left: u, top: u, right: u, bottom: u };
323
+ } else {
324
+ const d = "0%";
325
+ padding = {
326
+ left: formatAdaptiveInsetValue(pad.left, d),
327
+ top: formatAdaptiveInsetValue(pad.top, d),
328
+ right: formatAdaptiveInsetValue(pad.right, d),
329
+ bottom: formatAdaptiveInsetValue(pad.bottom, d)
330
+ };
331
+ }
332
+ }
333
+ return { scale, padding };
334
+ }
335
+ function normalizeAndroidAdaptiveColor(input) {
336
+ const raw = input.trim().replace(/^#/, "");
337
+ if (!/^[0-9a-fA-F]{3}$|^[0-9a-fA-F]{6}$|^[0-9a-fA-F]{8}$/.test(raw)) return null;
338
+ let h = raw;
339
+ if (h.length === 3) {
340
+ h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
341
+ }
342
+ if (h.length === 6) {
343
+ h = "FF" + h;
344
+ }
345
+ return `#${h.toUpperCase()}`;
346
+ }
300
347
  function resolveIconPaths(projectRoot, config) {
301
348
  const raw = config.icon;
302
349
  if (!raw) return null;
@@ -318,9 +365,225 @@ function resolveIconPaths(projectRoot, config) {
318
365
  const p = join(raw.ios);
319
366
  if (fs2.existsSync(p)) out.ios = p;
320
367
  }
368
+ const ad = raw.androidAdaptive;
369
+ if (ad?.foreground) {
370
+ const fg = join(ad.foreground);
371
+ if (fs2.existsSync(fg)) {
372
+ let usedAdaptive = false;
373
+ if (ad.background) {
374
+ const bg = join(ad.background);
375
+ if (fs2.existsSync(bg)) {
376
+ out.androidAdaptiveForeground = fg;
377
+ out.androidAdaptiveBackground = bg;
378
+ usedAdaptive = true;
379
+ }
380
+ }
381
+ if (!usedAdaptive && ad.backgroundColor) {
382
+ const norm = normalizeAndroidAdaptiveColor(ad.backgroundColor);
383
+ if (norm) {
384
+ out.androidAdaptiveForeground = fg;
385
+ out.androidAdaptiveBackgroundColor = norm;
386
+ }
387
+ }
388
+ if (out.androidAdaptiveForeground) {
389
+ const lay = resolveAdaptiveForegroundLayoutFromConfig(ad);
390
+ if (lay) out.androidAdaptiveForegroundLayout = lay;
391
+ }
392
+ }
393
+ }
321
394
  return Object.keys(out).length ? out : null;
322
395
  }
323
396
 
397
+ // src/common/syncAppIcons.ts
398
+ import fs3 from "fs";
399
+ import path3 from "path";
400
+ function purgeAdaptiveForegroundArtifacts(drawableDir) {
401
+ for (const base of ["ic_launcher_fg_src", "ic_launcher_fg_bm", "ic_launcher_fg_sc"]) {
402
+ for (const ext of [".png", ".webp", ".jpg", ".jpeg", ".xml"]) {
403
+ try {
404
+ fs3.unlinkSync(path3.join(drawableDir, base + ext));
405
+ } catch {
406
+ }
407
+ }
408
+ }
409
+ try {
410
+ fs3.unlinkSync(path3.join(drawableDir, "ic_launcher_foreground.xml"));
411
+ } catch {
412
+ }
413
+ }
414
+ function parsePadDp(v) {
415
+ if (v.endsWith("dp")) return Math.max(0, Math.min(54, parseFloat(v)));
416
+ if (v.endsWith("%")) return Math.max(0, Math.min(54, parseFloat(v) / 100 * 108));
417
+ return 0;
418
+ }
419
+ function writeAdaptiveForegroundLayer(drawableDir, fgSourcePath, fgExt, layout) {
420
+ purgeAdaptiveForegroundArtifacts(drawableDir);
421
+ for (const ext of [".png", ".webp", ".jpg", ".jpeg"]) {
422
+ try {
423
+ fs3.unlinkSync(path3.join(drawableDir, `ic_launcher_foreground${ext}`));
424
+ } catch {
425
+ }
426
+ }
427
+ if (!layout) {
428
+ fs3.copyFileSync(fgSourcePath, path3.join(drawableDir, `ic_launcher_foreground${fgExt}`));
429
+ return;
430
+ }
431
+ fs3.copyFileSync(fgSourcePath, path3.join(drawableDir, `ic_launcher_fg_src${fgExt}`));
432
+ const CANVAS_DP = 108;
433
+ const scale = layout.scale ?? 1;
434
+ const shrinkDp = (1 - scale) / 2 * CANVAS_DP;
435
+ const pad = layout.padding;
436
+ const parsedPad = {
437
+ left: pad ? parsePadDp(pad.left) : 0,
438
+ top: pad ? parsePadDp(pad.top) : 0,
439
+ right: pad ? parsePadDp(pad.right) : 0,
440
+ bottom: pad ? parsePadDp(pad.bottom) : 0
441
+ };
442
+ const insetLeft = Math.round(shrinkDp + parsedPad.left);
443
+ const insetTop = Math.round(shrinkDp + parsedPad.top);
444
+ const insetRight = Math.round(shrinkDp + parsedPad.right);
445
+ const insetBottom = Math.round(shrinkDp + parsedPad.bottom);
446
+ const layerXml = `<?xml version="1.0" encoding="utf-8"?>
447
+ <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
448
+ <item
449
+ android:left="${insetLeft}dp"
450
+ android:top="${insetTop}dp"
451
+ android:right="${insetRight}dp"
452
+ android:bottom="${insetBottom}dp"
453
+ android:drawable="@drawable/ic_launcher_fg_src" />
454
+ </layer-list>`;
455
+ fs3.writeFileSync(path3.join(drawableDir, "ic_launcher_foreground.xml"), layerXml);
456
+ }
457
+ function ensureAndroidManifestLauncherIcon(manifestPath) {
458
+ if (!fs3.existsSync(manifestPath)) return;
459
+ let content = fs3.readFileSync(manifestPath, "utf8");
460
+ if (content.includes("android:icon=")) return;
461
+ const next = content.replace(/<application(\s[^>]*)>/, (full, attrs) => {
462
+ if (String(attrs).includes("android:icon")) return full;
463
+ return `<application${attrs}
464
+ android:icon="@mipmap/ic_launcher"
465
+ android:roundIcon="@mipmap/ic_launcher">`;
466
+ });
467
+ if (next !== content) {
468
+ fs3.writeFileSync(manifestPath, next, "utf8");
469
+ console.log("\u2705 Added android:icon / roundIcon to AndroidManifest.xml");
470
+ }
471
+ }
472
+ function applyAndroidLauncherIcons(resDir, iconPaths) {
473
+ if (!iconPaths) return false;
474
+ fs3.mkdirSync(resDir, { recursive: true });
475
+ const fgAd = iconPaths.androidAdaptiveForeground;
476
+ const bgAd = iconPaths.androidAdaptiveBackground;
477
+ const bgColor = iconPaths.androidAdaptiveBackgroundColor;
478
+ if (fgAd && (bgAd || bgColor)) {
479
+ const drawableDir = path3.join(resDir, "drawable");
480
+ fs3.mkdirSync(drawableDir, { recursive: true });
481
+ const fgExt = path3.extname(fgAd) || ".png";
482
+ writeAdaptiveForegroundLayer(drawableDir, fgAd, fgExt, iconPaths.androidAdaptiveForegroundLayout);
483
+ if (bgColor) {
484
+ for (const ext of [".png", ".webp", ".jpg", ".jpeg"]) {
485
+ try {
486
+ fs3.unlinkSync(path3.join(drawableDir, `ic_launcher_background${ext}`));
487
+ } catch {
488
+ }
489
+ }
490
+ try {
491
+ fs3.unlinkSync(path3.join(drawableDir, "ic_launcher_background.xml"));
492
+ } catch {
493
+ }
494
+ const shapeXml = `<?xml version="1.0" encoding="utf-8"?>
495
+ <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
496
+ <solid android:color="${bgColor}" />
497
+ </shape>
498
+ `;
499
+ fs3.writeFileSync(path3.join(drawableDir, "ic_launcher_background.xml"), shapeXml);
500
+ } else {
501
+ try {
502
+ fs3.unlinkSync(path3.join(drawableDir, "ic_launcher_background.xml"));
503
+ } catch {
504
+ }
505
+ const bgExt = path3.extname(bgAd) || ".png";
506
+ fs3.copyFileSync(bgAd, path3.join(drawableDir, `ic_launcher_background${bgExt}`));
507
+ }
508
+ const anyDpi = path3.join(resDir, "mipmap-anydpi-v26");
509
+ fs3.mkdirSync(anyDpi, { recursive: true });
510
+ const adaptiveXml = `<?xml version="1.0" encoding="utf-8"?>
511
+ <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
512
+ <background android:drawable="@drawable/ic_launcher_background" />
513
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
514
+ </adaptive-icon>
515
+ `;
516
+ fs3.writeFileSync(path3.join(anyDpi, "ic_launcher.xml"), adaptiveXml);
517
+ fs3.writeFileSync(path3.join(anyDpi, "ic_launcher_round.xml"), adaptiveXml);
518
+ const legacySrc = iconPaths.source ?? fgAd;
519
+ const legacyExt = path3.extname(legacySrc) || ".png";
520
+ const mipmapDensities = ["mdpi", "hdpi", "xhdpi", "xxhdpi", "xxxhdpi"];
521
+ for (const d of mipmapDensities) {
522
+ const dir = path3.join(resDir, `mipmap-${d}`);
523
+ fs3.mkdirSync(dir, { recursive: true });
524
+ fs3.copyFileSync(legacySrc, path3.join(dir, `ic_launcher${legacyExt}`));
525
+ }
526
+ return true;
527
+ }
528
+ if (iconPaths.android) {
529
+ const src = iconPaths.android;
530
+ const entries = fs3.readdirSync(src, { withFileTypes: true });
531
+ for (const e of entries) {
532
+ const dest = path3.join(resDir, e.name);
533
+ if (e.isDirectory()) {
534
+ fs3.cpSync(path3.join(src, e.name), dest, { recursive: true });
535
+ } else {
536
+ fs3.copyFileSync(path3.join(src, e.name), dest);
537
+ }
538
+ }
539
+ return true;
540
+ }
541
+ if (iconPaths.source) {
542
+ const mipmapDensities = ["mdpi", "hdpi", "xhdpi", "xxhdpi", "xxxhdpi"];
543
+ for (const d of mipmapDensities) {
544
+ const dir = path3.join(resDir, `mipmap-${d}`);
545
+ fs3.mkdirSync(dir, { recursive: true });
546
+ fs3.copyFileSync(iconPaths.source, path3.join(dir, "ic_launcher.png"));
547
+ }
548
+ return true;
549
+ }
550
+ return false;
551
+ }
552
+ function applyIosAppIconAssets(appIconDir, iconPaths) {
553
+ if (!iconPaths) return false;
554
+ fs3.mkdirSync(appIconDir, { recursive: true });
555
+ if (iconPaths.ios) {
556
+ const entries = fs3.readdirSync(iconPaths.ios, { withFileTypes: true });
557
+ for (const e of entries) {
558
+ const dest = path3.join(appIconDir, e.name);
559
+ if (e.isDirectory()) {
560
+ fs3.cpSync(path3.join(iconPaths.ios, e.name), dest, { recursive: true });
561
+ } else {
562
+ fs3.copyFileSync(path3.join(iconPaths.ios, e.name), dest);
563
+ }
564
+ }
565
+ return true;
566
+ }
567
+ if (iconPaths.source) {
568
+ const ext = path3.extname(iconPaths.source) || ".png";
569
+ const icon1024 = `Icon-1024${ext}`;
570
+ fs3.copyFileSync(iconPaths.source, path3.join(appIconDir, icon1024));
571
+ fs3.writeFileSync(
572
+ path3.join(appIconDir, "Contents.json"),
573
+ JSON.stringify(
574
+ {
575
+ images: [{ filename: icon1024, idiom: "universal", platform: "ios", size: "1024x1024" }],
576
+ info: { author: "xcode", version: 1 }
577
+ },
578
+ null,
579
+ 2
580
+ )
581
+ );
582
+ return true;
583
+ }
584
+ return false;
585
+ }
586
+
324
587
  // src/explorer/ref.ts
325
588
  var LYNX_RAW_BASE = "https://raw.githubusercontent.com/lynx-family/lynx/develop/explorer";
326
589
  async function fetchExplorerFile(relativePath) {
@@ -573,6 +836,7 @@ function getProjectActivity(vars) {
573
836
  ` : "";
574
837
  const devClientImports = hasDevClient ? `
575
838
  import ${vars.packageName}.DevClientManager
839
+ import com.nanofuxion.tamerdevclient.DevClientModule
576
840
  import com.nanofuxion.tamerdevclient.TamerRelogLogService` : "";
577
841
  const reloadMethod = hasDevClient ? `
578
842
  private fun reloadProjectView() {
@@ -585,7 +849,7 @@ import com.nanofuxion.tamerdevclient.TamerRelogLogService` : "";
585
849
  setContentView(nextView)
586
850
  GeneratedActivityLifecycle.onViewAttached(nextView)
587
851
  GeneratedLynxExtensions.onHostViewChanged(nextView)
588
- nextView.renderTemplateUrl("main.lynx.bundle", "")
852
+ nextView.renderTemplateUrl("main.lynx.bundle", DevClientModule.getProjectInitDataJson(this))
589
853
  GeneratedActivityLifecycle.onCreateDelayed(handler)
590
854
  }
591
855
  ` : "";
@@ -595,9 +859,6 @@ import android.content.Intent
595
859
  import android.os.Bundle
596
860
  import android.os.Handler
597
861
  import android.os.Looper
598
- import android.view.MotionEvent
599
- import android.view.inputmethod.InputMethodManager
600
- import android.widget.EditText
601
862
  import androidx.appcompat.app.AppCompatActivity
602
863
  import androidx.core.view.WindowCompat
603
864
  import androidx.core.view.WindowInsetsControllerCompat
@@ -619,7 +880,7 @@ ${devClientField} private val handler = Handler(Looper.getMainLooper())
619
880
  setContentView(lynxView)
620
881
  GeneratedActivityLifecycle.onViewAttached(lynxView)
621
882
  GeneratedLynxExtensions.onHostViewChanged(lynxView)
622
- lynxView?.renderTemplateUrl("main.lynx.bundle", "")${devClientInit}
883
+ lynxView?.renderTemplateUrl("main.lynx.bundle", ${hasDevClient ? "DevClientModule.getProjectInitDataJson(this)" : '""'})${devClientInit}
623
884
  GeneratedActivityLifecycle.onCreateDelayed(handler)
624
885
  }
625
886
 
@@ -633,26 +894,6 @@ ${reloadMethod}
633
894
  GeneratedActivityLifecycle.onResume()
634
895
  }
635
896
 
636
- override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
637
- if (ev.action == MotionEvent.ACTION_DOWN) maybeClearFocusedInput(ev)
638
- return super.dispatchTouchEvent(ev)
639
- }
640
-
641
- private fun maybeClearFocusedInput(ev: MotionEvent) {
642
- val focused = currentFocus
643
- if (focused is EditText) {
644
- val loc = IntArray(2)
645
- focused.getLocationOnScreen(loc)
646
- val x = ev.rawX.toInt()
647
- val y = ev.rawY.toInt()
648
- if (x < loc[0] || x > loc[0] + focused.width || y < loc[1] || y > loc[1] + focused.height) {
649
- focused.clearFocus()
650
- (getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager)
651
- ?.hideSoftInputFromWindow(focused.windowToken, 0)
652
- }
653
- }
654
- }
655
-
656
897
  override fun onNewIntent(intent: Intent) {
657
898
  super.onNewIntent(intent)
658
899
  setIntent(intent)
@@ -778,9 +1019,6 @@ import com.nanofuxion.tamerdevclient.DevClientModule
778
1019
 
779
1020
  import android.os.Build
780
1021
  import android.os.Bundle
781
- import android.view.MotionEvent
782
- import android.view.inputmethod.InputMethodManager
783
- import android.widget.EditText
784
1022
  import androidx.appcompat.app.AppCompatActivity
785
1023
  import androidx.core.view.WindowCompat
786
1024
  import androidx.core.view.WindowInsetsControllerCompat
@@ -813,26 +1051,6 @@ ${devClientField} private var lynxView: LynxView? = null${!hasDevClient ? "\n
813
1051
  GeneratedActivityLifecycle.onResume()
814
1052
  }
815
1053
 
816
- override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
817
- if (ev.action == MotionEvent.ACTION_DOWN) maybeClearFocusedInput(ev)
818
- return super.dispatchTouchEvent(ev)
819
- }
820
-
821
- private fun maybeClearFocusedInput(ev: MotionEvent) {
822
- val focused = currentFocus
823
- if (focused is EditText) {
824
- val loc = IntArray(2)
825
- focused.getLocationOnScreen(loc)
826
- val x = ev.rawX.toInt()
827
- val y = ev.rawY.toInt()
828
- if (x < loc[0] || x > loc[0] + focused.width || y < loc[1] || y > loc[1] + focused.height) {
829
- focused.clearFocus()
830
- (getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager)
831
- ?.hideSoftInputFromWindow(focused.windowToken, 0)
832
- }
833
- }
834
- }
835
-
836
1054
  @Deprecated("Deprecated in Java")
837
1055
  override fun onBackPressed() {
838
1056
  GeneratedActivityLifecycle.onBackPressed { consumed ->
@@ -903,7 +1121,7 @@ object DevServerPrefs {
903
1121
 
904
1122
  // src/android/create.ts
905
1123
  function readAndSubstituteTemplate(templatePath, vars) {
906
- const raw = fs3.readFileSync(templatePath, "utf-8");
1124
+ const raw = fs4.readFileSync(templatePath, "utf-8");
907
1125
  return Object.entries(vars).reduce(
908
1126
  (s, [k, v]) => s.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), v),
909
1127
  raw
@@ -914,7 +1132,7 @@ var create = async (opts = {}) => {
914
1132
  const origCwd = process.cwd();
915
1133
  if (target === "dev-app") {
916
1134
  const devAppDir = findDevAppPackage(origCwd) ?? findDevAppPackage(findRepoRoot(origCwd));
917
- if (!devAppDir || !fs3.existsSync(path3.join(devAppDir, "tamer.config.json"))) {
1135
+ if (!devAppDir || !fs4.existsSync(path4.join(devAppDir, "tamer.config.json"))) {
918
1136
  console.error("\u274C tamer-dev-app not found. Add @tamer4lynx/tamer-dev-app to dependencies.");
919
1137
  process.exit(1);
920
1138
  }
@@ -944,30 +1162,30 @@ var create = async (opts = {}) => {
944
1162
  const packagePath = packageName.replace(/\./g, "/");
945
1163
  const gradleVersion = "8.14.2";
946
1164
  const androidDir = config.paths?.androidDir ?? "android";
947
- const rootDir = path3.join(process.cwd(), androidDir);
948
- const appDir = path3.join(rootDir, "app");
949
- const mainDir = path3.join(appDir, "src", "main");
950
- const javaDir = path3.join(mainDir, "java", packagePath);
951
- const kotlinDir = path3.join(mainDir, "kotlin", packagePath);
952
- const kotlinGeneratedDir = path3.join(kotlinDir, "generated");
953
- const assetsDir = path3.join(mainDir, "assets");
954
- const themesDir = path3.join(mainDir, "res", "values");
955
- const gradleDir = path3.join(rootDir, "gradle");
1165
+ const rootDir = path4.join(process.cwd(), androidDir);
1166
+ const appDir = path4.join(rootDir, "app");
1167
+ const mainDir = path4.join(appDir, "src", "main");
1168
+ const javaDir = path4.join(mainDir, "java", packagePath);
1169
+ const kotlinDir = path4.join(mainDir, "kotlin", packagePath);
1170
+ const kotlinGeneratedDir = path4.join(kotlinDir, "generated");
1171
+ const assetsDir = path4.join(mainDir, "assets");
1172
+ const themesDir = path4.join(mainDir, "res", "values");
1173
+ const gradleDir = path4.join(rootDir, "gradle");
956
1174
  function writeFile2(filePath, content, options) {
957
- fs3.mkdirSync(path3.dirname(filePath), { recursive: true });
958
- fs3.writeFileSync(
1175
+ fs4.mkdirSync(path4.dirname(filePath), { recursive: true });
1176
+ fs4.writeFileSync(
959
1177
  filePath,
960
1178
  content.trimStart(),
961
1179
  options?.encoding ?? "utf8"
962
1180
  );
963
1181
  }
964
- if (fs3.existsSync(rootDir)) {
1182
+ if (fs4.existsSync(rootDir)) {
965
1183
  console.log(`\u{1F9F9} Removing existing directory: ${rootDir}`);
966
- fs3.rmSync(rootDir, { recursive: true, force: true });
1184
+ fs4.rmSync(rootDir, { recursive: true, force: true });
967
1185
  }
968
1186
  console.log(`\u{1F680} Creating a new Tamer4Lynx project in: ${rootDir}`);
969
1187
  writeFile2(
970
- path3.join(gradleDir, "libs.versions.toml"),
1188
+ path4.join(gradleDir, "libs.versions.toml"),
971
1189
  `
972
1190
  [versions]
973
1191
  agp = "8.9.1"
@@ -1028,7 +1246,7 @@ kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
1028
1246
  `
1029
1247
  );
1030
1248
  writeFile2(
1031
- path3.join(rootDir, "settings.gradle.kts"),
1249
+ path4.join(rootDir, "settings.gradle.kts"),
1032
1250
  `
1033
1251
  pluginManagement {
1034
1252
  repositories {
@@ -1062,7 +1280,7 @@ println("If you have native modules please run tamer android link")
1062
1280
  `
1063
1281
  );
1064
1282
  writeFile2(
1065
- path3.join(rootDir, "build.gradle.kts"),
1283
+ path4.join(rootDir, "build.gradle.kts"),
1066
1284
  `
1067
1285
  // Top-level build file where you can add configuration options common to all sub-projects/modules.
1068
1286
  plugins {
@@ -1074,7 +1292,7 @@ plugins {
1074
1292
  `
1075
1293
  );
1076
1294
  writeFile2(
1077
- path3.join(rootDir, "gradle.properties"),
1295
+ path4.join(rootDir, "gradle.properties"),
1078
1296
  `
1079
1297
  org.gradle.jvmargs=-Xmx2048m
1080
1298
  android.useAndroidX=true
@@ -1083,7 +1301,7 @@ android.enableJetifier=true
1083
1301
  `
1084
1302
  );
1085
1303
  writeFile2(
1086
- path3.join(appDir, "build.gradle.kts"),
1304
+ path4.join(appDir, "build.gradle.kts"),
1087
1305
  `
1088
1306
  plugins {
1089
1307
  alias(libs.plugins.android.application)
@@ -1175,7 +1393,7 @@ dependencies {
1175
1393
  `
1176
1394
  );
1177
1395
  writeFile2(
1178
- path3.join(themesDir, "themes.xml"),
1396
+ path4.join(themesDir, "themes.xml"),
1179
1397
  `
1180
1398
  <resources>
1181
1399
  <style name="Theme.MyApp" parent="Theme.AppCompat.Light.NoActionBar">
@@ -1209,7 +1427,7 @@ dependencies {
1209
1427
  const iconPaths = resolveIconPaths(process.cwd(), config);
1210
1428
  const manifestIconAttrs = iconPaths ? ' android:icon="@mipmap/ic_launcher"\n android:roundIcon="@mipmap/ic_launcher"\n' : "";
1211
1429
  writeFile2(
1212
- path3.join(mainDir, "AndroidManifest.xml"),
1430
+ path4.join(mainDir, "AndroidManifest.xml"),
1213
1431
  `
1214
1432
  <manifest xmlns:android="http://schemas.android.com/apk/res/android">
1215
1433
  ${manifestPermissions}
@@ -1223,7 +1441,7 @@ ${manifestIconAttrs} android:usesCleartextTraffic="true"
1223
1441
  `
1224
1442
  );
1225
1443
  writeFile2(
1226
- path3.join(kotlinGeneratedDir, "GeneratedLynxExtensions.kt"),
1444
+ path4.join(kotlinGeneratedDir, "GeneratedLynxExtensions.kt"),
1227
1445
  `
1228
1446
  package ${packageName}.generated
1229
1447
 
@@ -1251,14 +1469,14 @@ object GeneratedLynxExtensions {
1251
1469
  const hostPkg = findTamerHostPackage(process.cwd());
1252
1470
  const devClientPkg = findDevClientPackage(process.cwd());
1253
1471
  if (!hasDevLauncher && hostPkg) {
1254
- const templateDir = path3.join(hostPkg, "android", "templates");
1472
+ const templateDir = path4.join(hostPkg, "android", "templates");
1255
1473
  for (const [src, dst] of [
1256
- ["App.java", path3.join(javaDir, "App.java")],
1257
- ["TemplateProvider.java", path3.join(javaDir, "TemplateProvider.java")],
1258
- ["MainActivity.kt", path3.join(kotlinDir, "MainActivity.kt")]
1474
+ ["App.java", path4.join(javaDir, "App.java")],
1475
+ ["TemplateProvider.java", path4.join(javaDir, "TemplateProvider.java")],
1476
+ ["MainActivity.kt", path4.join(kotlinDir, "MainActivity.kt")]
1259
1477
  ]) {
1260
- const srcPath = path3.join(templateDir, src);
1261
- if (fs3.existsSync(srcPath)) {
1478
+ const srcPath = path4.join(templateDir, src);
1479
+ if (fs4.existsSync(srcPath)) {
1262
1480
  writeFile2(dst, readAndSubstituteTemplate(srcPath, templateVars));
1263
1481
  }
1264
1482
  }
@@ -1267,74 +1485,61 @@ object GeneratedLynxExtensions {
1267
1485
  fetchAndPatchApplication(vars),
1268
1486
  fetchAndPatchTemplateProvider(vars)
1269
1487
  ]);
1270
- writeFile2(path3.join(javaDir, "App.java"), applicationSource);
1271
- writeFile2(path3.join(javaDir, "TemplateProvider.java"), templateProviderSource);
1272
- writeFile2(path3.join(kotlinDir, "MainActivity.kt"), getStandaloneMainActivity(vars));
1488
+ writeFile2(path4.join(javaDir, "App.java"), applicationSource);
1489
+ writeFile2(path4.join(javaDir, "TemplateProvider.java"), templateProviderSource);
1490
+ writeFile2(path4.join(kotlinDir, "MainActivity.kt"), getStandaloneMainActivity(vars));
1273
1491
  if (hasDevLauncher) {
1274
1492
  if (devClientPkg) {
1275
- const templateDir = path3.join(devClientPkg, "android", "templates");
1493
+ const templateDir = path4.join(devClientPkg, "android", "templates");
1276
1494
  for (const [src, dst] of [
1277
- ["ProjectActivity.kt", path3.join(kotlinDir, "ProjectActivity.kt")],
1278
- ["DevClientManager.kt", path3.join(kotlinDir, "DevClientManager.kt")],
1279
- ["DevServerPrefs.kt", path3.join(kotlinDir, "DevServerPrefs.kt")]
1495
+ ["ProjectActivity.kt", path4.join(kotlinDir, "ProjectActivity.kt")],
1496
+ ["DevClientManager.kt", path4.join(kotlinDir, "DevClientManager.kt")],
1497
+ ["DevServerPrefs.kt", path4.join(kotlinDir, "DevServerPrefs.kt")]
1280
1498
  ]) {
1281
- const srcPath = path3.join(templateDir, src);
1282
- if (fs3.existsSync(srcPath)) {
1499
+ const srcPath = path4.join(templateDir, src);
1500
+ if (fs4.existsSync(srcPath)) {
1283
1501
  writeFile2(dst, readAndSubstituteTemplate(srcPath, templateVars));
1284
1502
  }
1285
1503
  }
1286
1504
  } else {
1287
- writeFile2(path3.join(kotlinDir, "ProjectActivity.kt"), getProjectActivity(vars));
1505
+ writeFile2(path4.join(kotlinDir, "ProjectActivity.kt"), getProjectActivity(vars));
1288
1506
  const devClientManagerSource = getDevClientManager(vars);
1289
1507
  if (devClientManagerSource) {
1290
- writeFile2(path3.join(kotlinDir, "DevClientManager.kt"), devClientManagerSource);
1291
- writeFile2(path3.join(kotlinDir, "DevServerPrefs.kt"), getDevServerPrefs(vars));
1508
+ writeFile2(path4.join(kotlinDir, "DevClientManager.kt"), devClientManagerSource);
1509
+ writeFile2(path4.join(kotlinDir, "DevServerPrefs.kt"), getDevServerPrefs(vars));
1292
1510
  }
1293
1511
  }
1294
1512
  }
1295
1513
  }
1296
1514
  if (iconPaths) {
1297
- const resDir = path3.join(mainDir, "res");
1298
- if (iconPaths.android) {
1299
- const src = iconPaths.android;
1300
- const entries = fs3.readdirSync(src, { withFileTypes: true });
1301
- for (const e of entries) {
1302
- const dest = path3.join(resDir, e.name);
1303
- if (e.isDirectory()) {
1304
- fs3.cpSync(path3.join(src, e.name), dest, { recursive: true });
1305
- } else {
1306
- fs3.mkdirSync(resDir, { recursive: true });
1307
- fs3.copyFileSync(path3.join(src, e.name), dest);
1308
- }
1515
+ const resDir = path4.join(mainDir, "res");
1516
+ if (applyAndroidLauncherIcons(resDir, iconPaths)) {
1517
+ if (iconPaths.androidAdaptiveForeground && (iconPaths.androidAdaptiveBackground || iconPaths.androidAdaptiveBackgroundColor)) {
1518
+ console.log("\u2705 Android adaptive launcher from tamer.config.json icon.androidAdaptive");
1519
+ } else if (iconPaths.android) {
1520
+ console.log("\u2705 Copied Android icon from tamer.config.json icon.android");
1521
+ } else if (iconPaths.source) {
1522
+ console.log("\u2705 Copied app icon from tamer.config.json icon.source");
1309
1523
  }
1310
- console.log("\u2705 Copied Android icon from tamer.config.json icon.android");
1311
- } else if (iconPaths.source) {
1312
- const mipmapDensities = ["mdpi", "hdpi", "xhdpi", "xxhdpi", "xxxhdpi"];
1313
- for (const d of mipmapDensities) {
1314
- const dir = path3.join(resDir, `mipmap-${d}`);
1315
- fs3.mkdirSync(dir, { recursive: true });
1316
- fs3.copyFileSync(iconPaths.source, path3.join(dir, "ic_launcher.png"));
1317
- }
1318
- console.log("\u2705 Copied app icon from tamer.config.json icon.source");
1319
1524
  }
1320
1525
  }
1321
- fs3.mkdirSync(assetsDir, { recursive: true });
1322
- fs3.writeFileSync(path3.join(assetsDir, ".gitkeep"), "");
1526
+ fs4.mkdirSync(assetsDir, { recursive: true });
1527
+ fs4.writeFileSync(path4.join(assetsDir, ".gitkeep"), "");
1323
1528
  console.log(`\u2705 Android Kotlin project created at ${rootDir}`);
1324
1529
  async function finalizeProjectSetup() {
1325
1530
  if (androidSdk) {
1326
1531
  try {
1327
1532
  const sdkDirContent = `sdk.dir=${androidSdk.replace(/\\/g, "/")}`;
1328
- writeFile2(path3.join(rootDir, "local.properties"), sdkDirContent);
1533
+ writeFile2(path4.join(rootDir, "local.properties"), sdkDirContent);
1329
1534
  console.log("\u{1F4E6} Created local.properties from tamer.config.json.");
1330
1535
  } catch (err) {
1331
1536
  console.error(`\u274C Failed to create local.properties: ${err.message}`);
1332
1537
  }
1333
1538
  } else {
1334
- const localPropsPath = path3.join(process.cwd(), "local.properties");
1335
- if (fs3.existsSync(localPropsPath)) {
1539
+ const localPropsPath = path4.join(process.cwd(), "local.properties");
1540
+ if (fs4.existsSync(localPropsPath)) {
1336
1541
  try {
1337
- fs3.copyFileSync(localPropsPath, path3.join(rootDir, "local.properties"));
1542
+ fs4.copyFileSync(localPropsPath, path4.join(rootDir, "local.properties"));
1338
1543
  console.log("\u{1F4E6} Copied existing local.properties to the android project.");
1339
1544
  } catch (err) {
1340
1545
  console.error("\u274C Failed to copy local.properties:", err);
@@ -1353,32 +1558,33 @@ object GeneratedLynxExtensions {
1353
1558
  var create_default = create;
1354
1559
 
1355
1560
  // src/android/autolink.ts
1561
+ import fs7 from "fs";
1562
+ import path7 from "path";
1563
+ import { execSync as execSync2 } from "child_process";
1564
+
1565
+ // src/common/discoverModules.ts
1356
1566
  import fs6 from "fs";
1357
1567
  import path6 from "path";
1358
1568
 
1359
- // src/common/discoverModules.ts
1569
+ // src/common/config.ts
1360
1570
  import fs5 from "fs";
1361
1571
  import path5 from "path";
1362
-
1363
- // src/common/config.ts
1364
- import fs4 from "fs";
1365
- import path4 from "path";
1366
1572
  var LYNX_EXT_JSON = "lynx.ext.json";
1367
1573
  var TAMER_JSON = "tamer.json";
1368
1574
  function loadLynxExtJson(packagePath) {
1369
- const p = path4.join(packagePath, LYNX_EXT_JSON);
1370
- if (!fs4.existsSync(p)) return null;
1575
+ const p = path5.join(packagePath, LYNX_EXT_JSON);
1576
+ if (!fs5.existsSync(p)) return null;
1371
1577
  try {
1372
- return JSON.parse(fs4.readFileSync(p, "utf8"));
1578
+ return JSON.parse(fs5.readFileSync(p, "utf8"));
1373
1579
  } catch {
1374
1580
  return null;
1375
1581
  }
1376
1582
  }
1377
1583
  function loadTamerJson(packagePath) {
1378
- const p = path4.join(packagePath, TAMER_JSON);
1379
- if (!fs4.existsSync(p)) return null;
1584
+ const p = path5.join(packagePath, TAMER_JSON);
1585
+ if (!fs5.existsSync(p)) return null;
1380
1586
  try {
1381
- return JSON.parse(fs4.readFileSync(p, "utf8"));
1587
+ return JSON.parse(fs5.readFileSync(p, "utf8"));
1382
1588
  } catch {
1383
1589
  return null;
1384
1590
  }
@@ -1431,7 +1637,7 @@ function loadExtensionConfig(packagePath) {
1431
1637
  return normalized;
1432
1638
  }
1433
1639
  function hasExtensionConfig(packagePath) {
1434
- return fs4.existsSync(path4.join(packagePath, LYNX_EXT_JSON)) || fs4.existsSync(path4.join(packagePath, TAMER_JSON));
1640
+ return fs5.existsSync(path5.join(packagePath, LYNX_EXT_JSON)) || fs5.existsSync(path5.join(packagePath, TAMER_JSON));
1435
1641
  }
1436
1642
  function getAndroidModuleClassNames(config) {
1437
1643
  if (!config) return [];
@@ -1449,15 +1655,15 @@ function getIosElements(config) {
1449
1655
  return config?.elements ?? {};
1450
1656
  }
1451
1657
  function getNodeModulesPath(projectRoot) {
1452
- let nodeModulesPath = path4.join(projectRoot, "node_modules");
1453
- const workspaceRoot = path4.join(projectRoot, "..", "..");
1454
- const rootNodeModules = path4.join(workspaceRoot, "node_modules");
1455
- if (fs4.existsSync(path4.join(workspaceRoot, "package.json")) && fs4.existsSync(rootNodeModules) && path4.basename(path4.dirname(projectRoot)) === "packages") {
1658
+ let nodeModulesPath = path5.join(projectRoot, "node_modules");
1659
+ const workspaceRoot = path5.join(projectRoot, "..", "..");
1660
+ const rootNodeModules = path5.join(workspaceRoot, "node_modules");
1661
+ if (fs5.existsSync(path5.join(workspaceRoot, "package.json")) && fs5.existsSync(rootNodeModules) && path5.basename(path5.dirname(projectRoot)) === "packages") {
1456
1662
  nodeModulesPath = rootNodeModules;
1457
- } else if (!fs4.existsSync(nodeModulesPath)) {
1458
- const altRoot = path4.join(projectRoot, "..", "..");
1459
- const altNodeModules = path4.join(altRoot, "node_modules");
1460
- if (fs4.existsSync(path4.join(altRoot, "package.json")) && fs4.existsSync(altNodeModules)) {
1663
+ } else if (!fs5.existsSync(nodeModulesPath)) {
1664
+ const altRoot = path5.join(projectRoot, "..", "..");
1665
+ const altNodeModules = path5.join(altRoot, "node_modules");
1666
+ if (fs5.existsSync(path5.join(altRoot, "package.json")) && fs5.existsSync(altNodeModules)) {
1461
1667
  nodeModulesPath = altNodeModules;
1462
1668
  }
1463
1669
  }
@@ -1466,8 +1672,8 @@ function getNodeModulesPath(projectRoot) {
1466
1672
  function discoverNativeExtensions(projectRoot) {
1467
1673
  const nodeModulesPath = getNodeModulesPath(projectRoot);
1468
1674
  const result = [];
1469
- if (!fs4.existsSync(nodeModulesPath)) return result;
1470
- const packageDirs = fs4.readdirSync(nodeModulesPath);
1675
+ if (!fs5.existsSync(nodeModulesPath)) return result;
1676
+ const packageDirs = fs5.readdirSync(nodeModulesPath);
1471
1677
  const check = (name, packagePath) => {
1472
1678
  if (!hasExtensionConfig(packagePath)) return;
1473
1679
  const config = loadExtensionConfig(packagePath);
@@ -1477,11 +1683,11 @@ function discoverNativeExtensions(projectRoot) {
1477
1683
  }
1478
1684
  };
1479
1685
  for (const dirName of packageDirs) {
1480
- const fullPath = path4.join(nodeModulesPath, dirName);
1686
+ const fullPath = path5.join(nodeModulesPath, dirName);
1481
1687
  if (dirName.startsWith("@")) {
1482
1688
  try {
1483
- for (const scopedDirName of fs4.readdirSync(fullPath)) {
1484
- check(`${dirName}/${scopedDirName}`, path4.join(fullPath, scopedDirName));
1689
+ for (const scopedDirName of fs5.readdirSync(fullPath)) {
1690
+ check(`${dirName}/${scopedDirName}`, path5.join(fullPath, scopedDirName));
1485
1691
  }
1486
1692
  } catch {
1487
1693
  }
@@ -1494,15 +1700,15 @@ function discoverNativeExtensions(projectRoot) {
1494
1700
 
1495
1701
  // src/common/discoverModules.ts
1496
1702
  function resolveNodeModulesPath(projectRoot) {
1497
- let nodeModulesPath = path5.join(projectRoot, "node_modules");
1498
- const workspaceRoot = path5.join(projectRoot, "..", "..");
1499
- const rootNodeModules = path5.join(workspaceRoot, "node_modules");
1500
- if (fs5.existsSync(path5.join(workspaceRoot, "package.json")) && fs5.existsSync(rootNodeModules) && path5.basename(path5.dirname(projectRoot)) === "packages") {
1703
+ let nodeModulesPath = path6.join(projectRoot, "node_modules");
1704
+ const workspaceRoot = path6.join(projectRoot, "..", "..");
1705
+ const rootNodeModules = path6.join(workspaceRoot, "node_modules");
1706
+ if (fs6.existsSync(path6.join(workspaceRoot, "package.json")) && fs6.existsSync(rootNodeModules) && path6.basename(path6.dirname(projectRoot)) === "packages") {
1501
1707
  nodeModulesPath = rootNodeModules;
1502
- } else if (!fs5.existsSync(nodeModulesPath)) {
1503
- const altRoot = path5.join(projectRoot, "..", "..");
1504
- const altNodeModules = path5.join(altRoot, "node_modules");
1505
- if (fs5.existsSync(path5.join(altRoot, "package.json")) && fs5.existsSync(altNodeModules)) {
1708
+ } else if (!fs6.existsSync(nodeModulesPath)) {
1709
+ const altRoot = path6.join(projectRoot, "..", "..");
1710
+ const altNodeModules = path6.join(altRoot, "node_modules");
1711
+ if (fs6.existsSync(path6.join(altRoot, "package.json")) && fs6.existsSync(altNodeModules)) {
1506
1712
  nodeModulesPath = altNodeModules;
1507
1713
  }
1508
1714
  }
@@ -1511,12 +1717,12 @@ function resolveNodeModulesPath(projectRoot) {
1511
1717
  function discoverModules(projectRoot) {
1512
1718
  const nodeModulesPath = resolveNodeModulesPath(projectRoot);
1513
1719
  const packages = [];
1514
- if (!fs5.existsSync(nodeModulesPath)) {
1720
+ if (!fs6.existsSync(nodeModulesPath)) {
1515
1721
  return [];
1516
1722
  }
1517
- const packageDirs = fs5.readdirSync(nodeModulesPath);
1723
+ const packageDirs = fs6.readdirSync(nodeModulesPath);
1518
1724
  for (const dirName of packageDirs) {
1519
- const fullPath = path5.join(nodeModulesPath, dirName);
1725
+ const fullPath = path6.join(nodeModulesPath, dirName);
1520
1726
  const checkPackage = (name, packagePath) => {
1521
1727
  if (!hasExtensionConfig(packagePath)) return;
1522
1728
  const config = loadExtensionConfig(packagePath);
@@ -1525,9 +1731,9 @@ function discoverModules(projectRoot) {
1525
1731
  };
1526
1732
  if (dirName.startsWith("@")) {
1527
1733
  try {
1528
- const scopedDirs = fs5.readdirSync(fullPath);
1734
+ const scopedDirs = fs6.readdirSync(fullPath);
1529
1735
  for (const scopedDirName of scopedDirs) {
1530
- const scopedPackagePath = path5.join(fullPath, scopedDirName);
1736
+ const scopedPackagePath = path6.join(fullPath, scopedDirName);
1531
1737
  checkPackage(`${dirName}/${scopedDirName}`, scopedPackagePath);
1532
1738
  }
1533
1739
  } catch (e) {
@@ -1542,16 +1748,25 @@ function discoverModules(projectRoot) {
1542
1748
  }
1543
1749
 
1544
1750
  // src/common/generateExtCode.ts
1545
- function generateLynxExtensionsKotlin(packages, projectPackage) {
1546
- const modulePackages = packages.filter((p) => getAndroidModuleClassNames(p.config.android).length > 0);
1547
- const elementPackages = packages.filter((p) => p.config.android?.elements && Object.keys(p.config.android.elements).length > 0);
1751
+ function getDedupedAndroidModuleClassNames(packages) {
1548
1752
  const seenNames = /* @__PURE__ */ new Set();
1549
- const allModuleClasses = modulePackages.flatMap((p) => getAndroidModuleClassNames(p.config.android)).filter((fullClassName) => {
1753
+ return packages.flatMap((p) => getAndroidModuleClassNames(p.config.android)).filter((fullClassName) => {
1550
1754
  const simple = fullClassName.split(".").pop();
1551
1755
  if (seenNames.has(simple)) return false;
1552
1756
  seenNames.add(simple);
1553
1757
  return true;
1554
1758
  });
1759
+ }
1760
+ function generateLynxExtensionsKotlin(packages, projectPackage) {
1761
+ const modulePackages = packages.filter((p) => getAndroidModuleClassNames(p.config.android).length > 0);
1762
+ const elementPackages = packages.filter((p) => p.config.android?.elements && Object.keys(p.config.android.elements).length > 0);
1763
+ const allModuleClasses = getDedupedAndroidModuleClassNames(packages);
1764
+ const hasDevClient = packages.some((p) => p.name === "@tamer4lynx/tamer-dev-client");
1765
+ const devClientSupportedBlock = hasDevClient && allModuleClasses.length > 0 ? `
1766
+ com.nanofuxion.tamerdevclient.DevClientModule.attachSupportedModuleClassNames(listOf(
1767
+ ${allModuleClasses.map((c) => ` "${c.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`).join(",\n")}
1768
+ ))
1769
+ ` : hasDevClient ? "\n com.nanofuxion.tamerdevclient.DevClientModule.attachSupportedModuleClassNames(emptyList())\n" : "";
1555
1770
  const moduleImports = allModuleClasses.map((c) => `import ${c}`).join("\n");
1556
1771
  const elementImports = elementPackages.flatMap((p) => Object.values(p.config.android.elements).map((cls) => `import ${cls}`)).filter((v, i, a) => a.indexOf(v) === i).join("\n");
1557
1772
  const moduleRegistrations = allModuleClasses.map((fullClassName) => {
@@ -1591,7 +1806,7 @@ ${elementImports}
1591
1806
 
1592
1807
  object GeneratedLynxExtensions {
1593
1808
  fun register(context: Context) {
1594
- ${allRegistrations}
1809
+ ${allRegistrations}${devClientSupportedBlock}
1595
1810
  }
1596
1811
 
1597
1812
  fun configureViewBuilder(viewBuilder: LynxViewBuilder) {
@@ -1724,11 +1939,11 @@ var autolink = (opts) => {
1724
1939
  const packageName = config.android.packageName;
1725
1940
  const projectRoot = resolved.projectRoot;
1726
1941
  function updateGeneratedSection(filePath, newContent, startMarker, endMarker) {
1727
- if (!fs6.existsSync(filePath)) {
1942
+ if (!fs7.existsSync(filePath)) {
1728
1943
  console.warn(`\u26A0\uFE0F File not found, skipping update: ${filePath}`);
1729
1944
  return;
1730
1945
  }
1731
- let fileContent = fs6.readFileSync(filePath, "utf8");
1946
+ let fileContent = fs7.readFileSync(filePath, "utf8");
1732
1947
  const escapedStartMarker = startMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1733
1948
  const escapedEndMarker = endMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1734
1949
  const regex = new RegExp(`${escapedStartMarker}[\\s\\S]*?${escapedEndMarker}`, "g");
@@ -1738,16 +1953,16 @@ ${endMarker}`;
1738
1953
  if (regex.test(fileContent)) {
1739
1954
  fileContent = fileContent.replace(regex, replacementBlock);
1740
1955
  } else {
1741
- console.warn(`\u26A0\uFE0F Could not find autolink markers in ${path6.basename(filePath)}. Appending to the end of the file.`);
1956
+ console.warn(`\u26A0\uFE0F Could not find autolink markers in ${path7.basename(filePath)}. Appending to the end of the file.`);
1742
1957
  fileContent += `
1743
1958
  ${replacementBlock}
1744
1959
  `;
1745
1960
  }
1746
- fs6.writeFileSync(filePath, fileContent);
1747
- console.log(`\u2705 Updated autolinked section in ${path6.basename(filePath)}`);
1961
+ fs7.writeFileSync(filePath, fileContent);
1962
+ console.log(`\u2705 Updated autolinked section in ${path7.basename(filePath)}`);
1748
1963
  }
1749
1964
  function updateSettingsGradle(packages) {
1750
- const settingsFilePath = path6.join(appAndroidPath, "settings.gradle.kts");
1965
+ const settingsFilePath = path7.join(appAndroidPath, "settings.gradle.kts");
1751
1966
  let scriptContent = `// This section is automatically generated by Tamer4Lynx.
1752
1967
  // Manual edits will be overwritten.`;
1753
1968
  const androidPackages = packages.filter((p) => p.config.android);
@@ -1755,7 +1970,7 @@ ${replacementBlock}
1755
1970
  androidPackages.forEach((pkg) => {
1756
1971
  const gradleProjectName = pkg.name.replace(/^@/, "").replace(/\//g, "_");
1757
1972
  const sourceDir = pkg.config.android?.sourceDir || "android";
1758
- const projectPath = path6.resolve(pkg.packagePath, sourceDir).replace(/\\/g, "/");
1973
+ const projectPath = path7.resolve(pkg.packagePath, sourceDir).replace(/\\/g, "/");
1759
1974
  scriptContent += `
1760
1975
  include(":${gradleProjectName}")`;
1761
1976
  scriptContent += `
@@ -1768,7 +1983,7 @@ println("No native modules found by Tamer4Lynx autolinker.")`;
1768
1983
  updateGeneratedSection(settingsFilePath, scriptContent.trim(), "// GENERATED AUTOLINK START", "// GENERATED AUTOLINK END");
1769
1984
  }
1770
1985
  function updateAppBuildGradle(packages) {
1771
- const appBuildGradlePath = path6.join(appAndroidPath, "app", "build.gradle.kts");
1986
+ const appBuildGradlePath = path7.join(appAndroidPath, "app", "build.gradle.kts");
1772
1987
  const androidPackages = packages.filter((p) => p.config.android);
1773
1988
  const implementationLines = androidPackages.map((p) => {
1774
1989
  const gradleProjectName = p.name.replace(/^@/, "").replace(/\//g, "_");
@@ -1786,35 +2001,35 @@ ${implementationLines || " // No native dependencies found to link."}`;
1786
2001
  }
1787
2002
  function generateKotlinExtensionsFile(packages, projectPackage) {
1788
2003
  const packagePath = projectPackage.replace(/\./g, "/");
1789
- const generatedDir = path6.join(appAndroidPath, "app", "src", "main", "kotlin", packagePath, "generated");
1790
- const kotlinExtensionsPath = path6.join(generatedDir, "GeneratedLynxExtensions.kt");
2004
+ const generatedDir = path7.join(appAndroidPath, "app", "src", "main", "kotlin", packagePath, "generated");
2005
+ const kotlinExtensionsPath = path7.join(generatedDir, "GeneratedLynxExtensions.kt");
1791
2006
  const content = `/**
1792
2007
  * This file is generated by the Tamer4Lynx autolinker.
1793
2008
  * Do not edit this file manually.
1794
2009
  */
1795
2010
  ${generateLynxExtensionsKotlin(packages, projectPackage)}`;
1796
- fs6.mkdirSync(generatedDir, { recursive: true });
1797
- fs6.writeFileSync(kotlinExtensionsPath, content.trimStart());
2011
+ fs7.mkdirSync(generatedDir, { recursive: true });
2012
+ fs7.writeFileSync(kotlinExtensionsPath, content.trimStart());
1798
2013
  console.log(`\u2705 Generated Kotlin extensions at ${kotlinExtensionsPath}`);
1799
2014
  }
1800
2015
  function generateActivityLifecycleFile(packages, projectPackage) {
1801
2016
  const packageKotlinPath = projectPackage.replace(/\./g, "/");
1802
- const generatedDir = path6.join(appAndroidPath, "app", "src", "main", "kotlin", packageKotlinPath, "generated");
1803
- const outputPath = path6.join(generatedDir, "GeneratedActivityLifecycle.kt");
2017
+ const generatedDir = path7.join(appAndroidPath, "app", "src", "main", "kotlin", packageKotlinPath, "generated");
2018
+ const outputPath = path7.join(generatedDir, "GeneratedActivityLifecycle.kt");
1804
2019
  const content = `/**
1805
2020
  * This file is generated by the Tamer4Lynx autolinker.
1806
2021
  * Do not edit this file manually.
1807
2022
  */
1808
2023
  ${generateActivityLifecycleKotlin(packages, projectPackage)}`;
1809
- fs6.mkdirSync(generatedDir, { recursive: true });
1810
- fs6.writeFileSync(outputPath, content);
2024
+ fs7.mkdirSync(generatedDir, { recursive: true });
2025
+ fs7.writeFileSync(outputPath, content);
1811
2026
  console.log(`\u2705 Generated activity lifecycle patches at ${outputPath}`);
1812
2027
  }
1813
2028
  function syncDeepLinkIntentFilters() {
1814
2029
  const deepLinks = config.android?.deepLinks;
1815
2030
  if (!deepLinks || deepLinks.length === 0) return;
1816
- const manifestPath = path6.join(appAndroidPath, "app", "src", "main", "AndroidManifest.xml");
1817
- if (!fs6.existsSync(manifestPath)) return;
2031
+ const manifestPath = path7.join(appAndroidPath, "app", "src", "main", "AndroidManifest.xml");
2032
+ if (!fs7.existsSync(manifestPath)) return;
1818
2033
  const intentFilters = deepLinks.map((link) => {
1819
2034
  const dataAttrs = [
1820
2035
  `android:scheme="${link.scheme}"`,
@@ -1839,9 +2054,9 @@ ${generateActivityLifecycleKotlin(packages, projectPackage)}`;
1839
2054
  console.log("\u{1F50E} Finding Lynx extension packages (lynx.ext.json / tamer.json)...");
1840
2055
  let packages = discoverModules(projectRoot).filter((p) => p.config.android);
1841
2056
  const includeDevClient = opts?.includeDevClient === true;
1842
- const devClientScoped = path6.join(projectRoot, "node_modules", "@tamer4lynx", "tamer-dev-client");
1843
- const devClientFlat = path6.join(projectRoot, "node_modules", "tamer-dev-client");
1844
- const devClientPath = fs6.existsSync(path6.join(devClientScoped, "android")) ? devClientScoped : fs6.existsSync(path6.join(devClientFlat, "android")) ? devClientFlat : null;
2057
+ const devClientScoped = path7.join(projectRoot, "node_modules", "@tamer4lynx", "tamer-dev-client");
2058
+ const devClientFlat = path7.join(projectRoot, "node_modules", "tamer-dev-client");
2059
+ const devClientPath = fs7.existsSync(path7.join(devClientScoped, "android")) ? devClientScoped : fs7.existsSync(path7.join(devClientFlat, "android")) ? devClientFlat : null;
1845
2060
  const hasDevClient = packages.some((p) => p.name === "@tamer4lynx/tamer-dev-client" || p.name === "tamer-dev-client");
1846
2061
  if (includeDevClient && devClientPath && !hasDevClient) {
1847
2062
  packages = [{
@@ -1864,17 +2079,33 @@ ${generateActivityLifecycleKotlin(packages, projectPackage)}`;
1864
2079
  syncVersionCatalog(packages);
1865
2080
  ensureXElementDeps();
1866
2081
  ensureReleaseSigning();
2082
+ runGradleSync();
1867
2083
  console.log("\u2728 Autolinking complete.");
1868
2084
  }
2085
+ function runGradleSync() {
2086
+ const gradlew = path7.join(appAndroidPath, process.platform === "win32" ? "gradlew.bat" : "gradlew");
2087
+ if (!fs7.existsSync(gradlew)) return;
2088
+ try {
2089
+ console.log("\u2139\uFE0F Running Gradle sync in android directory...");
2090
+ execSync2(process.platform === "win32" ? "gradlew.bat projects" : "./gradlew projects", {
2091
+ cwd: appAndroidPath,
2092
+ stdio: "inherit"
2093
+ });
2094
+ console.log("\u2705 Gradle sync completed.");
2095
+ } catch (e) {
2096
+ console.warn("\u26A0\uFE0F Gradle sync failed:", e.message);
2097
+ console.log("\u26A0\uFE0F You can run `./gradlew tasks` in the android directory to sync.");
2098
+ }
2099
+ }
1869
2100
  function syncVersionCatalog(packages) {
1870
- const libsTomlPath = path6.join(appAndroidPath, "gradle", "libs.versions.toml");
1871
- if (!fs6.existsSync(libsTomlPath)) return;
2101
+ const libsTomlPath = path7.join(appAndroidPath, "gradle", "libs.versions.toml");
2102
+ if (!fs7.existsSync(libsTomlPath)) return;
1872
2103
  const requiredAliases = /* @__PURE__ */ new Set();
1873
2104
  const requiredPluginAliases = /* @__PURE__ */ new Set();
1874
2105
  for (const pkg of packages) {
1875
- const buildPath = path6.join(pkg.packagePath, pkg.config.android?.sourceDir || "android", "build.gradle.kts");
1876
- if (!fs6.existsSync(buildPath)) continue;
1877
- const content = fs6.readFileSync(buildPath, "utf8");
2106
+ const buildPath = path7.join(pkg.packagePath, pkg.config.android?.sourceDir || "android", "build.gradle.kts");
2107
+ if (!fs7.existsSync(buildPath)) continue;
2108
+ const content = fs7.readFileSync(buildPath, "utf8");
1878
2109
  for (const m of content.matchAll(/libs\.([\w.]+)/g)) {
1879
2110
  const alias = m[1];
1880
2111
  if (alias && alias in REQUIRED_CATALOG_ENTRIES) requiredAliases.add(alias);
@@ -1885,7 +2116,7 @@ ${generateActivityLifecycleKotlin(packages, projectPackage)}`;
1885
2116
  }
1886
2117
  }
1887
2118
  if (requiredAliases.size === 0 && requiredPluginAliases.size === 0) return;
1888
- let toml = fs6.readFileSync(libsTomlPath, "utf8");
2119
+ let toml = fs7.readFileSync(libsTomlPath, "utf8");
1889
2120
  let updated = false;
1890
2121
  for (const alias of requiredAliases) {
1891
2122
  const entry = REQUIRED_CATALOG_ENTRIES[alias];
@@ -1909,14 +2140,14 @@ ${generateActivityLifecycleKotlin(packages, projectPackage)}`;
1909
2140
  updated = true;
1910
2141
  }
1911
2142
  if (updated) {
1912
- fs6.writeFileSync(libsTomlPath, toml);
2143
+ fs7.writeFileSync(libsTomlPath, toml);
1913
2144
  console.log("\u2705 Synced version catalog (libs.versions.toml) for linked modules.");
1914
2145
  }
1915
2146
  }
1916
2147
  function ensureXElementDeps() {
1917
- const libsTomlPath = path6.join(appAndroidPath, "gradle", "libs.versions.toml");
1918
- if (fs6.existsSync(libsTomlPath)) {
1919
- let toml = fs6.readFileSync(libsTomlPath, "utf8");
2148
+ const libsTomlPath = path7.join(appAndroidPath, "gradle", "libs.versions.toml");
2149
+ if (fs7.existsSync(libsTomlPath)) {
2150
+ let toml = fs7.readFileSync(libsTomlPath, "utf8");
1920
2151
  let updated = false;
1921
2152
  if (!toml.includes("lynx-xelement =")) {
1922
2153
  toml = toml.replace(
@@ -1928,13 +2159,13 @@ lynx-xelement-input = { module = "org.lynxsdk.lynx:xelement-input", version.ref
1928
2159
  updated = true;
1929
2160
  }
1930
2161
  if (updated) {
1931
- fs6.writeFileSync(libsTomlPath, toml);
2162
+ fs7.writeFileSync(libsTomlPath, toml);
1932
2163
  console.log("\u2705 Added XElement entries to version catalog.");
1933
2164
  }
1934
2165
  }
1935
- const appBuildPath = path6.join(appAndroidPath, "app", "build.gradle.kts");
1936
- if (fs6.existsSync(appBuildPath)) {
1937
- let content = fs6.readFileSync(appBuildPath, "utf8");
2166
+ const appBuildPath = path7.join(appAndroidPath, "app", "build.gradle.kts");
2167
+ if (fs7.existsSync(appBuildPath)) {
2168
+ let content = fs7.readFileSync(appBuildPath, "utf8");
1938
2169
  if (!content.includes("lynx.xelement")) {
1939
2170
  content = content.replace(
1940
2171
  /(implementation\(libs\.lynx\.service\.http\))/,
@@ -1942,15 +2173,15 @@ lynx-xelement-input = { module = "org.lynxsdk.lynx:xelement-input", version.ref
1942
2173
  implementation(libs.lynx.xelement)
1943
2174
  implementation(libs.lynx.xelement.input)`
1944
2175
  );
1945
- fs6.writeFileSync(appBuildPath, content);
2176
+ fs7.writeFileSync(appBuildPath, content);
1946
2177
  console.log("\u2705 Added XElement dependencies to app build.gradle.kts.");
1947
2178
  }
1948
2179
  }
1949
2180
  }
1950
2181
  function ensureReleaseSigning() {
1951
- const appBuildPath = path6.join(appAndroidPath, "app", "build.gradle.kts");
1952
- if (!fs6.existsSync(appBuildPath)) return;
1953
- let content = fs6.readFileSync(appBuildPath, "utf8");
2182
+ const appBuildPath = path7.join(appAndroidPath, "app", "build.gradle.kts");
2183
+ if (!fs7.existsSync(appBuildPath)) return;
2184
+ let content = fs7.readFileSync(appBuildPath, "utf8");
1954
2185
  if (content.includes('signingConfig = signingConfigs.getByName("debug")')) return;
1955
2186
  const releaseBlock = /(release\s*\{)([\s\S]*?)(\n \}\s*\n(\s*\}|\s*compileOptions))/;
1956
2187
  const match = content.match(releaseBlock);
@@ -1961,13 +2192,13 @@ lynx-xelement-input = { module = "org.lynxsdk.lynx:xelement-input", version.ref
1961
2192
  signingConfig = signingConfigs.getByName("debug")
1962
2193
  $3`
1963
2194
  );
1964
- fs6.writeFileSync(appBuildPath, content);
2195
+ fs7.writeFileSync(appBuildPath, content);
1965
2196
  console.log("\u2705 Set release signing to debug so installRelease works without a keystore.");
1966
2197
  }
1967
2198
  }
1968
2199
  function syncManifestPermissions(packages) {
1969
- const manifestPath = path6.join(appAndroidPath, "app", "src", "main", "AndroidManifest.xml");
1970
- if (!fs6.existsSync(manifestPath)) return;
2200
+ const manifestPath = path7.join(appAndroidPath, "app", "src", "main", "AndroidManifest.xml");
2201
+ if (!fs7.existsSync(manifestPath)) return;
1971
2202
  const allPermissions = /* @__PURE__ */ new Set();
1972
2203
  for (const pkg of packages) {
1973
2204
  const perms = pkg.config.android?.permissions;
@@ -1979,7 +2210,7 @@ lynx-xelement-input = { module = "org.lynxsdk.lynx:xelement-input", version.ref
1979
2210
  }
1980
2211
  }
1981
2212
  if (allPermissions.size === 0) return;
1982
- let manifest = fs6.readFileSync(manifestPath, "utf8");
2213
+ let manifest = fs7.readFileSync(manifestPath, "utf8");
1983
2214
  const existingMatch = [...manifest.matchAll(/<uses-permission android:name="(android\.permission\.\w+)"\s*\/>/g)];
1984
2215
  const existing = new Set(existingMatch.map((m) => m[1]));
1985
2216
  const toAdd = [...allPermissions].filter((p) => !existing.has(p));
@@ -1990,7 +2221,7 @@ lynx-xelement-input = { module = "org.lynxsdk.lynx:xelement-input", version.ref
1990
2221
  `${newLines}
1991
2222
  $1$2`
1992
2223
  );
1993
- fs6.writeFileSync(manifestPath, manifest);
2224
+ fs7.writeFileSync(manifestPath, manifest);
1994
2225
  console.log(`\u2705 Synced manifest permissions: ${toAdd.map((p) => p.split(".").pop()).join(", ")}`);
1995
2226
  }
1996
2227
  run();
@@ -1998,26 +2229,26 @@ $1$2`
1998
2229
  var autolink_default = autolink;
1999
2230
 
2000
2231
  // src/android/bundle.ts
2001
- import fs9 from "fs";
2002
- import path9 from "path";
2003
- import { execSync as execSync2 } from "child_process";
2232
+ import fs10 from "fs";
2233
+ import path10 from "path";
2234
+ import { execSync as execSync3 } from "child_process";
2004
2235
 
2005
2236
  // src/common/copyDistAssets.ts
2006
- import fs7 from "fs";
2007
- import path7 from "path";
2237
+ import fs8 from "fs";
2238
+ import path8 from "path";
2008
2239
  var SKIP = /* @__PURE__ */ new Set([".rspeedy", "stats.json"]);
2009
2240
  function copyDistAssets(distDir, destDir, bundleFile) {
2010
- if (!fs7.existsSync(distDir)) return;
2011
- for (const entry of fs7.readdirSync(distDir)) {
2241
+ if (!fs8.existsSync(distDir)) return;
2242
+ for (const entry of fs8.readdirSync(distDir)) {
2012
2243
  if (SKIP.has(entry)) continue;
2013
- const src = path7.join(distDir, entry);
2014
- const dest = path7.join(destDir, entry);
2015
- const stat = fs7.statSync(src);
2244
+ const src = path8.join(distDir, entry);
2245
+ const dest = path8.join(destDir, entry);
2246
+ const stat = fs8.statSync(src);
2016
2247
  if (stat.isDirectory()) {
2017
- fs7.mkdirSync(dest, { recursive: true });
2248
+ fs8.mkdirSync(dest, { recursive: true });
2018
2249
  copyDistAssets(src, dest, bundleFile);
2019
2250
  } else {
2020
- fs7.copyFileSync(src, dest);
2251
+ fs8.copyFileSync(src, dest);
2021
2252
  if (entry !== bundleFile) {
2022
2253
  console.log(`\u2728 Copied asset: ${entry}`);
2023
2254
  }
@@ -2026,18 +2257,18 @@ function copyDistAssets(distDir, destDir, bundleFile) {
2026
2257
  }
2027
2258
 
2028
2259
  // src/android/syncDevClient.ts
2029
- import fs8 from "fs";
2030
- import path8 from "path";
2260
+ import fs9 from "fs";
2261
+ import path9 from "path";
2031
2262
  function readAndSubstituteTemplate2(templatePath, vars) {
2032
- const raw = fs8.readFileSync(templatePath, "utf-8");
2263
+ const raw = fs9.readFileSync(templatePath, "utf-8");
2033
2264
  return Object.entries(vars).reduce(
2034
2265
  (s, [k, v]) => s.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), v),
2035
2266
  raw
2036
2267
  );
2037
2268
  }
2038
2269
  function patchAppLogService(appPath) {
2039
- if (!fs8.existsSync(appPath)) return;
2040
- const raw = fs8.readFileSync(appPath, "utf-8");
2270
+ if (!fs9.existsSync(appPath)) return;
2271
+ const raw = fs9.readFileSync(appPath, "utf-8");
2041
2272
  const patched = raw.replace(
2042
2273
  /private void initLynxService\(\)\s*\{[\s\S]*?\n\s*}\s*\n\s*private void initFresco\(\)/,
2043
2274
  `private void initLynxService() {
@@ -2056,7 +2287,7 @@ function patchAppLogService(appPath) {
2056
2287
  private void initFresco()`
2057
2288
  );
2058
2289
  if (patched !== raw) {
2059
- fs8.writeFileSync(appPath, patched);
2290
+ fs9.writeFileSync(appPath, patched);
2060
2291
  }
2061
2292
  }
2062
2293
  async function syncDevClient(opts) {
@@ -2071,9 +2302,9 @@ async function syncDevClient(opts) {
2071
2302
  const packageName = config.android?.packageName;
2072
2303
  const appName = config.android?.appName;
2073
2304
  const packagePath = packageName.replace(/\./g, "/");
2074
- const javaDir = path8.join(rootDir, "app", "src", "main", "java", packagePath);
2075
- const kotlinDir = path8.join(rootDir, "app", "src", "main", "kotlin", packagePath);
2076
- if (!fs8.existsSync(javaDir) || !fs8.existsSync(kotlinDir)) {
2305
+ const javaDir = path9.join(rootDir, "app", "src", "main", "java", packagePath);
2306
+ const kotlinDir = path9.join(rootDir, "app", "src", "main", "kotlin", packagePath);
2307
+ if (!fs9.existsSync(javaDir) || !fs9.existsSync(kotlinDir)) {
2077
2308
  console.error("\u274C Android project not found. Run `tamer android create` first.");
2078
2309
  process.exit(1);
2079
2310
  }
@@ -2089,14 +2320,14 @@ async function syncDevClient(opts) {
2089
2320
  const [templateProviderSource] = await Promise.all([
2090
2321
  fetchAndPatchTemplateProvider(vars)
2091
2322
  ]);
2092
- fs8.writeFileSync(path8.join(javaDir, "TemplateProvider.java"), templateProviderSource);
2093
- fs8.writeFileSync(path8.join(kotlinDir, "MainActivity.kt"), getStandaloneMainActivity(vars));
2094
- patchAppLogService(path8.join(javaDir, "App.java"));
2095
- const appDir = path8.join(rootDir, "app");
2096
- const mainDir = path8.join(appDir, "src", "main");
2097
- const manifestPath = path8.join(mainDir, "AndroidManifest.xml");
2323
+ fs9.writeFileSync(path9.join(javaDir, "TemplateProvider.java"), templateProviderSource);
2324
+ fs9.writeFileSync(path9.join(kotlinDir, "MainActivity.kt"), getStandaloneMainActivity(vars));
2325
+ patchAppLogService(path9.join(javaDir, "App.java"));
2326
+ const appDir = path9.join(rootDir, "app");
2327
+ const mainDir = path9.join(appDir, "src", "main");
2328
+ const manifestPath = path9.join(mainDir, "AndroidManifest.xml");
2098
2329
  if (hasDevClient) {
2099
- const templateDir = path8.join(devClientPkg, "android", "templates");
2330
+ const templateDir = path9.join(devClientPkg, "android", "templates");
2100
2331
  const templateVars = { PACKAGE_NAME: packageName, APP_NAME: appName };
2101
2332
  const devClientFiles = [
2102
2333
  "DevClientManager.kt",
@@ -2105,13 +2336,13 @@ async function syncDevClient(opts) {
2105
2336
  "PortraitCaptureActivity.kt"
2106
2337
  ];
2107
2338
  for (const f of devClientFiles) {
2108
- const src = path8.join(templateDir, f);
2109
- if (fs8.existsSync(src)) {
2339
+ const src = path9.join(templateDir, f);
2340
+ if (fs9.existsSync(src)) {
2110
2341
  const content = readAndSubstituteTemplate2(src, templateVars);
2111
- fs8.writeFileSync(path8.join(kotlinDir, f), content);
2342
+ fs9.writeFileSync(path9.join(kotlinDir, f), content);
2112
2343
  }
2113
2344
  }
2114
- let manifest = fs8.readFileSync(manifestPath, "utf-8");
2345
+ let manifest = fs9.readFileSync(manifestPath, "utf-8");
2115
2346
  const projectActivityEntry = ' <activity android:name=".ProjectActivity" android:exported="false" android:taskAffinity="" android:launchMode="singleTask" android:documentLaunchMode="always" android:windowSoftInputMode="adjustResize" />';
2116
2347
  const portraitCaptureEntry = ' <activity android:name=".PortraitCaptureActivity" android:screenOrientation="portrait" android:stateNotNeeded="true" android:theme="@style/zxing_CaptureTheme" android:windowSoftInputMode="stateAlwaysHidden" />';
2117
2348
  if (!manifest.includes("ProjectActivity")) {
@@ -2133,16 +2364,16 @@ $1$2`);
2133
2364
  '$1 android:windowSoftInputMode="adjustResize"$2'
2134
2365
  );
2135
2366
  }
2136
- fs8.writeFileSync(manifestPath, manifest);
2367
+ fs9.writeFileSync(manifestPath, manifest);
2137
2368
  console.log("\u2705 Synced dev client (TemplateProvider, MainActivity, ProjectActivity, DevClientManager)");
2138
2369
  } else {
2139
2370
  for (const f of ["DevClientManager.kt", "DevServerPrefs.kt", "ProjectActivity.kt", "PortraitCaptureActivity.kt", "DevLauncherActivity.kt"]) {
2140
2371
  try {
2141
- fs8.rmSync(path8.join(kotlinDir, f));
2372
+ fs9.rmSync(path9.join(kotlinDir, f));
2142
2373
  } catch {
2143
2374
  }
2144
2375
  }
2145
- let manifest = fs8.readFileSync(manifestPath, "utf-8");
2376
+ let manifest = fs9.readFileSync(manifestPath, "utf-8");
2146
2377
  manifest = manifest.replace(/\s*<activity android:name="\.ProjectActivity"[^\/]*\/>\n?/g, "");
2147
2378
  manifest = manifest.replace(/\s*<activity android:name="\.PortraitCaptureActivity"[^\/]*\/>\n?/g, "");
2148
2379
  const mainActivityTag = manifest.match(/<activity[^>]*android:name="\.MainActivity"[^>]*>/);
@@ -2152,7 +2383,7 @@ $1$2`);
2152
2383
  '$1 android:windowSoftInputMode="adjustResize"$2'
2153
2384
  );
2154
2385
  }
2155
- fs8.writeFileSync(manifestPath, manifest);
2386
+ fs9.writeFileSync(manifestPath, manifest);
2156
2387
  console.log("\u2705 Synced (dev client disabled - use -d for debug build with dev client)");
2157
2388
  }
2158
2389
  }
@@ -2174,24 +2405,27 @@ async function bundleAndDeploy(opts = {}) {
2174
2405
  const destinationDir = androidAssetsDir;
2175
2406
  autolink_default({ includeDevClient });
2176
2407
  await syncDevClient_default({ includeDevClient });
2177
- const bundleExists = fs9.existsSync(lynxBundlePath);
2178
- if (!bundleExists) {
2179
- try {
2180
- console.log("\u{1F4E6} Building Lynx bundle...");
2181
- execSync2("npm run build", { stdio: "inherit", cwd: lynxProjectDir });
2182
- console.log("\u2705 Build completed successfully.");
2183
- } catch (error) {
2184
- console.error("\u274C Build process failed.");
2185
- process.exit(1);
2408
+ const iconPaths = resolveIconPaths(projectRoot, resolved.config);
2409
+ if (iconPaths) {
2410
+ const resDir = path10.join(resolved.androidAppDir, "src", "main", "res");
2411
+ if (applyAndroidLauncherIcons(resDir, iconPaths)) {
2412
+ console.log("\u2705 Synced Android launcher icon(s) from tamer.config.json");
2413
+ ensureAndroidManifestLauncherIcon(path10.join(resolved.androidAppDir, "src", "main", "AndroidManifest.xml"));
2186
2414
  }
2187
- } else {
2188
- console.log("\u{1F4E6} Using pre-built Lynx bundle.");
2189
2415
  }
2190
- if (includeDevClient && devClientBundlePath && !fs9.existsSync(devClientBundlePath)) {
2191
- const devClientDir = path9.dirname(path9.dirname(devClientBundlePath));
2416
+ try {
2417
+ console.log("\u{1F4E6} Building Lynx bundle...");
2418
+ execSync3("npm run build", { stdio: "inherit", cwd: lynxProjectDir });
2419
+ console.log("\u2705 Build completed successfully.");
2420
+ } catch (error) {
2421
+ console.error("\u274C Build process failed.");
2422
+ process.exit(1);
2423
+ }
2424
+ if (includeDevClient && devClientBundlePath && !fs10.existsSync(devClientBundlePath)) {
2425
+ const devClientDir = path10.dirname(path10.dirname(devClientBundlePath));
2192
2426
  try {
2193
2427
  console.log("\u{1F4E6} Building dev launcher (tamer-dev-client)...");
2194
- execSync2("npm run build", { stdio: "inherit", cwd: devClientDir });
2428
+ execSync3("npm run build", { stdio: "inherit", cwd: devClientDir });
2195
2429
  console.log("\u2705 Dev launcher build completed.");
2196
2430
  } catch (error) {
2197
2431
  console.error("\u274C Dev launcher build failed.");
@@ -2199,22 +2433,22 @@ async function bundleAndDeploy(opts = {}) {
2199
2433
  }
2200
2434
  }
2201
2435
  try {
2202
- fs9.mkdirSync(destinationDir, { recursive: true });
2436
+ fs10.mkdirSync(destinationDir, { recursive: true });
2203
2437
  if (release) {
2204
- const devClientAsset = path9.join(destinationDir, "dev-client.lynx.bundle");
2205
- if (fs9.existsSync(devClientAsset)) {
2206
- fs9.rmSync(devClientAsset);
2438
+ const devClientAsset = path10.join(destinationDir, "dev-client.lynx.bundle");
2439
+ if (fs10.existsSync(devClientAsset)) {
2440
+ fs10.rmSync(devClientAsset);
2207
2441
  console.log(`\u2728 Removed dev-client.lynx.bundle from assets (production build)`);
2208
2442
  }
2209
- } else if (includeDevClient && devClientBundlePath && fs9.existsSync(devClientBundlePath)) {
2210
- fs9.copyFileSync(devClientBundlePath, path9.join(destinationDir, "dev-client.lynx.bundle"));
2443
+ } else if (includeDevClient && devClientBundlePath && fs10.existsSync(devClientBundlePath)) {
2444
+ fs10.copyFileSync(devClientBundlePath, path10.join(destinationDir, "dev-client.lynx.bundle"));
2211
2445
  console.log(`\u2728 Copied dev-client.lynx.bundle to assets`);
2212
2446
  }
2213
- if (!fs9.existsSync(lynxBundlePath)) {
2447
+ if (!fs10.existsSync(lynxBundlePath)) {
2214
2448
  console.error(`\u274C Build output not found at: ${lynxBundlePath}`);
2215
2449
  process.exit(1);
2216
2450
  }
2217
- const distDir = path9.dirname(lynxBundlePath);
2451
+ const distDir = path10.dirname(lynxBundlePath);
2218
2452
  copyDistAssets(distDir, destinationDir, resolved.lynxBundleFile);
2219
2453
  console.log(`\u2728 Copied ${resolved.lynxBundleFile} to assets`);
2220
2454
  } catch (error) {
@@ -2225,8 +2459,8 @@ async function bundleAndDeploy(opts = {}) {
2225
2459
  var bundle_default = bundleAndDeploy;
2226
2460
 
2227
2461
  // src/android/build.ts
2228
- import path10 from "path";
2229
- import { execSync as execSync3 } from "child_process";
2462
+ import path11 from "path";
2463
+ import { execSync as execSync4 } from "child_process";
2230
2464
  async function buildApk(opts = {}) {
2231
2465
  let resolved;
2232
2466
  try {
@@ -2236,19 +2470,19 @@ async function buildApk(opts = {}) {
2236
2470
  }
2237
2471
  await bundle_default({ release: opts.release });
2238
2472
  const androidDir = resolved.androidDir;
2239
- const gradlew = path10.join(androidDir, process.platform === "win32" ? "gradlew.bat" : "gradlew");
2473
+ const gradlew = path11.join(androidDir, process.platform === "win32" ? "gradlew.bat" : "gradlew");
2240
2474
  const variant = opts.release ? "Release" : "Debug";
2241
2475
  const task = opts.install ? `install${variant}` : `assemble${variant}`;
2242
2476
  console.log(`
2243
2477
  \u{1F528} Building ${variant.toLowerCase()} APK${opts.install ? " and installing" : ""}...`);
2244
- execSync3(`"${gradlew}" ${task}`, { stdio: "inherit", cwd: androidDir });
2478
+ execSync4(`"${gradlew}" ${task}`, { stdio: "inherit", cwd: androidDir });
2245
2479
  console.log(`\u2705 APK ${opts.install ? "installed" : "built"} successfully.`);
2246
2480
  if (opts.install) {
2247
2481
  const packageName = resolved.config.android?.packageName;
2248
2482
  if (packageName) {
2249
2483
  try {
2250
2484
  console.log(`\u{1F680} Launching ${packageName}...`);
2251
- execSync3(`adb shell am start -n ${packageName}/.MainActivity`, { stdio: "inherit" });
2485
+ execSync4(`adb shell am start -n ${packageName}/.MainActivity`, { stdio: "inherit" });
2252
2486
  console.log("\u2705 App launched.");
2253
2487
  } catch (e) {
2254
2488
  console.warn("\u26A0\uFE0F Could not launch app. Is a device/emulator connected?");
@@ -2261,16 +2495,16 @@ async function buildApk(opts = {}) {
2261
2495
  var build_default = buildApk;
2262
2496
 
2263
2497
  // src/ios/create.ts
2264
- import fs11 from "fs";
2265
- import path12 from "path";
2498
+ import fs12 from "fs";
2499
+ import path13 from "path";
2266
2500
 
2267
2501
  // src/ios/getPod.ts
2268
- import { execSync as execSync4 } from "child_process";
2269
- import fs10 from "fs";
2270
- import path11 from "path";
2502
+ import { execSync as execSync5 } from "child_process";
2503
+ import fs11 from "fs";
2504
+ import path12 from "path";
2271
2505
  function isCocoaPodsInstalled() {
2272
2506
  try {
2273
- execSync4("command -v pod >/dev/null 2>&1");
2507
+ execSync5("command -v pod >/dev/null 2>&1");
2274
2508
  return true;
2275
2509
  } catch (error) {
2276
2510
  return false;
@@ -2289,19 +2523,19 @@ async function setupCocoaPods(rootDir) {
2289
2523
  }
2290
2524
  try {
2291
2525
  console.log("\u{1F4E6} CocoaPods is installed. Proceeding with dependency installation...");
2292
- const podfilePath = path11.join(rootDir, "Podfile");
2293
- if (!fs10.existsSync(podfilePath)) {
2526
+ const podfilePath = path12.join(rootDir, "Podfile");
2527
+ if (!fs11.existsSync(podfilePath)) {
2294
2528
  throw new Error(`Podfile not found at ${podfilePath}`);
2295
2529
  }
2296
2530
  console.log(`\u{1F680} Executing pod install in: ${rootDir}`);
2297
2531
  try {
2298
- execSync4("pod install", {
2532
+ execSync5("pod install", {
2299
2533
  cwd: rootDir,
2300
2534
  stdio: "inherit"
2301
2535
  });
2302
2536
  } catch {
2303
2537
  console.log("\u2139\uFE0F Retrying CocoaPods install with repo update...");
2304
- execSync4("pod install --repo-update", {
2538
+ execSync5("pod install --repo-update", {
2305
2539
  cwd: rootDir,
2306
2540
  stdio: "inherit"
2307
2541
  });
@@ -2316,7 +2550,7 @@ async function setupCocoaPods(rootDir) {
2316
2550
  // src/ios/create.ts
2317
2551
  import { randomBytes } from "crypto";
2318
2552
  function readAndSubstituteTemplate3(templatePath, vars) {
2319
- const raw = fs11.readFileSync(templatePath, "utf-8");
2553
+ const raw = fs12.readFileSync(templatePath, "utf-8");
2320
2554
  return Object.entries(vars).reduce(
2321
2555
  (s, [k, v]) => s.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), v),
2322
2556
  raw
@@ -2339,17 +2573,17 @@ var create2 = () => {
2339
2573
  process.exit(1);
2340
2574
  }
2341
2575
  const iosDir = config.paths?.iosDir ?? "ios";
2342
- const rootDir = path12.join(process.cwd(), iosDir);
2343
- const projectDir = path12.join(rootDir, appName);
2344
- const xcodeprojDir = path12.join(rootDir, `${appName}.xcodeproj`);
2576
+ const rootDir = path13.join(process.cwd(), iosDir);
2577
+ const projectDir = path13.join(rootDir, appName);
2578
+ const xcodeprojDir = path13.join(rootDir, `${appName}.xcodeproj`);
2345
2579
  const bridgingHeader = `${appName}-Bridging-Header.h`;
2346
2580
  function writeFile2(filePath, content) {
2347
- fs11.mkdirSync(path12.dirname(filePath), { recursive: true });
2348
- fs11.writeFileSync(filePath, content.trimStart(), "utf8");
2581
+ fs12.mkdirSync(path13.dirname(filePath), { recursive: true });
2582
+ fs12.writeFileSync(filePath, content.trimStart(), "utf8");
2349
2583
  }
2350
- if (fs11.existsSync(rootDir)) {
2584
+ if (fs12.existsSync(rootDir)) {
2351
2585
  console.log(`\u{1F9F9} Removing existing directory: ${rootDir}`);
2352
- fs11.rmSync(rootDir, { recursive: true, force: true });
2586
+ fs12.rmSync(rootDir, { recursive: true, force: true });
2353
2587
  }
2354
2588
  console.log(`\u{1F680} Creating a new Tamer4Lynx project in: ${rootDir}`);
2355
2589
  const ids = {
@@ -2385,7 +2619,7 @@ var create2 = () => {
2385
2619
  targetDebugConfig: generateId(),
2386
2620
  targetReleaseConfig: generateId()
2387
2621
  };
2388
- writeFile2(path12.join(rootDir, "Podfile"), `
2622
+ writeFile2(path13.join(rootDir, "Podfile"), `
2389
2623
  source 'https://cdn.cocoapods.org/'
2390
2624
 
2391
2625
  platform :ios, '13.0'
@@ -2445,6 +2679,12 @@ post_install do |installer|
2445
2679
  config.build_settings['CLANG_WARN_ENUM_CONVERSION'] = 'NO'
2446
2680
  end
2447
2681
  end
2682
+ if target.name == 'PrimJS'
2683
+ target.build_configurations.each do |config|
2684
+ config.build_settings['OTHER_CFLAGS'] = "$(inherited) -Wno-macro-redefined"
2685
+ config.build_settings['OTHER_CPLUSPLUSFLAGS'] = "$(inherited) -Wno-macro-redefined"
2686
+ end
2687
+ end
2448
2688
  end
2449
2689
  Dir.glob(File.join(installer.sandbox.root, 'Target Support Files', 'Lynx', '*.xcconfig')).each do |xcconfig_path|
2450
2690
  next unless File.file?(xcconfig_path)
@@ -2464,15 +2704,15 @@ end
2464
2704
  const hostPkg = findTamerHostPackage(process.cwd());
2465
2705
  const templateVars = { PACKAGE_NAME: bundleId, APP_NAME: appName, BUNDLE_ID: bundleId };
2466
2706
  if (hostPkg) {
2467
- const templateDir = path12.join(hostPkg, "ios", "templates");
2707
+ const templateDir = path13.join(hostPkg, "ios", "templates");
2468
2708
  for (const f of ["AppDelegate.swift", "SceneDelegate.swift", "ViewController.swift", "LynxProvider.swift", "LynxInitProcessor.swift"]) {
2469
- const srcPath = path12.join(templateDir, f);
2470
- if (fs11.existsSync(srcPath)) {
2471
- writeFile2(path12.join(projectDir, f), readAndSubstituteTemplate3(srcPath, templateVars));
2709
+ const srcPath = path13.join(templateDir, f);
2710
+ if (fs12.existsSync(srcPath)) {
2711
+ writeFile2(path13.join(projectDir, f), readAndSubstituteTemplate3(srcPath, templateVars));
2472
2712
  }
2473
2713
  }
2474
2714
  } else {
2475
- writeFile2(path12.join(projectDir, "AppDelegate.swift"), `
2715
+ writeFile2(path13.join(projectDir, "AppDelegate.swift"), `
2476
2716
  import UIKit
2477
2717
 
2478
2718
  @UIApplicationMain
@@ -2487,7 +2727,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
2487
2727
  }
2488
2728
  }
2489
2729
  `);
2490
- writeFile2(path12.join(projectDir, "SceneDelegate.swift"), `
2730
+ writeFile2(path13.join(projectDir, "SceneDelegate.swift"), `
2491
2731
  import UIKit
2492
2732
 
2493
2733
  class SceneDelegate: UIResponder, UIWindowSceneDelegate {
@@ -2501,7 +2741,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
2501
2741
  }
2502
2742
  }
2503
2743
  `);
2504
- writeFile2(path12.join(projectDir, "ViewController.swift"), `
2744
+ writeFile2(path13.join(projectDir, "ViewController.swift"), `
2505
2745
  import UIKit
2506
2746
  import Lynx
2507
2747
  import tamerinsets
@@ -2554,6 +2794,7 @@ class ViewController: UIViewController {
2554
2794
  private func setupLynxView() {
2555
2795
  let lv = buildLynxView()
2556
2796
  view.addSubview(lv)
2797
+ TamerInsetsModule.attachHostView(lv)
2557
2798
  lv.loadTemplate(fromURL: "main.lynx.bundle", initData: nil)
2558
2799
  self.lynxView = lv
2559
2800
  }
@@ -2571,7 +2812,7 @@ class ViewController: UIViewController {
2571
2812
  }
2572
2813
  }
2573
2814
  `);
2574
- writeFile2(path12.join(projectDir, "LynxProvider.swift"), `
2815
+ writeFile2(path13.join(projectDir, "LynxProvider.swift"), `
2575
2816
  import Foundation
2576
2817
 
2577
2818
  class LynxProvider: NSObject, LynxTemplateProvider {
@@ -2590,7 +2831,7 @@ class LynxProvider: NSObject, LynxTemplateProvider {
2590
2831
  }
2591
2832
  }
2592
2833
  `);
2593
- writeFile2(path12.join(projectDir, "LynxInitProcessor.swift"), `
2834
+ writeFile2(path13.join(projectDir, "LynxInitProcessor.swift"), `
2594
2835
  // Copyright 2024 The Lynx Authors. All rights reserved.
2595
2836
  // Licensed under the Apache License Version 2.0 that can be found in the
2596
2837
  // LICENSE file in the root directory of this source tree.
@@ -2630,7 +2871,7 @@ final class LynxInitProcessor {
2630
2871
  }
2631
2872
  `);
2632
2873
  }
2633
- writeFile2(path12.join(projectDir, bridgingHeader), `
2874
+ writeFile2(path13.join(projectDir, bridgingHeader), `
2634
2875
  #import <Lynx/LynxConfig.h>
2635
2876
  #import <Lynx/LynxEnv.h>
2636
2877
  #import <Lynx/LynxTemplateProvider.h>
@@ -2639,7 +2880,7 @@ final class LynxInitProcessor {
2639
2880
  #import <SDWebImage/SDWebImage.h>
2640
2881
  #import <SDWebImageWebPCoder/SDWebImageWebPCoder.h>
2641
2882
  `);
2642
- writeFile2(path12.join(projectDir, "Info.plist"), `
2883
+ writeFile2(path13.join(projectDir, "Info.plist"), `
2643
2884
  <?xml version="1.0" encoding="UTF-8"?>
2644
2885
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2645
2886
  <plist version="1.0">
@@ -2697,39 +2938,21 @@ final class LynxInitProcessor {
2697
2938
  </dict>
2698
2939
  </plist>
2699
2940
  `);
2700
- const appIconDir = path12.join(projectDir, "Assets.xcassets", "AppIcon.appiconset");
2701
- fs11.mkdirSync(appIconDir, { recursive: true });
2941
+ const appIconDir = path13.join(projectDir, "Assets.xcassets", "AppIcon.appiconset");
2942
+ fs12.mkdirSync(appIconDir, { recursive: true });
2702
2943
  const iconPaths = resolveIconPaths(process.cwd(), config);
2703
- if (iconPaths?.ios) {
2704
- const entries = fs11.readdirSync(iconPaths.ios, { withFileTypes: true });
2705
- for (const e of entries) {
2706
- const dest = path12.join(appIconDir, e.name);
2707
- if (e.isDirectory()) {
2708
- fs11.cpSync(path12.join(iconPaths.ios, e.name), dest, { recursive: true });
2709
- } else {
2710
- fs11.copyFileSync(path12.join(iconPaths.ios, e.name), dest);
2711
- }
2712
- }
2713
- console.log("\u2705 Copied iOS icon from tamer.config.json icon.ios");
2714
- } else if (iconPaths?.source) {
2715
- const ext = path12.extname(iconPaths.source) || ".png";
2716
- const icon1024 = `Icon-1024${ext}`;
2717
- fs11.copyFileSync(iconPaths.source, path12.join(appIconDir, icon1024));
2718
- writeFile2(path12.join(appIconDir, "Contents.json"), JSON.stringify({
2719
- images: [{ filename: icon1024, idiom: "universal", platform: "ios", size: "1024x1024" }],
2720
- info: { author: "xcode", version: 1 }
2721
- }, null, 2));
2722
- console.log("\u2705 Copied app icon from tamer.config.json icon.source");
2944
+ if (applyIosAppIconAssets(appIconDir, iconPaths)) {
2945
+ console.log(iconPaths?.ios ? "\u2705 Copied iOS icon from tamer.config.json icon.ios" : "\u2705 Copied app icon from tamer.config.json icon.source");
2723
2946
  } else {
2724
- writeFile2(path12.join(appIconDir, "Contents.json"), `
2947
+ writeFile2(path13.join(appIconDir, "Contents.json"), `
2725
2948
  {
2726
2949
  "images" : [ { "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ],
2727
2950
  "info" : { "author" : "xcode", "version" : 1 }
2728
2951
  }
2729
2952
  `);
2730
2953
  }
2731
- fs11.mkdirSync(xcodeprojDir, { recursive: true });
2732
- writeFile2(path12.join(xcodeprojDir, "project.pbxproj"), `
2954
+ fs12.mkdirSync(xcodeprojDir, { recursive: true });
2955
+ writeFile2(path13.join(xcodeprojDir, "project.pbxproj"), `
2733
2956
  // !$*UTF8*$!
2734
2957
  {
2735
2958
  archiveVersion = 1;
@@ -3015,450 +3238,16 @@ final class LynxInitProcessor {
3015
3238
  var create_default2 = create2;
3016
3239
 
3017
3240
  // src/ios/autolink.ts
3018
- import fs12 from "fs";
3019
- import path13 from "path";
3020
- import { execSync as execSync5 } from "child_process";
3021
- var autolink2 = () => {
3022
- let resolved;
3023
- try {
3024
- resolved = resolveHostPaths();
3025
- } catch (error) {
3026
- console.error(`\u274C Error loading configuration: ${error.message}`);
3027
- process.exit(1);
3028
- }
3029
- const projectRoot = resolved.projectRoot;
3030
- const iosProjectPath = resolved.iosDir;
3031
- function updateGeneratedSection(filePath, newContent, startMarker, endMarker) {
3032
- if (!fs12.existsSync(filePath)) {
3033
- console.warn(`\u26A0\uFE0F File not found, skipping update: ${filePath}`);
3034
- return;
3035
- }
3036
- let fileContent = fs12.readFileSync(filePath, "utf8");
3037
- const escapedStartMarker = startMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3038
- const escapedEndMarker = endMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3039
- const regex = new RegExp(`${escapedStartMarker}[\\s\\S]*?${escapedEndMarker}`, "g");
3040
- const replacementBlock = `${startMarker}
3041
- ${newContent}
3042
- ${endMarker}`;
3043
- if (regex.test(fileContent)) {
3044
- const firstStartIdx = fileContent.indexOf(startMarker);
3045
- fileContent = fileContent.replace(regex, "");
3046
- if (firstStartIdx !== -1) {
3047
- const before = fileContent.slice(0, firstStartIdx);
3048
- const after = fileContent.slice(firstStartIdx);
3049
- fileContent = `${before}${replacementBlock}${after}`;
3050
- } else {
3051
- fileContent += `
3052
- ${replacementBlock}
3053
- `;
3054
- }
3055
- } else {
3056
- console.warn(`\u26A0\uFE0F Could not find autolink markers in ${path13.basename(filePath)}. Appending to the end of the file.`);
3057
- fileContent += `
3058
- ${replacementBlock}
3241
+ import fs14 from "fs";
3242
+ import path15 from "path";
3243
+ import { execSync as execSync6 } from "child_process";
3244
+
3245
+ // src/common/hostNativeModulesManifest.ts
3246
+ var TAMER_HOST_NATIVE_MODULES_FILENAME = "tamer-host-native-modules.json";
3247
+ function buildHostNativeModulesManifestJson(moduleClassNames) {
3248
+ return `${JSON.stringify({ moduleClassNames }, null, 2)}
3059
3249
  `;
3060
- }
3061
- fs12.writeFileSync(filePath, fileContent, "utf8");
3062
- console.log(`\u2705 Updated autolinked section in ${path13.basename(filePath)}`);
3063
- }
3064
- function resolvePodDirectory(pkg) {
3065
- const configuredDir = path13.join(pkg.packagePath, pkg.config.ios?.podspecPath || ".");
3066
- if (fs12.existsSync(configuredDir)) {
3067
- return configuredDir;
3068
- }
3069
- const iosDir = path13.join(pkg.packagePath, "ios");
3070
- if (fs12.existsSync(iosDir)) {
3071
- const stack = [iosDir];
3072
- while (stack.length > 0) {
3073
- const current = stack.pop();
3074
- try {
3075
- const entries = fs12.readdirSync(current, { withFileTypes: true });
3076
- const podspec = entries.find((entry) => entry.isFile() && entry.name.endsWith(".podspec"));
3077
- if (podspec) {
3078
- return current;
3079
- }
3080
- for (const entry of entries) {
3081
- if (entry.isDirectory()) {
3082
- stack.push(path13.join(current, entry.name));
3083
- }
3084
- }
3085
- } catch {
3086
- }
3087
- }
3088
- }
3089
- return configuredDir;
3090
- }
3091
- function resolvePodName(pkg) {
3092
- const fullPodspecDir = resolvePodDirectory(pkg);
3093
- if (fs12.existsSync(fullPodspecDir)) {
3094
- try {
3095
- const files = fs12.readdirSync(fullPodspecDir);
3096
- const podspecFile = files.find((f) => f.endsWith(".podspec"));
3097
- if (podspecFile) return podspecFile.replace(".podspec", "");
3098
- } catch {
3099
- }
3100
- }
3101
- return pkg.name.split("/").pop().replace(/-/g, "");
3102
- }
3103
- function updatePodfile(packages) {
3104
- const podfilePath = path13.join(iosProjectPath, "Podfile");
3105
- let scriptContent = ` # This section is automatically generated by Tamer4Lynx.
3106
- # Manual edits will be overwritten.`;
3107
- const iosPackages = packages.filter((p) => p.config.ios);
3108
- if (iosPackages.length > 0) {
3109
- iosPackages.forEach((pkg) => {
3110
- const relativePath = path13.relative(iosProjectPath, resolvePodDirectory(pkg));
3111
- const podName = resolvePodName(pkg);
3112
- scriptContent += `
3113
- pod '${podName}', :path => '${relativePath}'`;
3114
- });
3115
- } else {
3116
- scriptContent += `
3117
- # No native modules found by Tamer4Lynx autolinker.`;
3118
- }
3119
- updateGeneratedSection(podfilePath, scriptContent.trim(), "# GENERATED AUTOLINK DEPENDENCIES START", "# GENERATED AUTOLINK DEPENDENCIES END");
3120
- }
3121
- function ensureXElementPod() {
3122
- const podfilePath = path13.join(iosProjectPath, "Podfile");
3123
- if (!fs12.existsSync(podfilePath)) return;
3124
- let content = fs12.readFileSync(podfilePath, "utf8");
3125
- if (content.includes("pod 'XElement'")) return;
3126
- const lynxVersionMatch = content.match(/pod\s+'Lynx',\s*'([^']+)'/);
3127
- const lynxVersion = lynxVersionMatch?.[1] ?? "3.6.0";
3128
- const xelementLine = `
3129
- pod 'XElement', '${lynxVersion}'`;
3130
- const insertAfter = /pod\s+'LynxService'[^\n]*(?:\n\s*'[^']*',?\s*)*/;
3131
- const serviceMatch = content.match(insertAfter);
3132
- if (serviceMatch) {
3133
- const idx = serviceMatch.index + serviceMatch[0].length;
3134
- content = content.slice(0, idx) + xelementLine + content.slice(idx);
3135
- } else {
3136
- content = content.replace(
3137
- /(# GENERATED AUTOLINK DEPENDENCIES START)/,
3138
- `pod 'XElement', '${lynxVersion}'
3139
-
3140
- $1`
3141
- );
3142
- }
3143
- fs12.writeFileSync(podfilePath, content, "utf8");
3144
- console.log(`\u2705 Added XElement pod (v${lynxVersion}) to Podfile`);
3145
- }
3146
- function ensureLynxPatchInPodfile() {
3147
- const podfilePath = path13.join(iosProjectPath, "Podfile");
3148
- if (!fs12.existsSync(podfilePath)) return;
3149
- let content = fs12.readFileSync(podfilePath, "utf8");
3150
- if (content.includes("content.gsub(/\\btypeof\\(/, '__typeof__(')")) return;
3151
- const patch = `
3152
- Dir.glob(File.join(installer.sandbox.root, 'Lynx/platform/darwin/**/*.{m,mm}')).each do |lynx_source|
3153
- next unless File.file?(lynx_source)
3154
- content = File.read(lynx_source)
3155
- next unless content.match?(/\\btypeof\\(/)
3156
- File.chmod(0644, lynx_source) rescue nil
3157
- File.write(lynx_source, content.gsub(/\\btypeof\\(/, '__typeof__('))
3158
- end`;
3159
- content = content.replace(/(\n end\s*\n)(end\s*)$/, `$1${patch}
3160
- $2`);
3161
- fs12.writeFileSync(podfilePath, content, "utf8");
3162
- console.log("\u2705 Added Lynx typeof patch to Podfile post_install.");
3163
- }
3164
- function ensurePodBuildSettings() {
3165
- const podfilePath = path13.join(iosProjectPath, "Podfile");
3166
- if (!fs12.existsSync(podfilePath)) return;
3167
- let content = fs12.readFileSync(podfilePath, "utf8");
3168
- let changed = false;
3169
- if (!content.includes("CLANG_ENABLE_EXPLICIT_MODULES")) {
3170
- content = content.replace(
3171
- /config\.build_settings\['IPHONEOS_DEPLOYMENT_TARGET'\]\s*=\s*'[^']*'/,
3172
- `$&
3173
- config.build_settings['CLANG_ENABLE_EXPLICIT_MODULES'] = 'NO'
3174
- config.build_settings['ONLY_ACTIVE_ARCH'] = 'YES'`
3175
- );
3176
- changed = true;
3177
- }
3178
- if (!content.includes("gsub('-Werror'")) {
3179
- const xcconfigStrip = `
3180
- Dir.glob(File.join(installer.sandbox.root, 'Target Support Files', 'Lynx', '*.xcconfig')).each do |xcconfig_path|
3181
- next unless File.file?(xcconfig_path)
3182
- content = File.read(xcconfig_path)
3183
- next unless content.include?('-Werror')
3184
- File.write(xcconfig_path, content.gsub('-Werror', ''))
3185
- end`;
3186
- content = content.replace(
3187
- /(Dir\.glob.*?Lynx\/platform\/darwin)/s,
3188
- `${xcconfigStrip}
3189
- $1`
3190
- );
3191
- changed = true;
3192
- }
3193
- if (changed) {
3194
- fs12.writeFileSync(podfilePath, content, "utf8");
3195
- console.log("\u2705 Added Xcode compatibility build settings to Podfile post_install.");
3196
- }
3197
- }
3198
- function updateLynxInitProcessor(packages) {
3199
- const appNameFromConfig = resolved.config.ios?.appName;
3200
- const candidatePaths = [];
3201
- if (appNameFromConfig) {
3202
- candidatePaths.push(path13.join(iosProjectPath, appNameFromConfig, "LynxInitProcessor.swift"));
3203
- }
3204
- candidatePaths.push(path13.join(iosProjectPath, "LynxInitProcessor.swift"));
3205
- const found = candidatePaths.find((p) => fs12.existsSync(p));
3206
- const lynxInitPath = found ?? candidatePaths[0];
3207
- const iosPackages = packages.filter((p) => getIosModuleClassNames(p.config.ios).length > 0 || Object.keys(getIosElements(p.config.ios)).length > 0);
3208
- const seenModules = /* @__PURE__ */ new Set();
3209
- const seenElements = /* @__PURE__ */ new Set();
3210
- const packagesWithContributions = /* @__PURE__ */ new Set();
3211
- for (const pkg of iosPackages) {
3212
- let hasUnique = false;
3213
- for (const cls of getIosModuleClassNames(pkg.config.ios)) {
3214
- if (!seenModules.has(cls)) {
3215
- seenModules.add(cls);
3216
- hasUnique = true;
3217
- } else {
3218
- console.warn(`\u26A0\uFE0F Skipping duplicate module "${cls}" from ${pkg.name} (already registered by another package)`);
3219
- }
3220
- }
3221
- for (const tag of Object.keys(getIosElements(pkg.config.ios))) {
3222
- if (!seenElements.has(tag)) {
3223
- seenElements.add(tag);
3224
- hasUnique = true;
3225
- } else {
3226
- console.warn(`\u26A0\uFE0F Skipping duplicate element "${tag}" from ${pkg.name} (already registered by another package)`);
3227
- }
3228
- }
3229
- if (hasUnique) packagesWithContributions.add(pkg);
3230
- }
3231
- const importPackages = iosPackages.filter((p) => packagesWithContributions.has(p));
3232
- function updateImportsSection(filePath, pkgs) {
3233
- const startMarker = "// GENERATED IMPORTS START";
3234
- const endMarker = "// GENERATED IMPORTS END";
3235
- if (pkgs.length === 0) {
3236
- const placeholder = "// No native imports found by Tamer4Lynx autolinker.";
3237
- updateGeneratedSection(filePath, placeholder, startMarker, endMarker);
3238
- return;
3239
- }
3240
- const imports = pkgs.map((pkg) => {
3241
- const podName = resolvePodName(pkg);
3242
- return `import ${podName}`;
3243
- }).join("\n");
3244
- const fileContent = fs12.readFileSync(filePath, "utf8");
3245
- if (fileContent.indexOf(startMarker) !== -1) {
3246
- updateGeneratedSection(filePath, imports, startMarker, endMarker);
3247
- return;
3248
- }
3249
- const importRegex = /^(import\s+[^\r\n]+)\r?\n/gm;
3250
- let match = null;
3251
- let lastMatchEnd = -1;
3252
- while ((match = importRegex.exec(fileContent)) !== null) {
3253
- lastMatchEnd = importRegex.lastIndex;
3254
- }
3255
- const block = `${startMarker}
3256
- ${imports}
3257
- ${endMarker}`;
3258
- let newContent;
3259
- if (lastMatchEnd !== -1) {
3260
- const before = fileContent.slice(0, lastMatchEnd);
3261
- const after = fileContent.slice(lastMatchEnd);
3262
- newContent = `${before}
3263
- ${block}
3264
- ${after}`;
3265
- } else {
3266
- const foundationIdx = fileContent.indexOf("import Foundation");
3267
- if (foundationIdx !== -1) {
3268
- const lineEnd = fileContent.indexOf("\n", foundationIdx);
3269
- const insertPos = lineEnd !== -1 ? lineEnd + 1 : foundationIdx + "import Foundation".length;
3270
- const before = fileContent.slice(0, insertPos);
3271
- const after = fileContent.slice(insertPos);
3272
- newContent = `${before}
3273
- ${block}
3274
- ${after}`;
3275
- } else {
3276
- newContent = `${block}
3277
-
3278
- ${fileContent}`;
3279
- }
3280
- }
3281
- fs12.writeFileSync(filePath, newContent, "utf8");
3282
- console.log(`\u2705 Updated imports in ${path13.basename(filePath)}`);
3283
- }
3284
- updateImportsSection(lynxInitPath, importPackages);
3285
- if (importPackages.length === 0) {
3286
- const placeholder = " // No native modules found by Tamer4Lynx autolinker.";
3287
- updateGeneratedSection(lynxInitPath, placeholder, "// GENERATED AUTOLINK START", "// GENERATED AUTOLINK END");
3288
- return;
3289
- }
3290
- const seenModules2 = /* @__PURE__ */ new Set();
3291
- const seenElements2 = /* @__PURE__ */ new Set();
3292
- const blocks = importPackages.flatMap((pkg) => {
3293
- const classNames = getIosModuleClassNames(pkg.config.ios);
3294
- const moduleBlocks = classNames.filter((cls) => {
3295
- if (seenModules2.has(cls)) return false;
3296
- seenModules2.add(cls);
3297
- return true;
3298
- }).map((classNameRaw) => [
3299
- ` // Register module from package: ${pkg.name}`,
3300
- ` globalConfig.register(${classNameRaw}.self)`
3301
- ].join("\n"));
3302
- const elementBlocks = Object.entries(getIosElements(pkg.config.ios)).filter(([tagName]) => {
3303
- if (seenElements2.has(tagName)) return false;
3304
- seenElements2.add(tagName);
3305
- return true;
3306
- }).map(([tagName, classNameRaw]) => [
3307
- ` // Register element from package: ${pkg.name}`,
3308
- ` globalConfig.registerUI(${classNameRaw}.self, withName: "${tagName}")`
3309
- ].join("\n"));
3310
- return [...moduleBlocks, ...elementBlocks];
3311
- });
3312
- const content = blocks.join("\n\n");
3313
- updateGeneratedSection(lynxInitPath, content, "// GENERATED AUTOLINK START", "// GENERATED AUTOLINK END");
3314
- }
3315
- function findInfoPlist() {
3316
- const appNameFromConfig = resolved.config.ios?.appName;
3317
- const candidates = [];
3318
- if (appNameFromConfig) {
3319
- candidates.push(path13.join(iosProjectPath, appNameFromConfig, "Info.plist"));
3320
- }
3321
- candidates.push(path13.join(iosProjectPath, "Info.plist"));
3322
- return candidates.find((p) => fs12.existsSync(p)) ?? null;
3323
- }
3324
- function readPlistXml(plistPath) {
3325
- return fs12.readFileSync(plistPath, "utf8");
3326
- }
3327
- function syncInfoPlistPermissions(packages) {
3328
- const plistPath = findInfoPlist();
3329
- if (!plistPath) return;
3330
- const allPermissions = {};
3331
- for (const pkg of packages) {
3332
- const iosPerms = pkg.config.ios?.iosPermissions;
3333
- if (iosPerms) {
3334
- for (const [key, desc] of Object.entries(iosPerms)) {
3335
- if (!allPermissions[key]) {
3336
- allPermissions[key] = desc;
3337
- }
3338
- }
3339
- }
3340
- }
3341
- if (Object.keys(allPermissions).length === 0) return;
3342
- let plist = readPlistXml(plistPath);
3343
- let added = 0;
3344
- for (const [key, desc] of Object.entries(allPermissions)) {
3345
- if (plist.includes(`<key>${key}</key>`)) continue;
3346
- const insertion = ` <key>${key}</key>
3347
- <string>${desc}</string>
3348
- `;
3349
- plist = plist.replace(
3350
- /(<\/dict>\s*<\/plist>)/,
3351
- `${insertion}$1`
3352
- );
3353
- added++;
3354
- }
3355
- if (added > 0) {
3356
- fs12.writeFileSync(plistPath, plist, "utf8");
3357
- console.log(`\u2705 Synced ${added} Info.plist permission description(s)`);
3358
- }
3359
- }
3360
- function syncInfoPlistUrlSchemes() {
3361
- const urlSchemes = resolved.config.ios?.urlSchemes;
3362
- if (!urlSchemes || urlSchemes.length === 0) return;
3363
- const plistPath = findInfoPlist();
3364
- if (!plistPath) return;
3365
- let plist = readPlistXml(plistPath);
3366
- const schemesXml = urlSchemes.map((s) => {
3367
- const role = s.role ?? "Editor";
3368
- return ` <dict>
3369
- <key>CFBundleTypeRole</key>
3370
- <string>${role}</string>
3371
- <key>CFBundleURLSchemes</key>
3372
- <array>
3373
- <string>${s.scheme}</string>
3374
- </array>
3375
- </dict>`;
3376
- }).join("\n");
3377
- const generatedBlock = ` <key>CFBundleURLTypes</key>
3378
- <array>
3379
- <!-- GENERATED URL SCHEMES START -->
3380
- ${schemesXml}
3381
- <!-- GENERATED URL SCHEMES END -->
3382
- </array>`;
3383
- const startMarker = "<!-- GENERATED URL SCHEMES START -->";
3384
- const endMarker = "<!-- GENERATED URL SCHEMES END -->";
3385
- if (plist.includes(startMarker) && plist.includes(endMarker)) {
3386
- const regex = new RegExp(
3387
- `${startMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${endMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`,
3388
- "g"
3389
- );
3390
- plist = plist.replace(regex, `${startMarker}
3391
- ${schemesXml}
3392
- ${endMarker}`);
3393
- } else if (plist.includes("<key>CFBundleURLTypes</key>")) {
3394
- console.log("\u2139\uFE0F CFBundleURLTypes exists but has no generated markers. Skipping URL scheme sync.");
3395
- return;
3396
- } else {
3397
- plist = plist.replace(
3398
- /(<\/dict>\s*<\/plist>)/,
3399
- `${generatedBlock}
3400
- $1`
3401
- );
3402
- }
3403
- fs12.writeFileSync(plistPath, plist, "utf8");
3404
- console.log(`\u2705 Synced ${urlSchemes.length} iOS URL scheme(s) into Info.plist`);
3405
- }
3406
- function runPodInstall(forcePath) {
3407
- const podfilePath = forcePath ?? path13.join(iosProjectPath, "Podfile");
3408
- if (!fs12.existsSync(podfilePath)) {
3409
- console.log("\u2139\uFE0F No Podfile found in ios directory; skipping `pod install`.");
3410
- return;
3411
- }
3412
- const cwd = path13.dirname(podfilePath);
3413
- try {
3414
- console.log(`\u2139\uFE0F Running \`pod install\` in ${cwd}...`);
3415
- try {
3416
- execSync5("pod install", { cwd, stdio: "inherit" });
3417
- } catch {
3418
- console.log("\u2139\uFE0F Retrying `pod install` with repo update...");
3419
- execSync5("pod install --repo-update", { cwd, stdio: "inherit" });
3420
- }
3421
- console.log("\u2705 `pod install` completed successfully.");
3422
- } catch (e) {
3423
- console.warn(`\u26A0\uFE0F 'pod install' failed: ${e.message}`);
3424
- console.log("\u26A0\uFE0F You can run `pod install` manually in the ios directory.");
3425
- }
3426
- }
3427
- function run() {
3428
- console.log("\u{1F50E} Finding Lynx extension packages (lynx.ext.json / tamer.json)...");
3429
- const packages = discoverModules(projectRoot).filter((p) => p.config.ios);
3430
- if (packages.length > 0) {
3431
- console.log(`Found ${packages.length} package(s): ${packages.map((p) => p.name).join(", ")}`);
3432
- } else {
3433
- console.log("\u2139\uFE0F No Tamer4Lynx native packages found.");
3434
- }
3435
- updatePodfile(packages);
3436
- ensureXElementPod();
3437
- ensureLynxPatchInPodfile();
3438
- ensurePodBuildSettings();
3439
- updateLynxInitProcessor(packages);
3440
- syncInfoPlistPermissions(packages);
3441
- syncInfoPlistUrlSchemes();
3442
- const appNameFromConfig = resolved.config.ios?.appName;
3443
- if (appNameFromConfig) {
3444
- const appPodfile = path13.join(iosProjectPath, appNameFromConfig, "Podfile");
3445
- if (fs12.existsSync(appPodfile)) {
3446
- runPodInstall(appPodfile);
3447
- console.log("\u2728 Autolinking complete for iOS.");
3448
- return;
3449
- }
3450
- }
3451
- runPodInstall();
3452
- console.log("\u2728 Autolinking complete for iOS.");
3453
- }
3454
- run();
3455
- };
3456
- var autolink_default2 = autolink2;
3457
-
3458
- // src/ios/bundle.ts
3459
- import fs14 from "fs";
3460
- import path15 from "path";
3461
- import { execSync as execSync6 } from "child_process";
3250
+ }
3462
3251
 
3463
3252
  // src/ios/syncHost.ts
3464
3253
  import fs13 from "fs";
@@ -3615,344 +3404,842 @@ function writeFile(filePath, content) {
3615
3404
  function getAppDelegateSwift() {
3616
3405
  return `import UIKit
3617
3406
 
3618
- @UIApplicationMain
3619
- class AppDelegate: UIResponder, UIApplicationDelegate {
3620
- func application(
3621
- _ application: UIApplication,
3622
- didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
3623
- ) -> Bool {
3624
- LynxInitProcessor.shared.setupEnvironment()
3625
- return true
3407
+ @UIApplicationMain
3408
+ class AppDelegate: UIResponder, UIApplicationDelegate {
3409
+ func application(
3410
+ _ application: UIApplication,
3411
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
3412
+ ) -> Bool {
3413
+ LynxInitProcessor.shared.setupEnvironment()
3414
+ return true
3415
+ }
3416
+
3417
+ func application(
3418
+ _ application: UIApplication,
3419
+ configurationForConnecting connectingSceneSession: UISceneSession,
3420
+ options: UIScene.ConnectionOptions
3421
+ ) -> UISceneConfiguration {
3422
+ return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
3423
+ }
3424
+ }
3425
+ `;
3426
+ }
3427
+ function getSceneDelegateSwift() {
3428
+ return `import UIKit
3429
+
3430
+ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
3431
+ var window: UIWindow?
3432
+
3433
+ func scene(
3434
+ _ scene: UIScene,
3435
+ willConnectTo session: UISceneSession,
3436
+ options connectionOptions: UIScene.ConnectionOptions
3437
+ ) {
3438
+ guard let windowScene = scene as? UIWindowScene else { return }
3439
+ window = UIWindow(windowScene: windowScene)
3440
+ window?.rootViewController = ViewController()
3441
+ window?.makeKeyAndVisible()
3442
+ }
3443
+ }
3444
+ `;
3445
+ }
3446
+ function getViewControllerSwift() {
3447
+ return `import UIKit
3448
+ import Lynx
3449
+ import tamerinsets
3450
+
3451
+ class ViewController: UIViewController {
3452
+ private var lynxView: LynxView?
3453
+
3454
+ override func viewDidLoad() {
3455
+ super.viewDidLoad()
3456
+ view.backgroundColor = .black
3457
+ edgesForExtendedLayout = .all
3458
+ extendedLayoutIncludesOpaqueBars = true
3459
+ additionalSafeAreaInsets = .zero
3460
+ view.insetsLayoutMarginsFromSafeArea = false
3461
+ view.preservesSuperviewLayoutMargins = false
3462
+ viewRespectsSystemMinimumLayoutMargins = false
3463
+ }
3464
+
3465
+ override func viewDidLayoutSubviews() {
3466
+ super.viewDidLayoutSubviews()
3467
+ guard view.bounds.width > 0, view.bounds.height > 0 else { return }
3468
+ if lynxView == nil {
3469
+ setupLynxView()
3470
+ } else {
3471
+ applyFullscreenLayout(to: lynxView!)
3472
+ }
3473
+ }
3474
+
3475
+ override func viewSafeAreaInsetsDidChange() {
3476
+ super.viewSafeAreaInsetsDidChange()
3477
+ TamerInsetsModule.reRequestInsets()
3478
+ }
3479
+
3480
+ override var preferredStatusBarStyle: UIStatusBarStyle { .lightContent }
3481
+
3482
+ private func buildLynxView() -> LynxView {
3483
+ let bounds = view.bounds
3484
+ let lv = LynxView { builder in
3485
+ builder.config = LynxConfig(provider: LynxProvider())
3486
+ builder.screenSize = bounds.size
3487
+ builder.fontScale = 1.0
3488
+ }
3489
+ lv.autoresizingMask = [.flexibleWidth, .flexibleHeight]
3490
+ lv.insetsLayoutMarginsFromSafeArea = false
3491
+ lv.preservesSuperviewLayoutMargins = false
3492
+ applyFullscreenLayout(to: lv)
3493
+ return lv
3494
+ }
3495
+
3496
+ private func setupLynxView() {
3497
+ let lv = buildLynxView()
3498
+ view.addSubview(lv)
3499
+ TamerInsetsModule.attachHostView(lv)
3500
+ lv.loadTemplate(fromURL: "main.lynx.bundle", initData: nil)
3501
+ self.lynxView = lv
3502
+ }
3503
+
3504
+ private func applyFullscreenLayout(to lynxView: LynxView) {
3505
+ let bounds = view.bounds
3506
+ let size = bounds.size
3507
+ lynxView.frame = bounds
3508
+ lynxView.updateScreenMetrics(withWidth: size.width, height: size.height)
3509
+ lynxView.updateViewport(withPreferredLayoutWidth: size.width, preferredLayoutHeight: size.height, needLayout: true)
3510
+ lynxView.preferredLayoutWidth = size.width
3511
+ lynxView.preferredLayoutHeight = size.height
3512
+ lynxView.layoutWidthMode = .exact
3513
+ lynxView.layoutHeightMode = .exact
3514
+ }
3515
+ }
3516
+ `;
3517
+ }
3518
+ function getDevViewControllerSwift() {
3519
+ return `import UIKit
3520
+ import Lynx
3521
+ import tamerdevclient
3522
+ import tamerinsets
3523
+
3524
+ class ViewController: UIViewController {
3525
+ private var lynxView: LynxView?
3526
+
3527
+ override func viewDidLoad() {
3528
+ super.viewDidLoad()
3529
+ view.backgroundColor = .black
3530
+ edgesForExtendedLayout = .all
3531
+ extendedLayoutIncludesOpaqueBars = true
3532
+ additionalSafeAreaInsets = .zero
3533
+ view.insetsLayoutMarginsFromSafeArea = false
3534
+ view.preservesSuperviewLayoutMargins = false
3535
+ viewRespectsSystemMinimumLayoutMargins = false
3536
+ setupLynxView()
3537
+ setupDevClientModule()
3626
3538
  }
3627
3539
 
3628
- func application(
3629
- _ application: UIApplication,
3630
- configurationForConnecting connectingSceneSession: UISceneSession,
3631
- options: UIScene.ConnectionOptions
3632
- ) -> UISceneConfiguration {
3633
- return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
3540
+ override func viewDidLayoutSubviews() {
3541
+ super.viewDidLayoutSubviews()
3542
+ if let lynxView = lynxView {
3543
+ applyFullscreenLayout(to: lynxView)
3544
+ }
3545
+ }
3546
+
3547
+ override func viewSafeAreaInsetsDidChange() {
3548
+ super.viewSafeAreaInsetsDidChange()
3549
+ TamerInsetsModule.reRequestInsets()
3550
+ }
3551
+
3552
+ override var preferredStatusBarStyle: UIStatusBarStyle { TamerPreferredStatusBar.style }
3553
+
3554
+ private func setupLynxView() {
3555
+ let size = fullscreenBounds().size
3556
+ let lv = LynxView { builder in
3557
+ builder.config = LynxConfig(provider: DevTemplateProvider())
3558
+ builder.screenSize = size
3559
+ builder.fontScale = 1.0
3560
+ }
3561
+ lv.autoresizingMask = [.flexibleWidth, .flexibleHeight]
3562
+ lv.insetsLayoutMarginsFromSafeArea = false
3563
+ lv.preservesSuperviewLayoutMargins = false
3564
+ view.addSubview(lv)
3565
+ applyFullscreenLayout(to: lv)
3566
+ TamerInsetsModule.attachHostView(lv)
3567
+ lv.loadTemplate(fromURL: "dev-client.lynx.bundle", initData: nil)
3568
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self, weak lv] in
3569
+ guard let self, let lv else { return }
3570
+ self.applyFullscreenLayout(to: lv)
3571
+ }
3572
+ self.lynxView = lv
3573
+ }
3574
+
3575
+ private func applyFullscreenLayout(to lynxView: LynxView) {
3576
+ let bounds = fullscreenBounds()
3577
+ let size = bounds.size
3578
+ lynxView.frame = bounds
3579
+ lynxView.updateScreenMetrics(withWidth: size.width, height: size.height)
3580
+ lynxView.updateViewport(withPreferredLayoutWidth: size.width, preferredLayoutHeight: size.height, needLayout: true)
3581
+ lynxView.preferredLayoutWidth = size.width
3582
+ lynxView.preferredLayoutHeight = size.height
3583
+ lynxView.layoutWidthMode = .exact
3584
+ lynxView.layoutHeightMode = .exact
3585
+ }
3586
+
3587
+ private func fullscreenBounds() -> CGRect {
3588
+ let bounds = view.bounds
3589
+ if bounds.width > 0, bounds.height > 0 { return bounds }
3590
+ return UIScreen.main.bounds
3591
+ }
3592
+
3593
+ private func setupDevClientModule() {
3594
+ DevClientModule.presentQRScanner = { [weak self] completion in
3595
+ let scanner = QRScannerViewController()
3596
+ scanner.onResult = { url in
3597
+ scanner.dismiss(animated: true) { completion(url) }
3598
+ }
3599
+ scanner.modalPresentationStyle = .fullScreen
3600
+ self?.present(scanner, animated: true)
3601
+ }
3602
+
3603
+ DevClientModule.reloadProjectHandler = { [weak self] in
3604
+ guard let self = self else { return }
3605
+ let projectVC = ProjectViewController()
3606
+ projectVC.modalPresentationStyle = .fullScreen
3607
+ self.present(projectVC, animated: true)
3608
+ }
3634
3609
  }
3635
3610
  }
3636
3611
  `;
3637
3612
  }
3638
- function getSceneDelegateSwift() {
3639
- return `import UIKit
3613
+ function patchInfoPlist(infoPlistPath) {
3614
+ if (!fs13.existsSync(infoPlistPath)) return;
3615
+ let content = fs13.readFileSync(infoPlistPath, "utf8");
3616
+ content = content.replace(/\s*<key>UIMainStoryboardFile<\/key>\s*<string>[^<]*<\/string>/g, "");
3617
+ if (!content.includes("UILaunchStoryboardName")) {
3618
+ content = content.replace("</dict>\n</plist>", ` <key>UILaunchStoryboardName</key>
3619
+ <string>LaunchScreen</string>
3620
+ </dict>
3621
+ </plist>`);
3622
+ console.log("\u2705 Added UILaunchStoryboardName to Info.plist");
3623
+ }
3624
+ if (!content.includes("UIApplicationSceneManifest")) {
3625
+ const sceneManifest = ` <key>UIApplicationSceneManifest</key>
3626
+ <dict>
3627
+ <key>UIApplicationSupportsMultipleScenes</key>
3628
+ <false/>
3629
+ <key>UISceneConfigurations</key>
3630
+ <dict>
3631
+ <key>UIWindowSceneSessionRoleApplication</key>
3632
+ <array>
3633
+ <dict>
3634
+ <key>UISceneConfigurationName</key>
3635
+ <string>Default Configuration</string>
3636
+ <key>UISceneDelegateClassName</key>
3637
+ <string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
3638
+ </dict>
3639
+ </array>
3640
+ </dict>
3641
+ </dict>`;
3642
+ content = content.replace("</dict>\n</plist>", `${sceneManifest}
3643
+ </dict>
3644
+ </plist>`);
3645
+ console.log("\u2705 Added UIApplicationSceneManifest to Info.plist");
3646
+ }
3647
+ fs13.writeFileSync(infoPlistPath, content, "utf8");
3648
+ }
3649
+ function getSimpleLynxProviderSwift() {
3650
+ return `import Foundation
3651
+ import Lynx
3640
3652
 
3641
- class SceneDelegate: UIResponder, UIWindowSceneDelegate {
3642
- var window: UIWindow?
3653
+ class LynxProvider: NSObject, LynxTemplateProvider {
3654
+ func loadTemplate(withUrl url: String!, onComplete callback: LynxTemplateLoadBlock!) {
3655
+ DispatchQueue.global(qos: .background).async {
3656
+ guard let url = url,
3657
+ let bundleUrl = Bundle.main.url(forResource: url, withExtension: nil),
3658
+ let data = try? Data(contentsOf: bundleUrl) else {
3659
+ let err = NSError(domain: "LynxProvider", code: 404,
3660
+ userInfo: [NSLocalizedDescriptionKey: "Bundle not found: \\(url ?? "nil")"])
3661
+ callback?(nil, err)
3662
+ return
3663
+ }
3664
+ callback?(data, nil)
3665
+ }
3666
+ }
3667
+ }
3668
+ `;
3669
+ }
3670
+ function readTemplateOrFallback(devClientPkg, templateName, fallback, vars = {}) {
3671
+ if (devClientPkg) {
3672
+ const tplPath = path14.join(devClientPkg, "ios", "templates", templateName);
3673
+ if (fs13.existsSync(tplPath)) {
3674
+ let content = fs13.readFileSync(tplPath, "utf8");
3675
+ for (const [k, v] of Object.entries(vars)) {
3676
+ content = content.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), v);
3677
+ }
3678
+ return content;
3679
+ }
3680
+ }
3681
+ return fallback;
3682
+ }
3683
+ function syncHostIos(opts) {
3684
+ const resolved = resolveHostPaths();
3685
+ const appName = resolved.config.ios?.appName;
3686
+ const release = opts?.release === true;
3687
+ const devClientPkg = findDevClientPackage(resolved.projectRoot);
3688
+ const useDevClient = opts?.includeDevClient ?? (!release && !!devClientPkg);
3689
+ if (!appName) {
3690
+ throw new Error('"ios.appName" must be defined in tamer.config.json');
3691
+ }
3692
+ const projectDir = path14.join(resolved.iosDir, appName);
3693
+ const infoPlistPath = path14.join(projectDir, "Info.plist");
3694
+ if (!fs13.existsSync(projectDir)) {
3695
+ throw new Error(`iOS project not found at ${projectDir}. Run \`tamer ios create\` first.`);
3696
+ }
3697
+ const pbxprojPath = path14.join(resolved.iosDir, `${appName}.xcodeproj`, "project.pbxproj");
3698
+ const baseLprojDir = path14.join(projectDir, "Base.lproj");
3699
+ const launchScreenPath = path14.join(baseLprojDir, "LaunchScreen.storyboard");
3700
+ patchInfoPlist(infoPlistPath);
3701
+ writeFile(path14.join(projectDir, "AppDelegate.swift"), getAppDelegateSwift());
3702
+ writeFile(path14.join(projectDir, "SceneDelegate.swift"), getSceneDelegateSwift());
3703
+ if (!fs13.existsSync(launchScreenPath)) {
3704
+ fs13.mkdirSync(baseLprojDir, { recursive: true });
3705
+ writeFile(launchScreenPath, getLaunchScreenStoryboard());
3706
+ addLaunchScreenToXcodeProject(pbxprojPath, appName);
3707
+ }
3708
+ addSwiftSourceToXcodeProject(pbxprojPath, appName, "SceneDelegate.swift");
3709
+ if (useDevClient) {
3710
+ const devClientPkg2 = findDevClientPackage(resolved.projectRoot);
3711
+ const segment = resolved.lynxProjectDir.split("/").filter(Boolean).pop() ?? "";
3712
+ const tplVars = { PROJECT_BUNDLE_SEGMENT: segment };
3713
+ writeFile(path14.join(projectDir, "ViewController.swift"), getDevViewControllerSwift());
3714
+ writeFile(path14.join(projectDir, "LynxProvider.swift"), getSimpleLynxProviderSwift());
3715
+ addSwiftSourceToXcodeProject(pbxprojPath, appName, "LynxProvider.swift");
3716
+ const devTPContent = readTemplateOrFallback(devClientPkg2, "DevTemplateProvider.swift", "", tplVars);
3717
+ if (devTPContent) {
3718
+ writeFile(path14.join(projectDir, "DevTemplateProvider.swift"), devTPContent);
3719
+ addSwiftSourceToXcodeProject(pbxprojPath, appName, "DevTemplateProvider.swift");
3720
+ }
3721
+ const projectVCContent = readTemplateOrFallback(devClientPkg2, "ProjectViewController.swift", "", tplVars);
3722
+ if (projectVCContent) {
3723
+ writeFile(path14.join(projectDir, "ProjectViewController.swift"), projectVCContent);
3724
+ addSwiftSourceToXcodeProject(pbxprojPath, appName, "ProjectViewController.swift");
3725
+ }
3726
+ const devCMContent = readTemplateOrFallback(devClientPkg2, "DevClientManager.swift", "", tplVars);
3727
+ if (devCMContent) {
3728
+ writeFile(path14.join(projectDir, "DevClientManager.swift"), devCMContent);
3729
+ addSwiftSourceToXcodeProject(pbxprojPath, appName, "DevClientManager.swift");
3730
+ }
3731
+ const qrContent = readTemplateOrFallback(devClientPkg2, "QRScannerViewController.swift", "", tplVars);
3732
+ if (qrContent) {
3733
+ writeFile(path14.join(projectDir, "QRScannerViewController.swift"), qrContent);
3734
+ addSwiftSourceToXcodeProject(pbxprojPath, appName, "QRScannerViewController.swift");
3735
+ }
3736
+ console.log("\u2705 Synced iOS host app (embedded dev mode) \u2014 ViewController, DevTemplateProvider, ProjectViewController, DevClientManager, QRScannerViewController");
3737
+ } else {
3738
+ writeFile(path14.join(projectDir, "ViewController.swift"), getViewControllerSwift());
3739
+ writeFile(path14.join(projectDir, "LynxProvider.swift"), getSimpleLynxProviderSwift());
3740
+ addSwiftSourceToXcodeProject(pbxprojPath, appName, "LynxProvider.swift");
3741
+ console.log("\u2705 Synced iOS host app controller files");
3742
+ }
3743
+ }
3744
+ var syncHost_default = syncHostIos;
3643
3745
 
3644
- func scene(
3645
- _ scene: UIScene,
3646
- willConnectTo session: UISceneSession,
3647
- options connectionOptions: UIScene.ConnectionOptions
3648
- ) {
3649
- guard let windowScene = scene as? UIWindowScene else { return }
3650
- window = UIWindow(windowScene: windowScene)
3651
- window?.rootViewController = ViewController()
3652
- window?.makeKeyAndVisible()
3746
+ // src/ios/autolink.ts
3747
+ var autolink2 = () => {
3748
+ let resolved;
3749
+ try {
3750
+ resolved = resolveHostPaths();
3751
+ } catch (error) {
3752
+ console.error(`\u274C Error loading configuration: ${error.message}`);
3753
+ process.exit(1);
3754
+ }
3755
+ const projectRoot = resolved.projectRoot;
3756
+ const iosProjectPath = resolved.iosDir;
3757
+ function updateGeneratedSection(filePath, newContent, startMarker, endMarker) {
3758
+ if (!fs14.existsSync(filePath)) {
3759
+ console.warn(`\u26A0\uFE0F File not found, skipping update: ${filePath}`);
3760
+ return;
3653
3761
  }
3654
- }
3762
+ let fileContent = fs14.readFileSync(filePath, "utf8");
3763
+ const escapedStartMarker = startMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3764
+ const escapedEndMarker = endMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3765
+ const regex = new RegExp(`${escapedStartMarker}[\\s\\S]*?${escapedEndMarker}`, "g");
3766
+ const replacementBlock = `${startMarker}
3767
+ ${newContent}
3768
+ ${endMarker}`;
3769
+ if (regex.test(fileContent)) {
3770
+ const firstStartIdx = fileContent.indexOf(startMarker);
3771
+ fileContent = fileContent.replace(regex, "");
3772
+ if (firstStartIdx !== -1) {
3773
+ const before = fileContent.slice(0, firstStartIdx);
3774
+ const after = fileContent.slice(firstStartIdx);
3775
+ fileContent = `${before}${replacementBlock}${after}`;
3776
+ } else {
3777
+ fileContent += `
3778
+ ${replacementBlock}
3779
+ `;
3780
+ }
3781
+ } else {
3782
+ console.warn(`\u26A0\uFE0F Could not find autolink markers in ${path15.basename(filePath)}. Appending to the end of the file.`);
3783
+ fileContent += `
3784
+ ${replacementBlock}
3655
3785
  `;
3656
- }
3657
- function getViewControllerSwift() {
3658
- return `import UIKit
3659
- import Lynx
3660
- import tamerinsets
3661
-
3662
- class ViewController: UIViewController {
3663
- private var lynxView: LynxView?
3664
-
3665
- override func viewDidLoad() {
3666
- super.viewDidLoad()
3667
- view.backgroundColor = .black
3668
- edgesForExtendedLayout = .all
3669
- extendedLayoutIncludesOpaqueBars = true
3670
- additionalSafeAreaInsets = .zero
3671
- view.insetsLayoutMarginsFromSafeArea = false
3672
- view.preservesSuperviewLayoutMargins = false
3673
- viewRespectsSystemMinimumLayoutMargins = false
3674
3786
  }
3675
-
3676
- override func viewDidLayoutSubviews() {
3677
- super.viewDidLayoutSubviews()
3678
- guard view.bounds.width > 0, view.bounds.height > 0 else { return }
3679
- if lynxView == nil {
3680
- setupLynxView()
3681
- } else {
3682
- applyFullscreenLayout(to: lynxView!)
3787
+ fs14.writeFileSync(filePath, fileContent, "utf8");
3788
+ console.log(`\u2705 Updated autolinked section in ${path15.basename(filePath)}`);
3789
+ }
3790
+ function resolvePodDirectory(pkg) {
3791
+ const configuredDir = path15.join(pkg.packagePath, pkg.config.ios?.podspecPath || ".");
3792
+ if (fs14.existsSync(configuredDir)) {
3793
+ return configuredDir;
3794
+ }
3795
+ const iosDir = path15.join(pkg.packagePath, "ios");
3796
+ if (fs14.existsSync(iosDir)) {
3797
+ const stack = [iosDir];
3798
+ while (stack.length > 0) {
3799
+ const current = stack.pop();
3800
+ try {
3801
+ const entries = fs14.readdirSync(current, { withFileTypes: true });
3802
+ const podspec = entries.find((entry) => entry.isFile() && entry.name.endsWith(".podspec"));
3803
+ if (podspec) {
3804
+ return current;
3805
+ }
3806
+ for (const entry of entries) {
3807
+ if (entry.isDirectory()) {
3808
+ stack.push(path15.join(current, entry.name));
3809
+ }
3810
+ }
3811
+ } catch {
3683
3812
  }
3813
+ }
3684
3814
  }
3685
-
3686
- override func viewSafeAreaInsetsDidChange() {
3687
- super.viewSafeAreaInsetsDidChange()
3688
- TamerInsetsModule.reRequestInsets()
3815
+ return configuredDir;
3816
+ }
3817
+ function resolvePodName(pkg) {
3818
+ const fullPodspecDir = resolvePodDirectory(pkg);
3819
+ if (fs14.existsSync(fullPodspecDir)) {
3820
+ try {
3821
+ const files = fs14.readdirSync(fullPodspecDir);
3822
+ const podspecFile = files.find((f) => f.endsWith(".podspec"));
3823
+ if (podspecFile) return podspecFile.replace(".podspec", "");
3824
+ } catch {
3825
+ }
3689
3826
  }
3690
-
3691
- override var preferredStatusBarStyle: UIStatusBarStyle { .lightContent }
3692
-
3693
- private func buildLynxView() -> LynxView {
3694
- let bounds = view.bounds
3695
- let lv = LynxView { builder in
3696
- builder.config = LynxConfig(provider: LynxProvider())
3697
- builder.screenSize = bounds.size
3698
- builder.fontScale = 1.0
3699
- }
3700
- lv.autoresizingMask = [.flexibleWidth, .flexibleHeight]
3701
- lv.insetsLayoutMarginsFromSafeArea = false
3702
- lv.preservesSuperviewLayoutMargins = false
3703
- applyFullscreenLayout(to: lv)
3704
- return lv
3827
+ return pkg.name.split("/").pop().replace(/-/g, "");
3828
+ }
3829
+ function updatePodfile(packages) {
3830
+ const podfilePath = path15.join(iosProjectPath, "Podfile");
3831
+ let scriptContent = ` # This section is automatically generated by Tamer4Lynx.
3832
+ # Manual edits will be overwritten.`;
3833
+ const iosPackages = packages.filter((p) => p.config.ios);
3834
+ if (iosPackages.length > 0) {
3835
+ iosPackages.forEach((pkg) => {
3836
+ const relativePath = path15.relative(iosProjectPath, resolvePodDirectory(pkg));
3837
+ const podName = resolvePodName(pkg);
3838
+ scriptContent += `
3839
+ pod '${podName}', :path => '${relativePath}'`;
3840
+ });
3841
+ } else {
3842
+ scriptContent += `
3843
+ # No native modules found by Tamer4Lynx autolinker.`;
3705
3844
  }
3845
+ updateGeneratedSection(podfilePath, scriptContent.trim(), "# GENERATED AUTOLINK DEPENDENCIES START", "# GENERATED AUTOLINK DEPENDENCIES END");
3846
+ }
3847
+ function ensureXElementPod() {
3848
+ const podfilePath = path15.join(iosProjectPath, "Podfile");
3849
+ if (!fs14.existsSync(podfilePath)) return;
3850
+ let content = fs14.readFileSync(podfilePath, "utf8");
3851
+ if (content.includes("pod 'XElement'")) return;
3852
+ const lynxVersionMatch = content.match(/pod\s+'Lynx',\s*'([^']+)'/);
3853
+ const lynxVersion = lynxVersionMatch?.[1] ?? "3.6.0";
3854
+ const xelementBlock = ` pod 'XElement', '${lynxVersion}'
3706
3855
 
3707
- private func setupLynxView() {
3708
- let lv = buildLynxView()
3709
- view.addSubview(lv)
3710
- lv.loadTemplate(fromURL: "main.lynx.bundle", initData: nil)
3711
- self.lynxView = lv
3856
+ `;
3857
+ if (content.includes("# GENERATED AUTOLINK DEPENDENCIES START")) {
3858
+ content = content.replace(
3859
+ /(# GENERATED AUTOLINK DEPENDENCIES START)/,
3860
+ `${xelementBlock}$1`
3861
+ );
3862
+ } else {
3863
+ const insertAfter = /pod\s+'LynxService'[^\n]*(?:\n\s*'[^']*',?\s*)*/;
3864
+ const serviceMatch = content.match(insertAfter);
3865
+ if (serviceMatch) {
3866
+ const idx = serviceMatch.index + serviceMatch[0].length;
3867
+ content = content.slice(0, idx) + `
3868
+ pod 'XElement', '${lynxVersion}'` + content.slice(idx);
3869
+ } else {
3870
+ content += `
3871
+ pod 'XElement', '${lynxVersion}'
3872
+ `;
3873
+ }
3712
3874
  }
3713
-
3714
- private func applyFullscreenLayout(to lynxView: LynxView) {
3715
- let bounds = view.bounds
3716
- let size = bounds.size
3717
- lynxView.frame = bounds
3718
- lynxView.updateScreenMetrics(withWidth: size.width, height: size.height)
3719
- lynxView.updateViewport(withPreferredLayoutWidth: size.width, preferredLayoutHeight: size.height, needLayout: true)
3720
- lynxView.preferredLayoutWidth = size.width
3721
- lynxView.preferredLayoutHeight = size.height
3722
- lynxView.layoutWidthMode = .exact
3723
- lynxView.layoutHeightMode = .exact
3875
+ fs14.writeFileSync(podfilePath, content, "utf8");
3876
+ console.log(`\u2705 Added XElement pod (v${lynxVersion}) to Podfile`);
3877
+ }
3878
+ function ensureLynxPatchInPodfile() {
3879
+ const podfilePath = path15.join(iosProjectPath, "Podfile");
3880
+ if (!fs14.existsSync(podfilePath)) return;
3881
+ let content = fs14.readFileSync(podfilePath, "utf8");
3882
+ if (content.includes("content.gsub(/\\btypeof\\(/, '__typeof__(')")) return;
3883
+ const patch = `
3884
+ Dir.glob(File.join(installer.sandbox.root, 'Lynx/platform/darwin/**/*.{m,mm}')).each do |lynx_source|
3885
+ next unless File.file?(lynx_source)
3886
+ content = File.read(lynx_source)
3887
+ next unless content.match?(/\\btypeof\\(/)
3888
+ File.chmod(0644, lynx_source) rescue nil
3889
+ File.write(lynx_source, content.gsub(/\\btypeof\\(/, '__typeof__('))
3890
+ end`;
3891
+ content = content.replace(/(\n end\s*\n)(end\s*)$/, `$1${patch}
3892
+ $2`);
3893
+ fs14.writeFileSync(podfilePath, content, "utf8");
3894
+ console.log("\u2705 Added Lynx typeof patch to Podfile post_install.");
3895
+ }
3896
+ function ensurePodBuildSettings() {
3897
+ const podfilePath = path15.join(iosProjectPath, "Podfile");
3898
+ if (!fs14.existsSync(podfilePath)) return;
3899
+ let content = fs14.readFileSync(podfilePath, "utf8");
3900
+ let changed = false;
3901
+ if (!content.includes("CLANG_ENABLE_EXPLICIT_MODULES")) {
3902
+ content = content.replace(
3903
+ /config\.build_settings\['IPHONEOS_DEPLOYMENT_TARGET'\]\s*=\s*'[^']*'/,
3904
+ `$&
3905
+ config.build_settings['CLANG_ENABLE_EXPLICIT_MODULES'] = 'NO'
3906
+ config.build_settings['ONLY_ACTIVE_ARCH'] = 'YES'`
3907
+ );
3908
+ changed = true;
3724
3909
  }
3725
- }
3726
- `;
3727
- }
3728
- function getDevViewControllerSwift() {
3729
- return `import UIKit
3730
- import Lynx
3731
- import tamerdevclient
3732
- import tamerinsets
3733
-
3734
- class ViewController: UIViewController {
3735
- private var lynxView: LynxView?
3736
-
3737
- override func viewDidLoad() {
3738
- super.viewDidLoad()
3739
- view.backgroundColor = .black
3740
- edgesForExtendedLayout = .all
3741
- extendedLayoutIncludesOpaqueBars = true
3742
- additionalSafeAreaInsets = .zero
3743
- view.insetsLayoutMarginsFromSafeArea = false
3744
- view.preservesSuperviewLayoutMargins = false
3745
- viewRespectsSystemMinimumLayoutMargins = false
3746
- setupLynxView()
3747
- setupDevClientModule()
3910
+ if (!content.includes("gsub('-Werror'")) {
3911
+ const xcconfigStrip = `
3912
+ Dir.glob(File.join(installer.sandbox.root, 'Target Support Files', 'Lynx', '*.xcconfig')).each do |xcconfig_path|
3913
+ next unless File.file?(xcconfig_path)
3914
+ content = File.read(xcconfig_path)
3915
+ next unless content.include?('-Werror')
3916
+ File.write(xcconfig_path, content.gsub('-Werror', ''))
3917
+ end`;
3918
+ content = content.replace(
3919
+ /(Dir\.glob.*?Lynx\/platform\/darwin)/s,
3920
+ `${xcconfigStrip}
3921
+ $1`
3922
+ );
3923
+ changed = true;
3748
3924
  }
3749
-
3750
- override func viewDidLayoutSubviews() {
3751
- super.viewDidLayoutSubviews()
3752
- if let lynxView = lynxView {
3753
- applyFullscreenLayout(to: lynxView)
3754
- }
3925
+ if (!content.includes("target.name == 'PrimJS'") && content.includes("target.name == 'Lynx'")) {
3926
+ const primjsBlock = `
3927
+ if target.name == 'PrimJS'
3928
+ target.build_configurations.each do |config|
3929
+ config.build_settings['OTHER_CFLAGS'] = "$(inherited) -Wno-macro-redefined"
3930
+ config.build_settings['OTHER_CPLUSPLUSFLAGS'] = "$(inherited) -Wno-macro-redefined"
3931
+ end
3932
+ end`;
3933
+ content = content.replace(
3934
+ /( end)\n( end)\n( Dir\.glob\(File\.join\(installer\.sandbox\.root,\s*'Target Support Files',\s*'Lynx')/,
3935
+ `$1${primjsBlock}
3936
+ $2
3937
+ $3`
3938
+ );
3939
+ changed = true;
3755
3940
  }
3756
-
3757
- override func viewSafeAreaInsetsDidChange() {
3758
- super.viewSafeAreaInsetsDidChange()
3759
- TamerInsetsModule.reRequestInsets()
3941
+ if (changed) {
3942
+ fs14.writeFileSync(podfilePath, content, "utf8");
3943
+ console.log("\u2705 Added Xcode compatibility build settings to Podfile post_install.");
3760
3944
  }
3761
-
3762
- override var preferredStatusBarStyle: UIStatusBarStyle { .lightContent }
3763
-
3764
- private func setupLynxView() {
3765
- let size = fullscreenBounds().size
3766
- let lv = LynxView { builder in
3767
- builder.config = LynxConfig(provider: DevTemplateProvider())
3768
- builder.screenSize = size
3769
- builder.fontScale = 1.0
3945
+ }
3946
+ function updateLynxInitProcessor(packages) {
3947
+ const appNameFromConfig = resolved.config.ios?.appName;
3948
+ const candidatePaths = [];
3949
+ if (appNameFromConfig) {
3950
+ candidatePaths.push(path15.join(iosProjectPath, appNameFromConfig, "LynxInitProcessor.swift"));
3951
+ }
3952
+ candidatePaths.push(path15.join(iosProjectPath, "LynxInitProcessor.swift"));
3953
+ const found = candidatePaths.find((p) => fs14.existsSync(p));
3954
+ const lynxInitPath = found ?? candidatePaths[0];
3955
+ const iosPackages = packages.filter((p) => getIosModuleClassNames(p.config.ios).length > 0 || Object.keys(getIosElements(p.config.ios)).length > 0);
3956
+ const seenModules = /* @__PURE__ */ new Set();
3957
+ const seenElements = /* @__PURE__ */ new Set();
3958
+ const packagesWithContributions = /* @__PURE__ */ new Set();
3959
+ for (const pkg of iosPackages) {
3960
+ let hasUnique = false;
3961
+ for (const cls of getIosModuleClassNames(pkg.config.ios)) {
3962
+ if (!seenModules.has(cls)) {
3963
+ seenModules.add(cls);
3964
+ hasUnique = true;
3965
+ } else {
3966
+ console.warn(`\u26A0\uFE0F Skipping duplicate module "${cls}" from ${pkg.name} (already registered by another package)`);
3770
3967
  }
3771
- lv.autoresizingMask = [.flexibleWidth, .flexibleHeight]
3772
- lv.insetsLayoutMarginsFromSafeArea = false
3773
- lv.preservesSuperviewLayoutMargins = false
3774
- view.addSubview(lv)
3775
- applyFullscreenLayout(to: lv)
3776
- lv.loadTemplate(fromURL: "dev-client.lynx.bundle", initData: nil)
3777
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self, weak lv] in
3778
- guard let self, let lv else { return }
3779
- self.applyFullscreenLayout(to: lv)
3968
+ }
3969
+ for (const tag of Object.keys(getIosElements(pkg.config.ios))) {
3970
+ if (!seenElements.has(tag)) {
3971
+ seenElements.add(tag);
3972
+ hasUnique = true;
3973
+ } else {
3974
+ console.warn(`\u26A0\uFE0F Skipping duplicate element "${tag}" from ${pkg.name} (already registered by another package)`);
3780
3975
  }
3781
- self.lynxView = lv
3976
+ }
3977
+ if (hasUnique) packagesWithContributions.add(pkg);
3782
3978
  }
3979
+ const importPackages = iosPackages.filter((p) => packagesWithContributions.has(p));
3980
+ function updateImportsSection(filePath, pkgs) {
3981
+ const startMarker = "// GENERATED IMPORTS START";
3982
+ const endMarker = "// GENERATED IMPORTS END";
3983
+ if (pkgs.length === 0) {
3984
+ const placeholder = "// No native imports found by Tamer4Lynx autolinker.";
3985
+ updateGeneratedSection(filePath, placeholder, startMarker, endMarker);
3986
+ return;
3987
+ }
3988
+ const imports = pkgs.map((pkg) => {
3989
+ const podName = resolvePodName(pkg);
3990
+ return `import ${podName}`;
3991
+ }).join("\n");
3992
+ const fileContent = fs14.readFileSync(filePath, "utf8");
3993
+ if (fileContent.indexOf(startMarker) !== -1) {
3994
+ updateGeneratedSection(filePath, imports, startMarker, endMarker);
3995
+ return;
3996
+ }
3997
+ const importRegex = /^(import\s+[^\r\n]+)\r?\n/gm;
3998
+ let match = null;
3999
+ let lastMatchEnd = -1;
4000
+ while ((match = importRegex.exec(fileContent)) !== null) {
4001
+ lastMatchEnd = importRegex.lastIndex;
4002
+ }
4003
+ const block = `${startMarker}
4004
+ ${imports}
4005
+ ${endMarker}`;
4006
+ let newContent;
4007
+ if (lastMatchEnd !== -1) {
4008
+ const before = fileContent.slice(0, lastMatchEnd);
4009
+ const after = fileContent.slice(lastMatchEnd);
4010
+ newContent = `${before}
4011
+ ${block}
4012
+ ${after}`;
4013
+ } else {
4014
+ const foundationIdx = fileContent.indexOf("import Foundation");
4015
+ if (foundationIdx !== -1) {
4016
+ const lineEnd = fileContent.indexOf("\n", foundationIdx);
4017
+ const insertPos = lineEnd !== -1 ? lineEnd + 1 : foundationIdx + "import Foundation".length;
4018
+ const before = fileContent.slice(0, insertPos);
4019
+ const after = fileContent.slice(insertPos);
4020
+ newContent = `${before}
4021
+ ${block}
4022
+ ${after}`;
4023
+ } else {
4024
+ newContent = `${block}
3783
4025
 
3784
- private func applyFullscreenLayout(to lynxView: LynxView) {
3785
- let bounds = fullscreenBounds()
3786
- let size = bounds.size
3787
- lynxView.frame = bounds
3788
- lynxView.updateScreenMetrics(withWidth: size.width, height: size.height)
3789
- lynxView.updateViewport(withPreferredLayoutWidth: size.width, preferredLayoutHeight: size.height, needLayout: true)
3790
- lynxView.preferredLayoutWidth = size.width
3791
- lynxView.preferredLayoutHeight = size.height
3792
- lynxView.layoutWidthMode = .exact
3793
- lynxView.layoutHeightMode = .exact
4026
+ ${fileContent}`;
4027
+ }
4028
+ }
4029
+ fs14.writeFileSync(filePath, newContent, "utf8");
4030
+ console.log(`\u2705 Updated imports in ${path15.basename(filePath)}`);
3794
4031
  }
3795
-
3796
- private func fullscreenBounds() -> CGRect {
3797
- let bounds = view.bounds
3798
- if bounds.width > 0, bounds.height > 0 { return bounds }
3799
- return UIScreen.main.bounds
4032
+ updateImportsSection(lynxInitPath, importPackages);
4033
+ if (importPackages.length === 0) {
4034
+ const placeholder = " // No native modules found by Tamer4Lynx autolinker.";
4035
+ updateGeneratedSection(lynxInitPath, placeholder, "// GENERATED AUTOLINK START", "// GENERATED AUTOLINK END");
4036
+ } else {
4037
+ const seenModules2 = /* @__PURE__ */ new Set();
4038
+ const seenElements2 = /* @__PURE__ */ new Set();
4039
+ const blocks = importPackages.flatMap((pkg) => {
4040
+ const classNames = getIosModuleClassNames(pkg.config.ios);
4041
+ const moduleBlocks = classNames.filter((cls) => {
4042
+ if (seenModules2.has(cls)) return false;
4043
+ seenModules2.add(cls);
4044
+ return true;
4045
+ }).map((classNameRaw) => [
4046
+ ` // Register module from package: ${pkg.name}`,
4047
+ ` globalConfig.register(${classNameRaw}.self)`
4048
+ ].join("\n"));
4049
+ const elementBlocks = Object.entries(getIosElements(pkg.config.ios)).filter(([tagName]) => {
4050
+ if (seenElements2.has(tagName)) return false;
4051
+ seenElements2.add(tagName);
4052
+ return true;
4053
+ }).map(([tagName, classNameRaw]) => [
4054
+ ` // Register element from package: ${pkg.name}`,
4055
+ ` globalConfig.registerUI(${classNameRaw}.self, withName: "${tagName}")`
4056
+ ].join("\n"));
4057
+ return [...moduleBlocks, ...elementBlocks];
4058
+ });
4059
+ const content = blocks.join("\n\n");
4060
+ updateGeneratedSection(lynxInitPath, content, "// GENERATED AUTOLINK START", "// GENERATED AUTOLINK END");
4061
+ }
4062
+ const hasDevClient = packages.some((p) => p.name === "@tamer4lynx/tamer-dev-client");
4063
+ const androidNames = getDedupedAndroidModuleClassNames(packages);
4064
+ let devClientSupportedBody;
4065
+ if (hasDevClient && androidNames.length > 0) {
4066
+ devClientSupportedBody = ` DevClientModule.attachSupportedModuleClassNames([
4067
+ ${androidNames.map((n) => ` "${n.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`).join(",\n")}
4068
+ ])`;
4069
+ } else if (hasDevClient) {
4070
+ devClientSupportedBody = " DevClientModule.attachSupportedModuleClassNames([])";
4071
+ } else {
4072
+ devClientSupportedBody = " // @tamer4lynx/tamer-dev-client not linked";
3800
4073
  }
3801
-
3802
- private func setupDevClientModule() {
3803
- DevClientModule.presentQRScanner = { [weak self] completion in
3804
- let scanner = QRScannerViewController()
3805
- scanner.onResult = { url in
3806
- scanner.dismiss(animated: true) { completion(url) }
3807
- }
3808
- scanner.modalPresentationStyle = .fullScreen
3809
- self?.present(scanner, animated: true)
3810
- }
3811
-
3812
- DevClientModule.reloadProjectHandler = { [weak self] in
3813
- guard let self = self else { return }
3814
- let projectVC = ProjectViewController()
3815
- projectVC.modalPresentationStyle = .fullScreen
3816
- self.present(projectVC, animated: true)
3817
- }
4074
+ if (fs14.readFileSync(lynxInitPath, "utf8").includes("GENERATED DEV_CLIENT_SUPPORTED START")) {
4075
+ updateGeneratedSection(lynxInitPath, devClientSupportedBody, "// GENERATED DEV_CLIENT_SUPPORTED START", "// GENERATED DEV_CLIENT_SUPPORTED END");
3818
4076
  }
3819
- }
3820
- `;
3821
- }
3822
- function patchInfoPlist(infoPlistPath) {
3823
- if (!fs13.existsSync(infoPlistPath)) return;
3824
- let content = fs13.readFileSync(infoPlistPath, "utf8");
3825
- content = content.replace(/\s*<key>UIMainStoryboardFile<\/key>\s*<string>[^<]*<\/string>/g, "");
3826
- if (!content.includes("UILaunchStoryboardName")) {
3827
- content = content.replace("</dict>\n</plist>", ` <key>UILaunchStoryboardName</key>
3828
- <string>LaunchScreen</string>
3829
- </dict>
3830
- </plist>`);
3831
- console.log("\u2705 Added UILaunchStoryboardName to Info.plist");
3832
4077
  }
3833
- if (!content.includes("UIApplicationSceneManifest")) {
3834
- const sceneManifest = ` <key>UIApplicationSceneManifest</key>
3835
- <dict>
3836
- <key>UIApplicationSupportsMultipleScenes</key>
3837
- <false/>
3838
- <key>UISceneConfigurations</key>
3839
- <dict>
3840
- <key>UIWindowSceneSessionRoleApplication</key>
3841
- <array>
3842
- <dict>
3843
- <key>UISceneConfigurationName</key>
3844
- <string>Default Configuration</string>
3845
- <key>UISceneDelegateClassName</key>
3846
- <string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
3847
- </dict>
3848
- </array>
3849
- </dict>
3850
- </dict>`;
3851
- content = content.replace("</dict>\n</plist>", `${sceneManifest}
3852
- </dict>
3853
- </plist>`);
3854
- console.log("\u2705 Added UIApplicationSceneManifest to Info.plist");
4078
+ function findInfoPlist() {
4079
+ const appNameFromConfig = resolved.config.ios?.appName;
4080
+ const candidates = [];
4081
+ if (appNameFromConfig) {
4082
+ candidates.push(path15.join(iosProjectPath, appNameFromConfig, "Info.plist"));
4083
+ }
4084
+ candidates.push(path15.join(iosProjectPath, "Info.plist"));
4085
+ return candidates.find((p) => fs14.existsSync(p)) ?? null;
3855
4086
  }
3856
- fs13.writeFileSync(infoPlistPath, content, "utf8");
3857
- }
3858
- function getSimpleLynxProviderSwift() {
3859
- return `import Foundation
3860
- import Lynx
3861
-
3862
- class LynxProvider: NSObject, LynxTemplateProvider {
3863
- func loadTemplate(withUrl url: String!, onComplete callback: LynxTemplateLoadBlock!) {
3864
- DispatchQueue.global(qos: .background).async {
3865
- guard let url = url,
3866
- let bundleUrl = Bundle.main.url(forResource: url, withExtension: nil),
3867
- let data = try? Data(contentsOf: bundleUrl) else {
3868
- let err = NSError(domain: "LynxProvider", code: 404,
3869
- userInfo: [NSLocalizedDescriptionKey: "Bundle not found: \\(url ?? "nil")"])
3870
- callback?(nil, err)
3871
- return
3872
- }
3873
- callback?(data, nil)
4087
+ function readPlistXml(plistPath) {
4088
+ return fs14.readFileSync(plistPath, "utf8");
4089
+ }
4090
+ function syncInfoPlistPermissions(packages) {
4091
+ const plistPath = findInfoPlist();
4092
+ if (!plistPath) return;
4093
+ const allPermissions = {};
4094
+ for (const pkg of packages) {
4095
+ const iosPerms = pkg.config.ios?.iosPermissions;
4096
+ if (iosPerms) {
4097
+ for (const [key, desc] of Object.entries(iosPerms)) {
4098
+ if (!allPermissions[key]) {
4099
+ allPermissions[key] = desc;
4100
+ }
3874
4101
  }
4102
+ }
3875
4103
  }
3876
- }
4104
+ if (Object.keys(allPermissions).length === 0) return;
4105
+ let plist = readPlistXml(plistPath);
4106
+ let added = 0;
4107
+ for (const [key, desc] of Object.entries(allPermissions)) {
4108
+ if (plist.includes(`<key>${key}</key>`)) continue;
4109
+ const insertion = ` <key>${key}</key>
4110
+ <string>${desc}</string>
3877
4111
  `;
3878
- }
3879
- function readTemplateOrFallback(devClientPkg, templateName, fallback, vars = {}) {
3880
- if (devClientPkg) {
3881
- const tplPath = path14.join(devClientPkg, "ios", "templates", templateName);
3882
- if (fs13.existsSync(tplPath)) {
3883
- let content = fs13.readFileSync(tplPath, "utf8");
3884
- for (const [k, v] of Object.entries(vars)) {
3885
- content = content.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), v);
3886
- }
3887
- return content;
4112
+ plist = plist.replace(
4113
+ /(<\/dict>\s*<\/plist>)/,
4114
+ `${insertion}$1`
4115
+ );
4116
+ added++;
4117
+ }
4118
+ if (added > 0) {
4119
+ fs14.writeFileSync(plistPath, plist, "utf8");
4120
+ console.log(`\u2705 Synced ${added} Info.plist permission description(s)`);
3888
4121
  }
3889
4122
  }
3890
- return fallback;
3891
- }
3892
- function syncHostIos(opts) {
3893
- const resolved = resolveHostPaths();
3894
- const appName = resolved.config.ios?.appName;
3895
- const release = opts?.release === true;
3896
- const devClientPkg = findDevClientPackage(resolved.projectRoot);
3897
- const useDevClient = opts?.includeDevClient ?? (!release && !!devClientPkg);
3898
- if (!appName) {
3899
- throw new Error('"ios.appName" must be defined in tamer.config.json');
3900
- }
3901
- const projectDir = path14.join(resolved.iosDir, appName);
3902
- const infoPlistPath = path14.join(projectDir, "Info.plist");
3903
- if (!fs13.existsSync(projectDir)) {
3904
- throw new Error(`iOS project not found at ${projectDir}. Run \`tamer ios create\` first.`);
3905
- }
3906
- const pbxprojPath = path14.join(resolved.iosDir, `${appName}.xcodeproj`, "project.pbxproj");
3907
- const baseLprojDir = path14.join(projectDir, "Base.lproj");
3908
- const launchScreenPath = path14.join(baseLprojDir, "LaunchScreen.storyboard");
3909
- patchInfoPlist(infoPlistPath);
3910
- writeFile(path14.join(projectDir, "AppDelegate.swift"), getAppDelegateSwift());
3911
- writeFile(path14.join(projectDir, "SceneDelegate.swift"), getSceneDelegateSwift());
3912
- if (!fs13.existsSync(launchScreenPath)) {
3913
- fs13.mkdirSync(baseLprojDir, { recursive: true });
3914
- writeFile(launchScreenPath, getLaunchScreenStoryboard());
3915
- addLaunchScreenToXcodeProject(pbxprojPath, appName);
4123
+ function syncInfoPlistUrlSchemes() {
4124
+ const urlSchemes = resolved.config.ios?.urlSchemes;
4125
+ if (!urlSchemes || urlSchemes.length === 0) return;
4126
+ const plistPath = findInfoPlist();
4127
+ if (!plistPath) return;
4128
+ let plist = readPlistXml(plistPath);
4129
+ const schemesXml = urlSchemes.map((s) => {
4130
+ const role = s.role ?? "Editor";
4131
+ return ` <dict>
4132
+ <key>CFBundleTypeRole</key>
4133
+ <string>${role}</string>
4134
+ <key>CFBundleURLSchemes</key>
4135
+ <array>
4136
+ <string>${s.scheme}</string>
4137
+ </array>
4138
+ </dict>`;
4139
+ }).join("\n");
4140
+ const generatedBlock = ` <key>CFBundleURLTypes</key>
4141
+ <array>
4142
+ <!-- GENERATED URL SCHEMES START -->
4143
+ ${schemesXml}
4144
+ <!-- GENERATED URL SCHEMES END -->
4145
+ </array>`;
4146
+ const startMarker = "<!-- GENERATED URL SCHEMES START -->";
4147
+ const endMarker = "<!-- GENERATED URL SCHEMES END -->";
4148
+ if (plist.includes(startMarker) && plist.includes(endMarker)) {
4149
+ const regex = new RegExp(
4150
+ `${startMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${endMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`,
4151
+ "g"
4152
+ );
4153
+ plist = plist.replace(regex, `${startMarker}
4154
+ ${schemesXml}
4155
+ ${endMarker}`);
4156
+ } else if (plist.includes("<key>CFBundleURLTypes</key>")) {
4157
+ console.log("\u2139\uFE0F CFBundleURLTypes exists but has no generated markers. Skipping URL scheme sync.");
4158
+ return;
4159
+ } else {
4160
+ plist = plist.replace(
4161
+ /(<\/dict>\s*<\/plist>)/,
4162
+ `${generatedBlock}
4163
+ $1`
4164
+ );
4165
+ }
4166
+ fs14.writeFileSync(plistPath, plist, "utf8");
4167
+ console.log(`\u2705 Synced ${urlSchemes.length} iOS URL scheme(s) into Info.plist`);
3916
4168
  }
3917
- addSwiftSourceToXcodeProject(pbxprojPath, appName, "SceneDelegate.swift");
3918
- if (useDevClient) {
3919
- const devClientPkg2 = findDevClientPackage(resolved.projectRoot);
3920
- const segment = resolved.lynxProjectDir.split("/").filter(Boolean).pop() ?? "";
3921
- const tplVars = { PROJECT_BUNDLE_SEGMENT: segment };
3922
- writeFile(path14.join(projectDir, "ViewController.swift"), getDevViewControllerSwift());
3923
- writeFile(path14.join(projectDir, "LynxProvider.swift"), getSimpleLynxProviderSwift());
3924
- addSwiftSourceToXcodeProject(pbxprojPath, appName, "LynxProvider.swift");
3925
- const devTPContent = readTemplateOrFallback(devClientPkg2, "DevTemplateProvider.swift", "", tplVars);
3926
- if (devTPContent) {
3927
- writeFile(path14.join(projectDir, "DevTemplateProvider.swift"), devTPContent);
3928
- addSwiftSourceToXcodeProject(pbxprojPath, appName, "DevTemplateProvider.swift");
4169
+ function runPodInstall(forcePath) {
4170
+ const podfilePath = forcePath ?? path15.join(iosProjectPath, "Podfile");
4171
+ if (!fs14.existsSync(podfilePath)) {
4172
+ console.log("\u2139\uFE0F No Podfile found in ios directory; skipping `pod install`.");
4173
+ return;
3929
4174
  }
3930
- const projectVCContent = readTemplateOrFallback(devClientPkg2, "ProjectViewController.swift", "", tplVars);
3931
- if (projectVCContent) {
3932
- writeFile(path14.join(projectDir, "ProjectViewController.swift"), projectVCContent);
3933
- addSwiftSourceToXcodeProject(pbxprojPath, appName, "ProjectViewController.swift");
4175
+ const cwd = path15.dirname(podfilePath);
4176
+ try {
4177
+ console.log(`\u2139\uFE0F Running \`pod install\` in ${cwd}...`);
4178
+ try {
4179
+ execSync6("pod install", { cwd, stdio: "inherit" });
4180
+ } catch {
4181
+ console.log("\u2139\uFE0F Retrying `pod install` with repo update...");
4182
+ execSync6("pod install --repo-update", { cwd, stdio: "inherit" });
4183
+ }
4184
+ console.log("\u2705 `pod install` completed successfully.");
4185
+ } catch (e) {
4186
+ console.warn(`\u26A0\uFE0F 'pod install' failed: ${e.message}`);
4187
+ console.log("\u26A0\uFE0F You can run `pod install` manually in the ios directory.");
3934
4188
  }
3935
- const devCMContent = readTemplateOrFallback(devClientPkg2, "DevClientManager.swift", "", tplVars);
3936
- if (devCMContent) {
3937
- writeFile(path14.join(projectDir, "DevClientManager.swift"), devCMContent);
3938
- addSwiftSourceToXcodeProject(pbxprojPath, appName, "DevClientManager.swift");
4189
+ }
4190
+ function run() {
4191
+ console.log("\u{1F50E} Finding Lynx extension packages (lynx.ext.json / tamer.json)...");
4192
+ const packages = discoverModules(projectRoot).filter((p) => p.config.ios);
4193
+ if (packages.length > 0) {
4194
+ console.log(`Found ${packages.length} package(s): ${packages.map((p) => p.name).join(", ")}`);
4195
+ } else {
4196
+ console.log("\u2139\uFE0F No Tamer4Lynx native packages found.");
3939
4197
  }
3940
- const qrContent = readTemplateOrFallback(devClientPkg2, "QRScannerViewController.swift", "", tplVars);
3941
- if (qrContent) {
3942
- writeFile(path14.join(projectDir, "QRScannerViewController.swift"), qrContent);
3943
- addSwiftSourceToXcodeProject(pbxprojPath, appName, "QRScannerViewController.swift");
4198
+ syncHost_default();
4199
+ updatePodfile(packages);
4200
+ ensureXElementPod();
4201
+ ensureLynxPatchInPodfile();
4202
+ ensurePodBuildSettings();
4203
+ updateLynxInitProcessor(packages);
4204
+ writeHostNativeModulesManifest();
4205
+ syncInfoPlistPermissions(packages);
4206
+ syncInfoPlistUrlSchemes();
4207
+ const appNameFromConfig = resolved.config.ios?.appName;
4208
+ if (appNameFromConfig) {
4209
+ const appPodfile = path15.join(iosProjectPath, appNameFromConfig, "Podfile");
4210
+ if (fs14.existsSync(appPodfile)) {
4211
+ runPodInstall(appPodfile);
4212
+ console.log("\u2728 Autolinking complete for iOS.");
4213
+ return;
4214
+ }
3944
4215
  }
3945
- console.log("\u2705 Synced iOS host app (embedded dev mode) \u2014 ViewController, DevTemplateProvider, ProjectViewController, DevClientManager, QRScannerViewController");
3946
- } else {
3947
- writeFile(path14.join(projectDir, "ViewController.swift"), getViewControllerSwift());
3948
- writeFile(path14.join(projectDir, "LynxProvider.swift"), getSimpleLynxProviderSwift());
3949
- addSwiftSourceToXcodeProject(pbxprojPath, appName, "LynxProvider.swift");
3950
- console.log("\u2705 Synced iOS host app controller files");
4216
+ runPodInstall();
4217
+ console.log("\u2728 Autolinking complete for iOS.");
3951
4218
  }
3952
- }
3953
- var syncHost_default = syncHostIos;
4219
+ function writeHostNativeModulesManifest() {
4220
+ const allPkgs = discoverModules(projectRoot);
4221
+ const hasDevClient = allPkgs.some((p) => p.name === "@tamer4lynx/tamer-dev-client");
4222
+ const appFolder = resolved.config.ios?.appName;
4223
+ if (!hasDevClient || !appFolder) return;
4224
+ const androidNames = getDedupedAndroidModuleClassNames(allPkgs);
4225
+ const appDir = path15.join(iosProjectPath, appFolder);
4226
+ fs14.mkdirSync(appDir, { recursive: true });
4227
+ const manifestPath = path15.join(appDir, TAMER_HOST_NATIVE_MODULES_FILENAME);
4228
+ fs14.writeFileSync(manifestPath, buildHostNativeModulesManifestJson(androidNames), "utf8");
4229
+ console.log(`\u2705 Wrote ${TAMER_HOST_NATIVE_MODULES_FILENAME} (native module ids for dev-client checks)`);
4230
+ const pbxprojPath = path15.join(iosProjectPath, `${appFolder}.xcodeproj`, "project.pbxproj");
4231
+ if (fs14.existsSync(pbxprojPath)) {
4232
+ addResourceToXcodeProject(pbxprojPath, appFolder, TAMER_HOST_NATIVE_MODULES_FILENAME);
4233
+ }
4234
+ }
4235
+ run();
4236
+ };
4237
+ var autolink_default2 = autolink2;
3954
4238
 
3955
4239
  // src/ios/bundle.ts
4240
+ import fs15 from "fs";
4241
+ import path16 from "path";
4242
+ import { execSync as execSync7 } from "child_process";
3956
4243
  function bundleAndDeploy2(opts = {}) {
3957
4244
  const release = opts.release === true;
3958
4245
  let resolved;
@@ -3969,53 +4256,60 @@ function bundleAndDeploy2(opts = {}) {
3969
4256
  const includeDevClient = !release && !!devClientPkg;
3970
4257
  const appName = resolved.config.ios.appName;
3971
4258
  const sourceBundlePath = resolved.lynxBundlePath;
3972
- const destinationDir = path15.join(resolved.iosDir, appName);
3973
- const destinationBundlePath = path15.join(destinationDir, resolved.lynxBundleFile);
4259
+ const destinationDir = path16.join(resolved.iosDir, appName);
4260
+ const destinationBundlePath = path16.join(destinationDir, resolved.lynxBundleFile);
3974
4261
  syncHost_default({ release, includeDevClient });
3975
4262
  autolink_default2();
4263
+ const iconPaths = resolveIconPaths(resolved.projectRoot, resolved.config);
4264
+ if (iconPaths) {
4265
+ const appIconDir = path16.join(destinationDir, "Assets.xcassets", "AppIcon.appiconset");
4266
+ if (applyIosAppIconAssets(appIconDir, iconPaths)) {
4267
+ console.log("\u2705 Synced iOS AppIcon from tamer.config.json");
4268
+ }
4269
+ }
3976
4270
  try {
3977
4271
  console.log("\u{1F4E6} Building Lynx bundle...");
3978
- execSync6("npm run build", { stdio: "inherit", cwd: resolved.lynxProjectDir });
4272
+ execSync7("npm run build", { stdio: "inherit", cwd: resolved.lynxProjectDir });
3979
4273
  console.log("\u2705 Build completed successfully.");
3980
4274
  } catch (error) {
3981
4275
  console.error("\u274C Build process failed. Please check the errors above.");
3982
4276
  process.exit(1);
3983
4277
  }
3984
4278
  try {
3985
- if (!fs14.existsSync(sourceBundlePath)) {
4279
+ if (!fs15.existsSync(sourceBundlePath)) {
3986
4280
  console.error(`\u274C Build output not found at: ${sourceBundlePath}`);
3987
4281
  process.exit(1);
3988
4282
  }
3989
- if (!fs14.existsSync(destinationDir)) {
4283
+ if (!fs15.existsSync(destinationDir)) {
3990
4284
  console.error(`Destination directory not found at: ${destinationDir}`);
3991
4285
  process.exit(1);
3992
4286
  }
3993
- const distDir = path15.dirname(sourceBundlePath);
4287
+ const distDir = path16.dirname(sourceBundlePath);
3994
4288
  console.log(`\u{1F69A} Copying bundle and assets to iOS project...`);
3995
4289
  copyDistAssets(distDir, destinationDir, resolved.lynxBundleFile);
3996
4290
  console.log(`\u2728 Successfully copied bundle to: ${destinationBundlePath}`);
3997
- const pbxprojPath = path15.join(resolved.iosDir, `${appName}.xcodeproj`, "project.pbxproj");
3998
- if (fs14.existsSync(pbxprojPath)) {
4291
+ const pbxprojPath = path16.join(resolved.iosDir, `${appName}.xcodeproj`, "project.pbxproj");
4292
+ if (fs15.existsSync(pbxprojPath)) {
3999
4293
  const skip = /* @__PURE__ */ new Set([".rspeedy", "stats.json"]);
4000
- for (const entry of fs14.readdirSync(distDir)) {
4001
- if (skip.has(entry) || fs14.statSync(path15.join(distDir, entry)).isDirectory()) continue;
4294
+ for (const entry of fs15.readdirSync(distDir)) {
4295
+ if (skip.has(entry) || fs15.statSync(path16.join(distDir, entry)).isDirectory()) continue;
4002
4296
  addResourceToXcodeProject(pbxprojPath, appName, entry);
4003
4297
  }
4004
4298
  }
4005
4299
  if (includeDevClient && devClientPkg) {
4006
- const devClientBundle = path15.join(destinationDir, "dev-client.lynx.bundle");
4300
+ const devClientBundle = path16.join(destinationDir, "dev-client.lynx.bundle");
4007
4301
  console.log("\u{1F4E6} Building dev-client bundle...");
4008
4302
  try {
4009
- execSync6("npm run build", { stdio: "inherit", cwd: devClientPkg });
4303
+ execSync7("npm run build", { stdio: "inherit", cwd: devClientPkg });
4010
4304
  } catch {
4011
4305
  console.warn("\u26A0\uFE0F dev-client build failed; skipping dev-client bundle");
4012
4306
  }
4013
- const builtBundle = path15.join(devClientPkg, "dist", "dev-client.lynx.bundle");
4014
- if (fs14.existsSync(builtBundle)) {
4015
- fs14.copyFileSync(builtBundle, devClientBundle);
4307
+ const builtBundle = path16.join(devClientPkg, "dist", "dev-client.lynx.bundle");
4308
+ if (fs15.existsSync(builtBundle)) {
4309
+ fs15.copyFileSync(builtBundle, devClientBundle);
4016
4310
  console.log("\u2728 Copied dev-client.lynx.bundle to iOS project");
4017
- const pbxprojPath2 = path15.join(resolved.iosDir, `${appName}.xcodeproj`, "project.pbxproj");
4018
- if (fs14.existsSync(pbxprojPath2)) {
4311
+ const pbxprojPath2 = path16.join(resolved.iosDir, `${appName}.xcodeproj`, "project.pbxproj");
4312
+ if (fs15.existsSync(pbxprojPath2)) {
4019
4313
  addResourceToXcodeProject(pbxprojPath2, appName, "dev-client.lynx.bundle");
4020
4314
  }
4021
4315
  }
@@ -4029,16 +4323,16 @@ function bundleAndDeploy2(opts = {}) {
4029
4323
  var bundle_default2 = bundleAndDeploy2;
4030
4324
 
4031
4325
  // src/ios/build.ts
4032
- import fs15 from "fs";
4033
- import path16 from "path";
4326
+ import fs16 from "fs";
4327
+ import path17 from "path";
4034
4328
  import os3 from "os";
4035
- import { execSync as execSync7 } from "child_process";
4329
+ import { execSync as execSync8 } from "child_process";
4036
4330
  function hostArch() {
4037
4331
  return os3.arch() === "arm64" ? "arm64" : "x86_64";
4038
4332
  }
4039
4333
  function findBootedSimulator() {
4040
4334
  try {
4041
- const out = execSync7("xcrun simctl list devices --json", { encoding: "utf8" });
4335
+ const out = execSync8("xcrun simctl list devices --json", { encoding: "utf8" });
4042
4336
  const json = JSON.parse(out);
4043
4337
  for (const runtimes of Object.values(json.devices)) {
4044
4338
  for (const device of runtimes) {
@@ -4060,11 +4354,11 @@ async function buildIpa(opts = {}) {
4060
4354
  const configuration = opts.release ? "Release" : "Debug";
4061
4355
  bundle_default2({ release: opts.release });
4062
4356
  const scheme = appName;
4063
- const workspacePath = path16.join(iosDir, `${appName}.xcworkspace`);
4064
- const projectPath = path16.join(iosDir, `${appName}.xcodeproj`);
4065
- const xcproject = fs15.existsSync(workspacePath) ? workspacePath : projectPath;
4357
+ const workspacePath = path17.join(iosDir, `${appName}.xcworkspace`);
4358
+ const projectPath = path17.join(iosDir, `${appName}.xcodeproj`);
4359
+ const xcproject = fs16.existsSync(workspacePath) ? workspacePath : projectPath;
4066
4360
  const flag = xcproject.endsWith(".xcworkspace") ? "-workspace" : "-project";
4067
- const derivedDataPath = path16.join(iosDir, "build");
4361
+ const derivedDataPath = path17.join(iosDir, "build");
4068
4362
  const sdk = opts.install ? "iphonesimulator" : "iphoneos";
4069
4363
  const signingArgs = opts.install ? "" : " CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO";
4070
4364
  const archFlag = opts.install ? `-arch ${hostArch()} ` : "";
@@ -4074,20 +4368,20 @@ async function buildIpa(opts = {}) {
4074
4368
  ].join(" ");
4075
4369
  console.log(`
4076
4370
  \u{1F528} Building ${configuration} (${sdk})...`);
4077
- execSync7(
4371
+ execSync8(
4078
4372
  `xcodebuild ${flag} "${xcproject}" -scheme "${scheme}" -configuration ${configuration} -sdk ${sdk} ${archFlag}-derivedDataPath "${derivedDataPath}" ${extraSettings}${signingArgs}`,
4079
4373
  { stdio: "inherit", cwd: iosDir }
4080
4374
  );
4081
4375
  console.log(`\u2705 Build completed.`);
4082
4376
  if (opts.install) {
4083
- const appGlob = path16.join(
4377
+ const appGlob = path17.join(
4084
4378
  derivedDataPath,
4085
4379
  "Build",
4086
4380
  "Products",
4087
4381
  `${configuration}-iphonesimulator`,
4088
4382
  `${appName}.app`
4089
4383
  );
4090
- if (!fs15.existsSync(appGlob)) {
4384
+ if (!fs16.existsSync(appGlob)) {
4091
4385
  console.error(`\u274C Built app not found at: ${appGlob}`);
4092
4386
  process.exit(1);
4093
4387
  }
@@ -4097,10 +4391,10 @@ async function buildIpa(opts = {}) {
4097
4391
  process.exit(1);
4098
4392
  }
4099
4393
  console.log(`\u{1F4F2} Installing on simulator ${udid}...`);
4100
- execSync7(`xcrun simctl install "${udid}" "${appGlob}"`, { stdio: "inherit" });
4394
+ execSync8(`xcrun simctl install "${udid}" "${appGlob}"`, { stdio: "inherit" });
4101
4395
  if (bundleId) {
4102
4396
  console.log(`\u{1F680} Launching ${bundleId}...`);
4103
- execSync7(`xcrun simctl launch "${udid}" "${bundleId}"`, { stdio: "inherit" });
4397
+ execSync8(`xcrun simctl launch "${udid}" "${bundleId}"`, { stdio: "inherit" });
4104
4398
  console.log("\u2705 App launched.");
4105
4399
  } else {
4106
4400
  console.log('\u2705 App installed. (Set "ios.bundleId" in tamer.config.json to auto-launch.)');
@@ -4110,8 +4404,8 @@ async function buildIpa(opts = {}) {
4110
4404
  var build_default2 = buildIpa;
4111
4405
 
4112
4406
  // src/common/init.ts
4113
- import fs16 from "fs";
4114
- import path17 from "path";
4407
+ import fs17 from "fs";
4408
+ import path18 from "path";
4115
4409
  import readline from "readline";
4116
4410
  var rl = readline.createInterface({
4117
4411
  input: process.stdin,
@@ -4162,31 +4456,82 @@ async function init() {
4162
4456
  paths: { androidDir: "android", iosDir: "ios" }
4163
4457
  };
4164
4458
  if (lynxProject) config.lynxProject = lynxProject;
4165
- const configPath = path17.join(process.cwd(), "tamer.config.json");
4166
- fs16.writeFileSync(configPath, JSON.stringify(config, null, 2));
4459
+ const configPath = path18.join(process.cwd(), "tamer.config.json");
4460
+ fs17.writeFileSync(configPath, JSON.stringify(config, null, 2));
4167
4461
  console.log(`
4168
4462
  \u2705 Generated tamer.config.json at ${configPath}`);
4463
+ const tamerTypesInclude = "node_modules/@tamer4lynx/tamer-*/src/**/*.d.ts";
4464
+ const tsconfigCandidates = lynxProject ? [path18.join(process.cwd(), lynxProject, "tsconfig.json"), path18.join(process.cwd(), "tsconfig.json")] : [path18.join(process.cwd(), "tsconfig.json")];
4465
+ for (const tsconfigPath of tsconfigCandidates) {
4466
+ if (!fs17.existsSync(tsconfigPath)) continue;
4467
+ try {
4468
+ const raw = fs17.readFileSync(tsconfigPath, "utf-8");
4469
+ const tsconfig = JSON.parse(raw);
4470
+ const include = tsconfig.include ?? [];
4471
+ const arr = Array.isArray(include) ? include : [include];
4472
+ if (arr.some((p) => (typeof p === "string" ? p : "").includes("tamer-"))) continue;
4473
+ arr.push(tamerTypesInclude);
4474
+ tsconfig.include = arr;
4475
+ fs17.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
4476
+ console.log(`\u2705 Updated ${path18.relative(process.cwd(), tsconfigPath)} to include tamer type declarations`);
4477
+ break;
4478
+ } catch (e) {
4479
+ console.warn(`\u26A0 Could not update ${tsconfigPath}:`, e.message);
4480
+ }
4481
+ }
4169
4482
  rl.close();
4170
4483
  }
4171
4484
  var init_default = init;
4172
4485
 
4173
4486
  // src/common/create.ts
4174
- import fs17 from "fs";
4175
- import path18 from "path";
4487
+ import fs18 from "fs";
4488
+ import path19 from "path";
4176
4489
  import readline2 from "readline";
4177
4490
  var rl2 = readline2.createInterface({ input: process.stdin, output: process.stdout, terminal: false });
4178
4491
  function ask2(question) {
4179
4492
  return new Promise((resolve) => rl2.question(question, (answer) => resolve(answer.trim())));
4180
4493
  }
4181
- async function create3() {
4494
+ async function create3(opts) {
4182
4495
  console.log("Tamer4Lynx: Create Lynx Extension\n");
4183
- console.log("Select extension types (space to toggle, enter to confirm):");
4184
- console.log(" [ ] Native Module");
4185
- console.log(" [ ] Element");
4186
- console.log(" [ ] Service\n");
4187
- const includeModule = /^y(es)?$/i.test(await ask2("Include Native Module? (Y/n): ") || "y");
4188
- const includeElement = /^y(es)?$/i.test(await ask2("Include Element? (y/N): ") || "n");
4189
- const includeService = /^y(es)?$/i.test(await ask2("Include Service? (y/N): ") || "n");
4496
+ let includeModule;
4497
+ let includeElement;
4498
+ let includeService;
4499
+ if (opts?.type) {
4500
+ switch (opts.type) {
4501
+ case "module":
4502
+ includeModule = true;
4503
+ includeElement = false;
4504
+ includeService = false;
4505
+ break;
4506
+ case "element":
4507
+ includeModule = false;
4508
+ includeElement = true;
4509
+ includeService = false;
4510
+ break;
4511
+ case "service":
4512
+ includeModule = false;
4513
+ includeElement = false;
4514
+ includeService = true;
4515
+ break;
4516
+ case "combo":
4517
+ includeModule = true;
4518
+ includeElement = true;
4519
+ includeService = true;
4520
+ break;
4521
+ default:
4522
+ includeModule = true;
4523
+ includeElement = false;
4524
+ includeService = false;
4525
+ }
4526
+ } else {
4527
+ console.log("Select extension types (space to toggle, enter to confirm):");
4528
+ console.log(" [ ] Native Module");
4529
+ console.log(" [ ] Element");
4530
+ console.log(" [ ] Service\n");
4531
+ includeModule = /^y(es)?$/i.test(await ask2("Include Native Module? (Y/n): ") || "y");
4532
+ includeElement = /^y(es)?$/i.test(await ask2("Include Element? (y/N): ") || "n");
4533
+ includeService = /^y(es)?$/i.test(await ask2("Include Service? (y/N): ") || "n");
4534
+ }
4190
4535
  if (!includeModule && !includeElement && !includeService) {
4191
4536
  console.error("\u274C At least one extension type is required.");
4192
4537
  rl2.close();
@@ -4202,13 +4547,13 @@ async function create3() {
4202
4547
  const simpleModuleName = extName.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("") + "Module";
4203
4548
  const fullModuleClassName = `${packageName}.${simpleModuleName}`;
4204
4549
  const cwd = process.cwd();
4205
- const root = path18.join(cwd, extName);
4206
- if (fs17.existsSync(root)) {
4550
+ const root = path19.join(cwd, extName);
4551
+ if (fs18.existsSync(root)) {
4207
4552
  console.error(`\u274C Directory ${extName} already exists.`);
4208
4553
  rl2.close();
4209
4554
  process.exit(1);
4210
4555
  }
4211
- fs17.mkdirSync(root, { recursive: true });
4556
+ fs18.mkdirSync(root, { recursive: true });
4212
4557
  const lynxExt = {
4213
4558
  platforms: {
4214
4559
  android: {
@@ -4223,7 +4568,7 @@ async function create3() {
4223
4568
  web: {}
4224
4569
  }
4225
4570
  };
4226
- fs17.writeFileSync(path18.join(root, "lynx.ext.json"), JSON.stringify(lynxExt, null, 2));
4571
+ fs18.writeFileSync(path19.join(root, "lynx.ext.json"), JSON.stringify(lynxExt, null, 2));
4227
4572
  const pkg = {
4228
4573
  name: extName,
4229
4574
  version: "0.0.1",
@@ -4236,17 +4581,20 @@ async function create3() {
4236
4581
  engines: { node: ">=18" }
4237
4582
  };
4238
4583
  if (includeModule) pkg.types = "src/index.d.ts";
4239
- fs17.writeFileSync(path18.join(root, "package.json"), JSON.stringify(pkg, null, 2));
4584
+ fs18.writeFileSync(path19.join(root, "package.json"), JSON.stringify(pkg, null, 2));
4240
4585
  const pkgPath = packageName.replace(/\./g, "/");
4586
+ const hasSrc = includeModule || includeElement || includeService;
4587
+ if (hasSrc) {
4588
+ fs18.mkdirSync(path19.join(root, "src"), { recursive: true });
4589
+ }
4241
4590
  if (includeModule) {
4242
- fs17.mkdirSync(path18.join(root, "src"), { recursive: true });
4243
- fs17.writeFileSync(path18.join(root, "src", "index.d.ts"), `/** @lynxmodule */
4591
+ fs18.writeFileSync(path19.join(root, "src", "index.d.ts"), `/** @lynxmodule */
4244
4592
  export declare class ${simpleModuleName} {
4245
4593
  // Add your module methods here
4246
4594
  }
4247
4595
  `);
4248
- fs17.mkdirSync(path18.join(root, "android", "src", "main", "kotlin", pkgPath), { recursive: true });
4249
- fs17.writeFileSync(path18.join(root, "android", "build.gradle.kts"), `plugins {
4596
+ fs18.mkdirSync(path19.join(root, "android", "src", "main", "kotlin", pkgPath), { recursive: true });
4597
+ fs18.writeFileSync(path19.join(root, "android", "build.gradle.kts"), `plugins {
4250
4598
  id("com.android.library")
4251
4599
  id("org.jetbrains.kotlin.android")
4252
4600
  }
@@ -4267,7 +4615,7 @@ dependencies {
4267
4615
  implementation(libs.lynx.jssdk)
4268
4616
  }
4269
4617
  `);
4270
- fs17.writeFileSync(path18.join(root, "android", "src", "main", "AndroidManifest.xml"), `<?xml version="1.0" encoding="utf-8"?>
4618
+ fs18.writeFileSync(path19.join(root, "android", "src", "main", "AndroidManifest.xml"), `<?xml version="1.0" encoding="utf-8"?>
4271
4619
  <manifest />
4272
4620
  `);
4273
4621
  const ktContent = `package ${packageName}
@@ -4284,8 +4632,8 @@ class ${simpleModuleName}(context: Context) : LynxModule(context) {
4284
4632
  }
4285
4633
  }
4286
4634
  `;
4287
- fs17.writeFileSync(path18.join(root, "android", "src", "main", "kotlin", pkgPath, `${simpleModuleName}.kt`), ktContent);
4288
- fs17.mkdirSync(path18.join(root, "ios", extName, extName, "Classes"), { recursive: true });
4635
+ fs18.writeFileSync(path19.join(root, "android", "src", "main", "kotlin", pkgPath, `${simpleModuleName}.kt`), ktContent);
4636
+ fs18.mkdirSync(path19.join(root, "ios", extName, extName, "Classes"), { recursive: true });
4289
4637
  const podspec = `Pod::Spec.new do |s|
4290
4638
  s.name = '${extName}'
4291
4639
  s.version = '0.0.1'
@@ -4299,7 +4647,7 @@ class ${simpleModuleName}(context: Context) : LynxModule(context) {
4299
4647
  s.dependency 'Lynx'
4300
4648
  end
4301
4649
  `;
4302
- fs17.writeFileSync(path18.join(root, "ios", extName, `${extName}.podspec`), podspec);
4650
+ fs18.writeFileSync(path19.join(root, "ios", extName, `${extName}.podspec`), podspec);
4303
4651
  const swiftContent = `import Foundation
4304
4652
 
4305
4653
  @objc public class ${simpleModuleName}: NSObject {
@@ -4308,16 +4656,35 @@ end
4308
4656
  }
4309
4657
  }
4310
4658
  `;
4311
- fs17.writeFileSync(path18.join(root, "ios", extName, extName, "Classes", `${simpleModuleName}.swift`), swiftContent);
4659
+ fs18.writeFileSync(path19.join(root, "ios", extName, extName, "Classes", `${simpleModuleName}.swift`), swiftContent);
4312
4660
  }
4313
- fs17.writeFileSync(path18.join(root, "index.js"), `'use strict';
4661
+ if (includeElement && !includeModule) {
4662
+ const elementName = extName.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
4663
+ fs18.writeFileSync(path19.join(root, "src", "index.tsx"), `import type { FC } from '@lynx-js/react';
4664
+
4665
+ export const ${elementName}: FC = () => {
4666
+ return null;
4667
+ };
4668
+ `);
4669
+ }
4670
+ fs18.writeFileSync(path19.join(root, "index.js"), `'use strict';
4314
4671
  module.exports = {};
4315
4672
  `);
4316
- fs17.writeFileSync(path18.join(root, "tsconfig.json"), JSON.stringify({
4317
- compilerOptions: { target: "ES2020", module: "ESNext", moduleResolution: "bundler", strict: true },
4318
- include: ["src"]
4673
+ const tsconfigCompiler = {
4674
+ target: "ES2020",
4675
+ module: "ESNext",
4676
+ moduleResolution: "bundler",
4677
+ strict: true
4678
+ };
4679
+ if (includeElement) {
4680
+ tsconfigCompiler.jsx = "preserve";
4681
+ tsconfigCompiler.jsxImportSource = "@lynx-js/react";
4682
+ }
4683
+ fs18.writeFileSync(path19.join(root, "tsconfig.json"), JSON.stringify({
4684
+ compilerOptions: tsconfigCompiler,
4685
+ include: includeElement ? ["src", "src/**/*.tsx"] : ["src"]
4319
4686
  }, null, 2));
4320
- fs17.writeFileSync(path18.join(root, "README.md"), `# ${extName}
4687
+ fs18.writeFileSync(path19.join(root, "README.md"), `# ${extName}
4321
4688
 
4322
4689
  Lynx extension for ${extName}.
4323
4690
 
@@ -4342,8 +4709,8 @@ This package uses \`lynx.ext.json\` (RFC-compliant) for autolinking.
4342
4709
  var create_default3 = create3;
4343
4710
 
4344
4711
  // src/common/codegen.ts
4345
- import fs18 from "fs";
4346
- import path19 from "path";
4712
+ import fs19 from "fs";
4713
+ import path20 from "path";
4347
4714
  function codegen() {
4348
4715
  const cwd = process.cwd();
4349
4716
  const config = loadExtensionConfig(cwd);
@@ -4351,9 +4718,9 @@ function codegen() {
4351
4718
  console.error("\u274C No lynx.ext.json or tamer.json found. Run from an extension package root.");
4352
4719
  process.exit(1);
4353
4720
  }
4354
- const srcDir = path19.join(cwd, "src");
4355
- const generatedDir = path19.join(cwd, "generated");
4356
- fs18.mkdirSync(generatedDir, { recursive: true });
4721
+ const srcDir = path20.join(cwd, "src");
4722
+ const generatedDir = path20.join(cwd, "generated");
4723
+ fs19.mkdirSync(generatedDir, { recursive: true });
4357
4724
  const dtsFiles = findDtsFiles(srcDir);
4358
4725
  const modules = extractLynxModules(dtsFiles);
4359
4726
  if (modules.length === 0) {
@@ -4363,28 +4730,28 @@ function codegen() {
4363
4730
  for (const mod of modules) {
4364
4731
  const tsContent = `export type { ${mod} } from '../src/index.js';
4365
4732
  `;
4366
- const outPath = path19.join(generatedDir, `${mod}.ts`);
4367
- fs18.writeFileSync(outPath, tsContent);
4733
+ const outPath = path20.join(generatedDir, `${mod}.ts`);
4734
+ fs19.writeFileSync(outPath, tsContent);
4368
4735
  console.log(`\u2705 Generated ${outPath}`);
4369
4736
  }
4370
4737
  if (config.android) {
4371
- const androidGenerated = path19.join(cwd, "android", "src", "main", "kotlin", config.android.moduleClassName.replace(/\./g, "/").replace(/[^/]+$/, ""), "generated");
4372
- fs18.mkdirSync(androidGenerated, { recursive: true });
4738
+ const androidGenerated = path20.join(cwd, "android", "src", "main", "kotlin", config.android.moduleClassName.replace(/\./g, "/").replace(/[^/]+$/, ""), "generated");
4739
+ fs19.mkdirSync(androidGenerated, { recursive: true });
4373
4740
  console.log(`\u2139\uFE0F Android generated dir: ${androidGenerated} (spec generation coming soon)`);
4374
4741
  }
4375
4742
  if (config.ios) {
4376
- const iosGenerated = path19.join(cwd, "ios", "generated");
4377
- fs18.mkdirSync(iosGenerated, { recursive: true });
4743
+ const iosGenerated = path20.join(cwd, "ios", "generated");
4744
+ fs19.mkdirSync(iosGenerated, { recursive: true });
4378
4745
  console.log(`\u2139\uFE0F iOS generated dir: ${iosGenerated} (spec generation coming soon)`);
4379
4746
  }
4380
4747
  console.log("\u2728 Codegen complete.");
4381
4748
  }
4382
4749
  function findDtsFiles(dir) {
4383
4750
  const result = [];
4384
- if (!fs18.existsSync(dir)) return result;
4385
- const entries = fs18.readdirSync(dir, { withFileTypes: true });
4751
+ if (!fs19.existsSync(dir)) return result;
4752
+ const entries = fs19.readdirSync(dir, { withFileTypes: true });
4386
4753
  for (const e of entries) {
4387
- const full = path19.join(dir, e.name);
4754
+ const full = path20.join(dir, e.name);
4388
4755
  if (e.isDirectory()) result.push(...findDtsFiles(full));
4389
4756
  else if (e.name.endsWith(".d.ts")) result.push(full);
4390
4757
  }
@@ -4394,7 +4761,7 @@ function extractLynxModules(files) {
4394
4761
  const modules = [];
4395
4762
  const seen = /* @__PURE__ */ new Set();
4396
4763
  for (const file of files) {
4397
- const content = fs18.readFileSync(file, "utf8");
4764
+ const content = fs19.readFileSync(file, "utf8");
4398
4765
  const regex = /\/\*\*\s*@lynxmodule\s*\*\/\s*export\s+declare\s+class\s+(\w+)/g;
4399
4766
  let m;
4400
4767
  while ((m = regex.exec(content)) !== null) {
@@ -4410,12 +4777,55 @@ var codegen_default = codegen;
4410
4777
 
4411
4778
  // src/common/devServer.ts
4412
4779
  import { spawn } from "child_process";
4413
- import fs19 from "fs";
4780
+ import fs20 from "fs";
4414
4781
  import http from "http";
4415
4782
  import os4 from "os";
4416
- import path20 from "path";
4783
+ import path21 from "path";
4784
+ import readline3 from "readline";
4417
4785
  import { WebSocketServer } from "ws";
4418
4786
  var DEFAULT_PORT = 3e3;
4787
+ var STATIC_MIME = {
4788
+ ".png": "image/png",
4789
+ ".jpg": "image/jpeg",
4790
+ ".jpeg": "image/jpeg",
4791
+ ".webp": "image/webp",
4792
+ ".ico": "image/x-icon",
4793
+ ".gif": "image/gif",
4794
+ ".svg": "image/svg+xml",
4795
+ ".pdf": "application/pdf"
4796
+ };
4797
+ function sendFileFromDisk(res, absPath) {
4798
+ fs20.readFile(absPath, (err, data) => {
4799
+ if (err) {
4800
+ res.writeHead(404);
4801
+ res.end("Not found");
4802
+ return;
4803
+ }
4804
+ const ext = path21.extname(absPath).toLowerCase();
4805
+ res.setHeader("Content-Type", STATIC_MIME[ext] ?? "application/octet-stream");
4806
+ res.setHeader("Access-Control-Allow-Origin", "*");
4807
+ res.end(data);
4808
+ });
4809
+ }
4810
+ function isPortInUse(port) {
4811
+ return new Promise((resolve) => {
4812
+ const server = http.createServer();
4813
+ server.once("error", (err) => {
4814
+ resolve(err.code === "EADDRINUSE");
4815
+ });
4816
+ server.once("listening", () => {
4817
+ server.close(() => resolve(false));
4818
+ });
4819
+ server.listen(port, "127.0.0.1");
4820
+ });
4821
+ }
4822
+ async function findAvailablePort(preferred, maxAttempts = 10) {
4823
+ for (let i = 0; i < maxAttempts; i++) {
4824
+ const port = preferred + i;
4825
+ if (!await isPortInUse(port)) return port;
4826
+ }
4827
+ throw new Error(`No available port in range ${preferred}-${preferred + maxAttempts - 1}`);
4828
+ }
4419
4829
  function getLanIp() {
4420
4830
  const nets = os4.networkInterfaces();
4421
4831
  for (const name of Object.keys(nets)) {
@@ -4431,13 +4841,12 @@ async function startDevServer(opts) {
4431
4841
  const verbose = opts?.verbose ?? false;
4432
4842
  const resolved = resolveHostPaths();
4433
4843
  const { projectRoot, lynxProjectDir, lynxBundlePath, lynxBundleFile, config } = resolved;
4434
- const distDir = path20.dirname(lynxBundlePath);
4435
- const port = config.devServer?.port ?? config.devServer?.httpPort ?? DEFAULT_PORT;
4844
+ const distDir = path21.dirname(lynxBundlePath);
4436
4845
  let buildProcess = null;
4437
4846
  function detectPackageManager2(cwd) {
4438
- const dir = path20.resolve(cwd);
4439
- if (fs19.existsSync(path20.join(dir, "pnpm-lock.yaml"))) return { cmd: "pnpm", args: ["run", "build"] };
4440
- if (fs19.existsSync(path20.join(dir, "bun.lockb")) || fs19.existsSync(path20.join(dir, "bun.lock"))) return { cmd: "bun", args: ["run", "build"] };
4847
+ const dir = path21.resolve(cwd);
4848
+ if (fs20.existsSync(path21.join(dir, "pnpm-lock.yaml"))) return { cmd: "pnpm", args: ["run", "build"] };
4849
+ if (fs20.existsSync(path21.join(dir, "bun.lockb")) || fs20.existsSync(path21.join(dir, "bun.lock"))) return { cmd: "bun", args: ["run", "build"] };
4441
4850
  return { cmd: "npm", args: ["run", "build"] };
4442
4851
  }
4443
4852
  function runBuild() {
@@ -4459,27 +4868,27 @@ async function startDevServer(opts) {
4459
4868
  });
4460
4869
  });
4461
4870
  }
4462
- const projectName = path20.basename(lynxProjectDir);
4871
+ const preferredPort = config.devServer?.port ?? config.devServer?.httpPort ?? DEFAULT_PORT;
4872
+ const port = await findAvailablePort(preferredPort);
4873
+ if (port !== preferredPort) {
4874
+ console.log(`\x1B[33m\u26A0 Port ${preferredPort} in use, using ${port}\x1B[0m`);
4875
+ }
4876
+ const projectName = path21.basename(lynxProjectDir);
4463
4877
  const basePath = `/${projectName}`;
4464
4878
  const iconPaths = resolveIconPaths(projectRoot, config);
4465
4879
  let iconFilePath = null;
4466
- if (iconPaths?.source && fs19.statSync(iconPaths.source).isFile()) {
4880
+ if (iconPaths?.source && fs20.statSync(iconPaths.source).isFile()) {
4467
4881
  iconFilePath = iconPaths.source;
4882
+ } else if (iconPaths?.androidAdaptiveForeground && fs20.statSync(iconPaths.androidAdaptiveForeground).isFile()) {
4883
+ iconFilePath = iconPaths.androidAdaptiveForeground;
4468
4884
  } else if (iconPaths?.android) {
4469
- const androidIcon = path20.join(iconPaths.android, "mipmap-xxxhdpi", "ic_launcher.png");
4470
- if (fs19.existsSync(androidIcon)) iconFilePath = androidIcon;
4885
+ const androidIcon = path21.join(iconPaths.android, "mipmap-xxxhdpi", "ic_launcher.png");
4886
+ if (fs20.existsSync(androidIcon)) iconFilePath = androidIcon;
4471
4887
  } else if (iconPaths?.ios) {
4472
- const iosIcon = path20.join(iconPaths.ios, "Icon-1024.png");
4473
- if (fs19.existsSync(iosIcon)) iconFilePath = iosIcon;
4474
- }
4475
- const iconExt = iconFilePath ? path20.extname(iconFilePath) || ".png" : "";
4476
- const iconMime = {
4477
- ".png": "image/png",
4478
- ".jpg": "image/jpeg",
4479
- ".jpeg": "image/jpeg",
4480
- ".webp": "image/webp",
4481
- ".ico": "image/x-icon"
4482
- };
4888
+ const iosIcon = path21.join(iconPaths.ios, "Icon-1024.png");
4889
+ if (fs20.existsSync(iosIcon)) iconFilePath = iosIcon;
4890
+ }
4891
+ const iconExt = iconFilePath ? path21.extname(iconFilePath) || ".png" : "";
4483
4892
  const httpServer = http.createServer((req, res) => {
4484
4893
  let reqPath = (req.url || "/").split("?")[0];
4485
4894
  if (reqPath === `${basePath}/status`) {
@@ -4491,6 +4900,11 @@ async function startDevServer(opts) {
4491
4900
  if (reqPath === `${basePath}/meta.json`) {
4492
4901
  const lanIp = getLanIp();
4493
4902
  const nativeModules = discoverNativeExtensions(projectRoot);
4903
+ const androidPackageName = config.android?.packageName?.trim();
4904
+ const iosBundleId = config.ios?.bundleId?.trim();
4905
+ const idParts = [androidPackageName?.toLowerCase(), iosBundleId?.toLowerCase()].filter(
4906
+ (x) => Boolean(x)
4907
+ );
4494
4908
  const meta = {
4495
4909
  name: projectName,
4496
4910
  slug: projectName,
@@ -4502,6 +4916,15 @@ async function startDevServer(opts) {
4502
4916
  packagerStatus: "running",
4503
4917
  nativeModules: nativeModules.map((m) => ({ packageName: m.packageName, moduleClassName: m.moduleClassName }))
4504
4918
  };
4919
+ if (androidPackageName) meta.androidPackageName = androidPackageName;
4920
+ if (iosBundleId) meta.iosBundleId = iosBundleId;
4921
+ if (idParts.length > 0) meta.tamerAppKey = idParts.join("|");
4922
+ const rawIcon = config.icon;
4923
+ if (rawIcon && typeof rawIcon === "object" && "source" in rawIcon && typeof rawIcon.source === "string") {
4924
+ meta.iconSource = rawIcon.source;
4925
+ } else if (typeof rawIcon === "string") {
4926
+ meta.iconSource = rawIcon;
4927
+ }
4505
4928
  if (iconFilePath) {
4506
4929
  meta.icon = `http://${lanIp}:${port}${basePath}/icon${iconExt}`;
4507
4930
  }
@@ -4511,32 +4934,67 @@ async function startDevServer(opts) {
4511
4934
  return;
4512
4935
  }
4513
4936
  if (iconFilePath && (reqPath === `${basePath}/icon` || reqPath === `${basePath}/icon${iconExt}`)) {
4514
- fs19.readFile(iconFilePath, (err, data) => {
4937
+ fs20.readFile(iconFilePath, (err, data) => {
4515
4938
  if (err) {
4516
4939
  res.writeHead(404);
4517
4940
  res.end();
4518
4941
  return;
4519
4942
  }
4520
- res.setHeader("Content-Type", iconMime[iconExt] ?? "image/png");
4943
+ res.setHeader("Content-Type", STATIC_MIME[iconExt] ?? "image/png");
4521
4944
  res.setHeader("Access-Control-Allow-Origin", "*");
4522
4945
  res.end(data);
4523
4946
  });
4524
4947
  return;
4525
4948
  }
4949
+ const lynxStaticMounts = [
4950
+ { prefix: `${basePath}/src/assets/`, rootSub: "src/assets" },
4951
+ { prefix: `${basePath}/assets/`, rootSub: "assets" }
4952
+ ];
4953
+ for (const { prefix, rootSub } of lynxStaticMounts) {
4954
+ if (!reqPath.startsWith(prefix)) continue;
4955
+ let rel = reqPath.slice(prefix.length);
4956
+ try {
4957
+ rel = decodeURIComponent(rel);
4958
+ } catch {
4959
+ res.writeHead(400);
4960
+ res.end();
4961
+ return;
4962
+ }
4963
+ const safe = path21.normalize(rel).replace(/^(\.\.(\/|\\|$))+/, "");
4964
+ if (path21.isAbsolute(safe) || safe.startsWith("..")) {
4965
+ res.writeHead(403);
4966
+ res.end();
4967
+ return;
4968
+ }
4969
+ const allowedRoot = path21.resolve(lynxProjectDir, rootSub);
4970
+ const abs = path21.resolve(allowedRoot, safe);
4971
+ if (!abs.startsWith(allowedRoot + path21.sep) && abs !== allowedRoot) {
4972
+ res.writeHead(403);
4973
+ res.end();
4974
+ return;
4975
+ }
4976
+ if (!fs20.existsSync(abs) || !fs20.statSync(abs).isFile()) {
4977
+ res.writeHead(404);
4978
+ res.end("Not found");
4979
+ return;
4980
+ }
4981
+ sendFileFromDisk(res, abs);
4982
+ return;
4983
+ }
4526
4984
  if (reqPath === "/" || reqPath === basePath || reqPath === `${basePath}/`) {
4527
4985
  reqPath = `${basePath}/${lynxBundleFile}`;
4528
4986
  } else if (!reqPath.startsWith(basePath)) {
4529
4987
  reqPath = basePath + (reqPath.startsWith("/") ? reqPath : "/" + reqPath);
4530
4988
  }
4531
4989
  const relPath = reqPath.replace(basePath, "").replace(/^\//, "") || lynxBundleFile;
4532
- const filePath = path20.resolve(distDir, relPath);
4533
- const distResolved = path20.resolve(distDir);
4534
- if (!filePath.startsWith(distResolved + path20.sep) && filePath !== distResolved) {
4990
+ const filePath = path21.resolve(distDir, relPath);
4991
+ const distResolved = path21.resolve(distDir);
4992
+ if (!filePath.startsWith(distResolved + path21.sep) && filePath !== distResolved) {
4535
4993
  res.writeHead(403);
4536
4994
  res.end();
4537
4995
  return;
4538
4996
  }
4539
- fs19.readFile(filePath, (err, data) => {
4997
+ fs20.readFile(filePath, (err, data) => {
4540
4998
  if (err) {
4541
4999
  res.writeHead(404);
4542
5000
  res.end("Not found");
@@ -4590,10 +5048,10 @@ async function startDevServer(opts) {
4590
5048
  }
4591
5049
  if (chokidar) {
4592
5050
  const watchPaths = [
4593
- path20.join(lynxProjectDir, "src"),
4594
- path20.join(lynxProjectDir, "lynx.config.ts"),
4595
- path20.join(lynxProjectDir, "lynx.config.js")
4596
- ].filter((p) => fs19.existsSync(p));
5051
+ path21.join(lynxProjectDir, "src"),
5052
+ path21.join(lynxProjectDir, "lynx.config.ts"),
5053
+ path21.join(lynxProjectDir, "lynx.config.js")
5054
+ ].filter((p) => fs20.existsSync(p));
4597
5055
  if (watchPaths.length > 0) {
4598
5056
  const watcher = chokidar.watch(watchPaths, { ignoreInitial: true });
4599
5057
  watcher.on("change", async () => {
@@ -4646,6 +5104,36 @@ async function startDevServer(opts) {
4646
5104
  qrcode.generate(devUrl, { small: true });
4647
5105
  }).catch(() => {
4648
5106
  });
5107
+ if (process.stdin.isTTY) {
5108
+ readline3.emitKeypressEvents(process.stdin);
5109
+ process.stdin.setRawMode(true);
5110
+ process.stdin.resume();
5111
+ process.stdin.setEncoding("utf8");
5112
+ const help = "\x1B[90m r: refresh c/Ctrl+L: clear Ctrl+C: exit\x1B[0m";
5113
+ console.log(help);
5114
+ process.stdin.on("keypress", (str, key) => {
5115
+ if (key.ctrl && key.name === "c") {
5116
+ void cleanup();
5117
+ return;
5118
+ }
5119
+ switch (key.name) {
5120
+ case "r":
5121
+ runBuild().then(() => {
5122
+ broadcastReload();
5123
+ console.log("\u{1F504} Refreshed, clients notified");
5124
+ }).catch((e) => console.error("Build failed:", e.message));
5125
+ break;
5126
+ case "c":
5127
+ process.stdout.write("\x1B[2J\x1B[H");
5128
+ break;
5129
+ case "l":
5130
+ if (key.ctrl) process.stdout.write("\x1B[2J\x1B[H");
5131
+ break;
5132
+ default:
5133
+ break;
5134
+ }
5135
+ });
5136
+ }
4649
5137
  });
4650
5138
  const cleanup = async () => {
4651
5139
  buildProcess?.kill();
@@ -4672,10 +5160,10 @@ async function start(opts) {
4672
5160
  var start_default = start;
4673
5161
 
4674
5162
  // src/common/injectHost.ts
4675
- import fs20 from "fs";
4676
- import path21 from "path";
5163
+ import fs21 from "fs";
5164
+ import path22 from "path";
4677
5165
  function readAndSubstitute(templatePath, vars) {
4678
- const raw = fs20.readFileSync(templatePath, "utf-8");
5166
+ const raw = fs21.readFileSync(templatePath, "utf-8");
4679
5167
  return Object.entries(vars).reduce(
4680
5168
  (s, [k, v]) => s.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), v),
4681
5169
  raw
@@ -4696,32 +5184,32 @@ async function injectHostAndroid(opts) {
4696
5184
  process.exit(1);
4697
5185
  }
4698
5186
  const androidDir = config.paths?.androidDir ?? "android";
4699
- const rootDir = path21.join(projectRoot, androidDir);
5187
+ const rootDir = path22.join(projectRoot, androidDir);
4700
5188
  const packagePath = packageName.replace(/\./g, "/");
4701
- const javaDir = path21.join(rootDir, "app", "src", "main", "java", packagePath);
4702
- const kotlinDir = path21.join(rootDir, "app", "src", "main", "kotlin", packagePath);
4703
- if (!fs20.existsSync(javaDir) || !fs20.existsSync(kotlinDir)) {
5189
+ const javaDir = path22.join(rootDir, "app", "src", "main", "java", packagePath);
5190
+ const kotlinDir = path22.join(rootDir, "app", "src", "main", "kotlin", packagePath);
5191
+ if (!fs21.existsSync(javaDir) || !fs21.existsSync(kotlinDir)) {
4704
5192
  console.error("\u274C Android project not found. Run `t4l android create` first or ensure android/ exists.");
4705
5193
  process.exit(1);
4706
5194
  }
4707
- const templateDir = path21.join(hostPkg, "android", "templates");
5195
+ const templateDir = path22.join(hostPkg, "android", "templates");
4708
5196
  const vars = { PACKAGE_NAME: packageName, APP_NAME: appName };
4709
5197
  const files = [
4710
- { src: "App.java", dst: path21.join(javaDir, "App.java") },
4711
- { src: "TemplateProvider.java", dst: path21.join(javaDir, "TemplateProvider.java") },
4712
- { src: "MainActivity.kt", dst: path21.join(kotlinDir, "MainActivity.kt") }
5198
+ { src: "App.java", dst: path22.join(javaDir, "App.java") },
5199
+ { src: "TemplateProvider.java", dst: path22.join(javaDir, "TemplateProvider.java") },
5200
+ { src: "MainActivity.kt", dst: path22.join(kotlinDir, "MainActivity.kt") }
4713
5201
  ];
4714
5202
  for (const { src, dst } of files) {
4715
- const srcPath = path21.join(templateDir, src);
4716
- if (!fs20.existsSync(srcPath)) continue;
4717
- if (fs20.existsSync(dst) && !opts?.force) {
4718
- console.log(`\u23ED\uFE0F Skipping ${path21.basename(dst)} (use --force to overwrite)`);
5203
+ const srcPath = path22.join(templateDir, src);
5204
+ if (!fs21.existsSync(srcPath)) continue;
5205
+ if (fs21.existsSync(dst) && !opts?.force) {
5206
+ console.log(`\u23ED\uFE0F Skipping ${path22.basename(dst)} (use --force to overwrite)`);
4719
5207
  continue;
4720
5208
  }
4721
5209
  const content = readAndSubstitute(srcPath, vars);
4722
- fs20.mkdirSync(path21.dirname(dst), { recursive: true });
4723
- fs20.writeFileSync(dst, content);
4724
- console.log(`\u2705 Injected ${path21.basename(dst)}`);
5210
+ fs21.mkdirSync(path22.dirname(dst), { recursive: true });
5211
+ fs21.writeFileSync(dst, content);
5212
+ console.log(`\u2705 Injected ${path22.basename(dst)}`);
4725
5213
  }
4726
5214
  }
4727
5215
  async function injectHostIos(opts) {
@@ -4739,13 +5227,13 @@ async function injectHostIos(opts) {
4739
5227
  process.exit(1);
4740
5228
  }
4741
5229
  const iosDir = config.paths?.iosDir ?? "ios";
4742
- const rootDir = path21.join(projectRoot, iosDir);
4743
- const projectDir = path21.join(rootDir, appName);
4744
- if (!fs20.existsSync(projectDir)) {
5230
+ const rootDir = path22.join(projectRoot, iosDir);
5231
+ const projectDir = path22.join(rootDir, appName);
5232
+ if (!fs21.existsSync(projectDir)) {
4745
5233
  console.error("\u274C iOS project not found. Run `t4l ios create` first or ensure ios/ exists.");
4746
5234
  process.exit(1);
4747
5235
  }
4748
- const templateDir = path21.join(hostPkg, "ios", "templates");
5236
+ const templateDir = path22.join(hostPkg, "ios", "templates");
4749
5237
  const vars = { PACKAGE_NAME: bundleId, APP_NAME: appName, BUNDLE_ID: bundleId };
4750
5238
  const files = [
4751
5239
  "AppDelegate.swift",
@@ -4755,23 +5243,23 @@ async function injectHostIos(opts) {
4755
5243
  "LynxInitProcessor.swift"
4756
5244
  ];
4757
5245
  for (const f of files) {
4758
- const srcPath = path21.join(templateDir, f);
4759
- const dstPath = path21.join(projectDir, f);
4760
- if (!fs20.existsSync(srcPath)) continue;
4761
- if (fs20.existsSync(dstPath) && !opts?.force) {
5246
+ const srcPath = path22.join(templateDir, f);
5247
+ const dstPath = path22.join(projectDir, f);
5248
+ if (!fs21.existsSync(srcPath)) continue;
5249
+ if (fs21.existsSync(dstPath) && !opts?.force) {
4762
5250
  console.log(`\u23ED\uFE0F Skipping ${f} (use --force to overwrite)`);
4763
5251
  continue;
4764
5252
  }
4765
5253
  const content = readAndSubstitute(srcPath, vars);
4766
- fs20.writeFileSync(dstPath, content);
5254
+ fs21.writeFileSync(dstPath, content);
4767
5255
  console.log(`\u2705 Injected ${f}`);
4768
5256
  }
4769
5257
  }
4770
5258
 
4771
5259
  // src/common/buildEmbeddable.ts
4772
- import fs21 from "fs";
4773
- import path22 from "path";
4774
- import { execSync as execSync8 } from "child_process";
5260
+ import fs22 from "fs";
5261
+ import path23 from "path";
5262
+ import { execSync as execSync9 } from "child_process";
4775
5263
  var EMBEDDABLE_DIR = "embeddable";
4776
5264
  var LIB_PACKAGE = "com.tamer.embeddable";
4777
5265
  var GRADLE_VERSION = "8.14.2";
@@ -4847,14 +5335,14 @@ object LynxEmbeddable {
4847
5335
  }
4848
5336
  `;
4849
5337
  function generateAndroidLibrary(outDir, androidDir, projectRoot, lynxBundlePath, lynxBundleFile, modules, abiFilters) {
4850
- const libDir = path22.join(androidDir, "lib");
4851
- const libSrcMain = path22.join(libDir, "src", "main");
4852
- const assetsDir = path22.join(libSrcMain, "assets");
4853
- const kotlinDir = path22.join(libSrcMain, "kotlin", LIB_PACKAGE.replace(/\./g, "/"));
4854
- const generatedDir = path22.join(kotlinDir, "generated");
4855
- fs21.mkdirSync(path22.join(androidDir, "gradle"), { recursive: true });
4856
- fs21.mkdirSync(generatedDir, { recursive: true });
4857
- fs21.mkdirSync(assetsDir, { recursive: true });
5338
+ const libDir = path23.join(androidDir, "lib");
5339
+ const libSrcMain = path23.join(libDir, "src", "main");
5340
+ const assetsDir = path23.join(libSrcMain, "assets");
5341
+ const kotlinDir = path23.join(libSrcMain, "kotlin", LIB_PACKAGE.replace(/\./g, "/"));
5342
+ const generatedDir = path23.join(kotlinDir, "generated");
5343
+ fs22.mkdirSync(path23.join(androidDir, "gradle"), { recursive: true });
5344
+ fs22.mkdirSync(generatedDir, { recursive: true });
5345
+ fs22.mkdirSync(assetsDir, { recursive: true });
4858
5346
  const androidModules = modules.filter((m) => m.config.android);
4859
5347
  const abiList = abiFilters.map((a) => `"${a}"`).join(", ");
4860
5348
  const settingsContent = `pluginManagement {
@@ -4874,7 +5362,7 @@ include(":lib")
4874
5362
  ${androidModules.map((p) => {
4875
5363
  const gradleName = p.name.replace(/^@/, "").replace(/\//g, "_");
4876
5364
  const sourceDir = p.config.android?.sourceDir || "android";
4877
- const absPath = path22.join(p.packagePath, sourceDir).replace(/\\/g, "/");
5365
+ const absPath = path23.join(p.packagePath, sourceDir).replace(/\\/g, "/");
4878
5366
  return `include(":${gradleName}")
4879
5367
  project(":${gradleName}").projectDir = file("${absPath}")`;
4880
5368
  }).join("\n")}
@@ -4923,10 +5411,10 @@ dependencies {
4923
5411
  ${libDeps}
4924
5412
  }
4925
5413
  `;
4926
- fs21.writeFileSync(path22.join(androidDir, "gradle", "libs.versions.toml"), LIBS_VERSIONS_TOML);
4927
- fs21.writeFileSync(path22.join(androidDir, "settings.gradle.kts"), settingsContent);
4928
- fs21.writeFileSync(
4929
- path22.join(androidDir, "build.gradle.kts"),
5414
+ fs22.writeFileSync(path23.join(androidDir, "gradle", "libs.versions.toml"), LIBS_VERSIONS_TOML);
5415
+ fs22.writeFileSync(path23.join(androidDir, "settings.gradle.kts"), settingsContent);
5416
+ fs22.writeFileSync(
5417
+ path23.join(androidDir, "build.gradle.kts"),
4930
5418
  `plugins {
4931
5419
  alias(libs.plugins.android.library) apply false
4932
5420
  alias(libs.plugins.kotlin.android) apply false
@@ -4934,26 +5422,26 @@ ${libDeps}
4934
5422
  }
4935
5423
  `
4936
5424
  );
4937
- fs21.writeFileSync(
4938
- path22.join(androidDir, "gradle.properties"),
5425
+ fs22.writeFileSync(
5426
+ path23.join(androidDir, "gradle.properties"),
4939
5427
  `org.gradle.jvmargs=-Xmx2048m
4940
5428
  android.useAndroidX=true
4941
5429
  kotlin.code.style=official
4942
5430
  `
4943
5431
  );
4944
- fs21.writeFileSync(path22.join(libDir, "build.gradle.kts"), libBuildContent);
4945
- fs21.writeFileSync(
4946
- path22.join(libSrcMain, "AndroidManifest.xml"),
5432
+ fs22.writeFileSync(path23.join(libDir, "build.gradle.kts"), libBuildContent);
5433
+ fs22.writeFileSync(
5434
+ path23.join(libSrcMain, "AndroidManifest.xml"),
4947
5435
  '<?xml version="1.0" encoding="utf-8"?>\n<manifest />'
4948
5436
  );
4949
- fs21.copyFileSync(lynxBundlePath, path22.join(assetsDir, lynxBundleFile));
4950
- fs21.writeFileSync(path22.join(kotlinDir, "LynxEmbeddable.kt"), LYNX_EMBEDDABLE_KT);
4951
- fs21.writeFileSync(
4952
- path22.join(generatedDir, "GeneratedLynxExtensions.kt"),
5437
+ fs22.copyFileSync(lynxBundlePath, path23.join(assetsDir, lynxBundleFile));
5438
+ fs22.writeFileSync(path23.join(kotlinDir, "LynxEmbeddable.kt"), LYNX_EMBEDDABLE_KT);
5439
+ fs22.writeFileSync(
5440
+ path23.join(generatedDir, "GeneratedLynxExtensions.kt"),
4953
5441
  generateLynxExtensionsKotlin(modules, LIB_PACKAGE)
4954
5442
  );
4955
- fs21.writeFileSync(
4956
- path22.join(generatedDir, "GeneratedActivityLifecycle.kt"),
5443
+ fs22.writeFileSync(
5444
+ path23.join(generatedDir, "GeneratedActivityLifecycle.kt"),
4957
5445
  generateActivityLifecycleKotlin(modules, LIB_PACKAGE)
4958
5446
  );
4959
5447
  }
@@ -4961,21 +5449,21 @@ async function buildEmbeddable(opts = {}) {
4961
5449
  const resolved = resolveHostPaths();
4962
5450
  const { lynxProjectDir, lynxBundlePath, lynxBundleFile, projectRoot, config } = resolved;
4963
5451
  console.log("\u{1F4E6} Building Lynx project (release)...");
4964
- execSync8("npm run build", { stdio: "inherit", cwd: lynxProjectDir });
4965
- if (!fs21.existsSync(lynxBundlePath)) {
5452
+ execSync9("npm run build", { stdio: "inherit", cwd: lynxProjectDir });
5453
+ if (!fs22.existsSync(lynxBundlePath)) {
4966
5454
  console.error(`\u274C Bundle not found at ${lynxBundlePath}`);
4967
5455
  process.exit(1);
4968
5456
  }
4969
- const outDir = path22.join(projectRoot, EMBEDDABLE_DIR);
4970
- fs21.mkdirSync(outDir, { recursive: true });
4971
- const distDir = path22.dirname(lynxBundlePath);
5457
+ const outDir = path23.join(projectRoot, EMBEDDABLE_DIR);
5458
+ fs22.mkdirSync(outDir, { recursive: true });
5459
+ const distDir = path23.dirname(lynxBundlePath);
4972
5460
  copyDistAssets(distDir, outDir, lynxBundleFile);
4973
5461
  const modules = discoverModules(projectRoot);
4974
5462
  const androidModules = modules.filter((m) => m.config.android);
4975
5463
  const abiFilters = resolveAbiFilters(config);
4976
- const androidDir = path22.join(outDir, "android");
4977
- if (fs21.existsSync(androidDir)) fs21.rmSync(androidDir, { recursive: true });
4978
- fs21.mkdirSync(androidDir, { recursive: true });
5464
+ const androidDir = path23.join(outDir, "android");
5465
+ if (fs22.existsSync(androidDir)) fs22.rmSync(androidDir, { recursive: true });
5466
+ fs22.mkdirSync(androidDir, { recursive: true });
4979
5467
  generateAndroidLibrary(
4980
5468
  outDir,
4981
5469
  androidDir,
@@ -4985,23 +5473,23 @@ async function buildEmbeddable(opts = {}) {
4985
5473
  modules,
4986
5474
  abiFilters
4987
5475
  );
4988
- const gradlewPath = path22.join(androidDir, "gradlew");
5476
+ const gradlewPath = path23.join(androidDir, "gradlew");
4989
5477
  const devAppDir = findDevAppPackage(projectRoot);
4990
5478
  const existingGradleDirs = [
4991
- path22.join(projectRoot, "android"),
4992
- devAppDir ? path22.join(devAppDir, "android") : null
5479
+ path23.join(projectRoot, "android"),
5480
+ devAppDir ? path23.join(devAppDir, "android") : null
4993
5481
  ].filter(Boolean);
4994
5482
  let hasWrapper = false;
4995
5483
  for (const d of existingGradleDirs) {
4996
- if (fs21.existsSync(path22.join(d, "gradlew"))) {
5484
+ if (fs22.existsSync(path23.join(d, "gradlew"))) {
4997
5485
  for (const name of ["gradlew", "gradlew.bat", "gradle"]) {
4998
- const src = path22.join(d, name);
4999
- if (fs21.existsSync(src)) {
5000
- const dest = path22.join(androidDir, name);
5001
- if (fs21.statSync(src).isDirectory()) {
5002
- fs21.cpSync(src, dest, { recursive: true });
5486
+ const src = path23.join(d, name);
5487
+ if (fs22.existsSync(src)) {
5488
+ const dest = path23.join(androidDir, name);
5489
+ if (fs22.statSync(src).isDirectory()) {
5490
+ fs22.cpSync(src, dest, { recursive: true });
5003
5491
  } else {
5004
- fs21.copyFileSync(src, dest);
5492
+ fs22.copyFileSync(src, dest);
5005
5493
  }
5006
5494
  }
5007
5495
  }
@@ -5015,15 +5503,15 @@ async function buildEmbeddable(opts = {}) {
5015
5503
  }
5016
5504
  try {
5017
5505
  console.log("\u{1F4E6} Building Android AAR...");
5018
- execSync8("./gradlew :lib:assembleRelease", { cwd: androidDir, stdio: "inherit" });
5506
+ execSync9("./gradlew :lib:assembleRelease", { cwd: androidDir, stdio: "inherit" });
5019
5507
  } catch (e) {
5020
5508
  console.error("\u274C Android AAR build failed. Run manually: cd embeddable/android && ./gradlew :lib:assembleRelease");
5021
5509
  throw e;
5022
5510
  }
5023
- const aarSrc = path22.join(androidDir, "lib", "build", "outputs", "aar", "lib-release.aar");
5024
- const aarDest = path22.join(outDir, "tamer-embeddable.aar");
5025
- if (fs21.existsSync(aarSrc)) {
5026
- fs21.copyFileSync(aarSrc, aarDest);
5511
+ const aarSrc = path23.join(androidDir, "lib", "build", "outputs", "aar", "lib-release.aar");
5512
+ const aarDest = path23.join(outDir, "tamer-embeddable.aar");
5513
+ if (fs22.existsSync(aarSrc)) {
5514
+ fs22.copyFileSync(aarSrc, aarDest);
5027
5515
  console.log(` - tamer-embeddable.aar`);
5028
5516
  }
5029
5517
  const snippetAndroid = `// Add to your app's build.gradle:
@@ -5034,7 +5522,7 @@ async function buildEmbeddable(opts = {}) {
5034
5522
  // LynxEmbeddable.init(applicationContext)
5035
5523
  // val lynxView = LynxEmbeddable.buildLynxView(containerViewGroup)
5036
5524
  `;
5037
- fs21.writeFileSync(path22.join(outDir, "snippet-android.kt"), snippetAndroid);
5525
+ fs22.writeFileSync(path23.join(outDir, "snippet-android.kt"), snippetAndroid);
5038
5526
  generateIosPod(outDir, projectRoot, lynxBundlePath, lynxBundleFile, modules);
5039
5527
  const readme = `# Embeddable Lynx Bundle
5040
5528
 
@@ -5065,7 +5553,7 @@ Add the \`Podfile.snippet\` entries to your Podfile (inside your app target), th
5065
5553
 
5066
5554
  - [Embedding LynxView](https://lynxjs.org/guide/embed-lynx-to-native)
5067
5555
  `;
5068
- fs21.writeFileSync(path22.join(outDir, "README.md"), readme);
5556
+ fs22.writeFileSync(path23.join(outDir, "README.md"), readme);
5069
5557
  console.log(`
5070
5558
  \u2705 Embeddable output at ${outDir}/`);
5071
5559
  console.log(" - main.lynx.bundle");
@@ -5077,20 +5565,20 @@ Add the \`Podfile.snippet\` entries to your Podfile (inside your app target), th
5077
5565
  console.log(" - README.md");
5078
5566
  }
5079
5567
  function generateIosPod(outDir, projectRoot, lynxBundlePath, lynxBundleFile, modules) {
5080
- const iosDir = path22.join(outDir, "ios");
5081
- const podDir = path22.join(iosDir, "TamerEmbeddable");
5082
- const resourcesDir = path22.join(podDir, "Resources");
5083
- fs21.mkdirSync(resourcesDir, { recursive: true });
5084
- fs21.copyFileSync(lynxBundlePath, path22.join(resourcesDir, lynxBundleFile));
5568
+ const iosDir = path23.join(outDir, "ios");
5569
+ const podDir = path23.join(iosDir, "TamerEmbeddable");
5570
+ const resourcesDir = path23.join(podDir, "Resources");
5571
+ fs22.mkdirSync(resourcesDir, { recursive: true });
5572
+ fs22.copyFileSync(lynxBundlePath, path23.join(resourcesDir, lynxBundleFile));
5085
5573
  const iosModules = modules.filter((m) => m.config.ios);
5086
5574
  const podDeps = iosModules.map((p) => {
5087
5575
  const podspecPath = p.config.ios?.podspecPath || ".";
5088
- const podspecDir = path22.join(p.packagePath, podspecPath);
5089
- if (!fs21.existsSync(podspecDir)) return null;
5090
- const files = fs21.readdirSync(podspecDir);
5576
+ const podspecDir = path23.join(p.packagePath, podspecPath);
5577
+ if (!fs22.existsSync(podspecDir)) return null;
5578
+ const files = fs22.readdirSync(podspecDir);
5091
5579
  const podspecFile = files.find((f) => f.endsWith(".podspec"));
5092
5580
  const podName = podspecFile ? podspecFile.replace(".podspec", "") : p.name.split("/").pop().replace(/-/g, "");
5093
- const absPath = path22.resolve(podspecDir);
5581
+ const absPath = path23.resolve(podspecDir);
5094
5582
  return { podName, absPath };
5095
5583
  }).filter(Boolean);
5096
5584
  const podDepLines = podDeps.map((d) => ` s.dependency '${d.podName}'`).join("\n");
@@ -5130,9 +5618,9 @@ end
5130
5618
  });
5131
5619
  const swiftImports = iosModules.map((p) => {
5132
5620
  const podspecPath = p.config.ios?.podspecPath || ".";
5133
- const podspecDir = path22.join(p.packagePath, podspecPath);
5134
- if (!fs21.existsSync(podspecDir)) return null;
5135
- const files = fs21.readdirSync(podspecDir);
5621
+ const podspecDir = path23.join(p.packagePath, podspecPath);
5622
+ if (!fs22.existsSync(podspecDir)) return null;
5623
+ const files = fs22.readdirSync(podspecDir);
5136
5624
  const podspecFile = files.find((f) => f.endsWith(".podspec"));
5137
5625
  return podspecFile ? podspecFile.replace(".podspec", "") : null;
5138
5626
  }).filter(Boolean);
@@ -5151,17 +5639,17 @@ ${regBlock}
5151
5639
  }
5152
5640
  }
5153
5641
  `;
5154
- fs21.writeFileSync(path22.join(iosDir, "TamerEmbeddable.podspec"), podspecContent);
5155
- fs21.writeFileSync(path22.join(podDir, "LynxEmbeddable.swift"), lynxEmbeddableSwift);
5156
- const absIosDir = path22.resolve(iosDir);
5642
+ fs22.writeFileSync(path23.join(iosDir, "TamerEmbeddable.podspec"), podspecContent);
5643
+ fs22.writeFileSync(path23.join(podDir, "LynxEmbeddable.swift"), lynxEmbeddableSwift);
5644
+ const absIosDir = path23.resolve(iosDir);
5157
5645
  const podfileSnippet = `# Paste into your app target in Podfile:
5158
5646
 
5159
5647
  pod 'TamerEmbeddable', :path => '${absIosDir}'
5160
5648
  ${podDeps.map((d) => `pod '${d.podName}', :path => '${d.absPath}'`).join("\n")}
5161
5649
  `;
5162
- fs21.writeFileSync(path22.join(iosDir, "Podfile.snippet"), podfileSnippet);
5163
- fs21.writeFileSync(
5164
- path22.join(outDir, "snippet-ios.swift"),
5650
+ fs22.writeFileSync(path23.join(iosDir, "Podfile.snippet"), podfileSnippet);
5651
+ fs22.writeFileSync(
5652
+ path23.join(outDir, "snippet-ios.swift"),
5165
5653
  `// Add LynxEmbeddable.initEnvironment() in your AppDelegate/SceneDelegate before presenting LynxView.
5166
5654
  // Then create LynxView with your bundle URL (main.lynx.bundle is in the pod resources).
5167
5655
  `
@@ -5169,32 +5657,29 @@ ${podDeps.map((d) => `pod '${d.podName}', :path => '${d.absPath}'`).join("\n")}
5169
5657
  }
5170
5658
 
5171
5659
  // src/common/add.ts
5172
- import fs22 from "fs";
5173
- import path23 from "path";
5174
- import { execSync as execSync9 } from "child_process";
5660
+ import fs23 from "fs";
5661
+ import path24 from "path";
5662
+ import { execSync as execSync10 } from "child_process";
5175
5663
  var CORE_PACKAGES = [
5176
5664
  "@tamer4lynx/tamer-app-shell",
5177
5665
  "@tamer4lynx/tamer-screen",
5178
5666
  "@tamer4lynx/tamer-router",
5179
5667
  "@tamer4lynx/tamer-insets",
5180
5668
  "@tamer4lynx/tamer-transports",
5181
- "@tamer4lynx/tamer-text-input",
5182
5669
  "@tamer4lynx/tamer-system-ui",
5183
5670
  "@tamer4lynx/tamer-icons"
5184
5671
  ];
5185
- var PACKAGE_ALIASES = {
5186
- input: "@tamer4lynx/tamer-text-input"
5187
- };
5672
+ var PACKAGE_ALIASES = {};
5188
5673
  function detectPackageManager(cwd) {
5189
- const dir = path23.resolve(cwd);
5190
- if (fs22.existsSync(path23.join(dir, "pnpm-lock.yaml"))) return "pnpm";
5191
- if (fs22.existsSync(path23.join(dir, "bun.lockb"))) return "bun";
5674
+ const dir = path24.resolve(cwd);
5675
+ if (fs23.existsSync(path24.join(dir, "pnpm-lock.yaml"))) return "pnpm";
5676
+ if (fs23.existsSync(path24.join(dir, "bun.lockb"))) return "bun";
5192
5677
  return "npm";
5193
5678
  }
5194
5679
  function runInstall(cwd, packages, pm) {
5195
5680
  const args = pm === "npm" ? ["install", ...packages] : ["add", ...packages];
5196
5681
  const cmd = pm === "npm" ? "npm" : pm === "pnpm" ? "pnpm" : "bun";
5197
- execSync9(`${cmd} ${args.join(" ")}`, { stdio: "inherit", cwd });
5682
+ execSync10(`${cmd} ${args.join(" ")}`, { stdio: "inherit", cwd });
5198
5683
  }
5199
5684
  function addCore() {
5200
5685
  const { lynxProjectDir } = resolveHostPaths();
@@ -5224,71 +5709,67 @@ function add(packages = []) {
5224
5709
  }
5225
5710
 
5226
5711
  // index.ts
5712
+ function readCliVersion() {
5713
+ const root = path25.dirname(fileURLToPath(import.meta.url));
5714
+ const here = path25.join(root, "package.json");
5715
+ const parent = path25.join(root, "..", "package.json");
5716
+ const pkgPath = fs24.existsSync(here) ? here : parent;
5717
+ return JSON.parse(fs24.readFileSync(pkgPath, "utf8")).version;
5718
+ }
5719
+ var version = readCliVersion();
5227
5720
  function validateDebugRelease(debug, release) {
5228
5721
  if (debug && release) {
5229
5722
  console.error("Cannot use --debug and --release together.");
5230
5723
  process.exit(1);
5231
5724
  }
5232
5725
  }
5726
+ function parsePlatform(value) {
5727
+ const p = value?.toLowerCase();
5728
+ if (p === "ios" || p === "android") return p;
5729
+ if (p === "all" || p === "both") return "all";
5730
+ return null;
5731
+ }
5233
5732
  program.version(version).description("Tamer4Lynx CLI - A tool for managing Lynx projects");
5234
5733
  program.command("init").description("Initialize tamer.config.json interactively").action(() => {
5235
5734
  init_default();
5236
5735
  });
5237
- var android = program.command("android").description("Android project commands");
5238
- android.command("create").option("-t, --target <target>", "Create target: host (default) or dev-app", "host").description("Create a new Android project").action(async (opts) => {
5239
- await create_default({ target: opts.target });
5240
- });
5241
- android.command("link").description("Link native modules to the Android project").action(() => {
5242
- autolink_default();
5243
- });
5244
- android.command("bundle").option("-d, --debug", "Debug bundle with dev client embedded (default)").option("-r, --release", "Release bundle without dev client").description("Build Lynx bundle and copy to Android assets (runs autolink first)").action(async (opts) => {
5245
- validateDebugRelease(opts.debug, opts.release);
5246
- const release = opts.release === true;
5247
- await bundle_default({ release });
5248
- });
5249
- var androidBuildCmd = android.command("build").option("-i, --install", "Install APK to connected device and launch app after building").option("-e, --embeddable", "Build for embedding in existing app (host only). Use with --release for production-ready embeddable.").option("-d, --debug", "Debug APK with dev client embedded (default)").option("-r, --release", "Release APK without dev client").description("Build APK (autolink + bundle + gradle)").action(async () => {
5250
- const opts = androidBuildCmd.opts();
5251
- validateDebugRelease(opts.debug, opts.release);
5252
- const release = opts.release === true;
5253
- if (opts.embeddable) {
5254
- await buildEmbeddable({ release: true });
5736
+ program.command("create <target>").description("Create a project or extension. Target: ios | android | module | element | service | combo").option("-d, --debug", "For android: create host project (default)").option("-r, --release", "For android: create dev-app project").action(async (target, opts) => {
5737
+ const t = target.toLowerCase();
5738
+ if (t === "ios") {
5739
+ create_default2();
5255
5740
  return;
5256
5741
  }
5257
- await build_default({ install: opts.install, release });
5258
- });
5259
- android.command("sync").description("Sync dev client files (TemplateProvider, MainActivity, DevClientManager) from tamer.config.json").action(async () => {
5260
- await syncDevClient_default();
5261
- });
5262
- android.command("inject").option("-f, --force", "Overwrite existing files").description("Inject tamer-host templates into an existing Android project").action(async (opts) => {
5263
- await injectHostAndroid({ force: opts.force });
5264
- });
5265
- var ios = program.command("ios").description("iOS project commands");
5266
- ios.command("create").description("Create a new iOS project").action(() => {
5267
- create_default2();
5268
- });
5269
- ios.command("inject").option("-f, --force", "Overwrite existing files").description("Inject tamer-host templates into an existing iOS project").action(async (opts) => {
5270
- await injectHostIos({ force: opts.force });
5271
- });
5272
- ios.command("link").description("Link native modules to the iOS project").action(() => {
5273
- autolink_default2();
5274
- });
5275
- ios.command("bundle").option("-d, --debug", "Debug bundle with dev client embedded (default)").option("-r, --release", "Release bundle without dev client").description("Build Lynx bundle and copy to iOS project (runs autolink first)").action((opts) => {
5276
- validateDebugRelease(opts.debug, opts.release);
5277
- const release = opts.release === true;
5278
- bundle_default2({ release });
5742
+ if (t === "android") {
5743
+ if (opts.debug && opts.release) {
5744
+ console.error("Cannot use --debug and --release together.");
5745
+ process.exit(1);
5746
+ }
5747
+ await create_default({ target: opts.release ? "dev-app" : "host" });
5748
+ return;
5749
+ }
5750
+ if (["module", "element", "service", "combo"].includes(t)) {
5751
+ await create_default3({ type: t });
5752
+ return;
5753
+ }
5754
+ console.error(`Invalid create target: ${target}. Use ios | android | module | element | service | combo`);
5755
+ process.exit(1);
5279
5756
  });
5280
- var iosBuildCmd = ios.command("build").option("-e, --embeddable", "Output bundle + code snippets to embeddable/ for adding LynxView to an existing app. Use with --release.").option("-i, --install", "Install and launch on booted simulator after building").option("-d, --debug", "Debug build with dev client embedded (default)").option("-r, --release", "Release build without dev client").description("Build iOS app (autolink + bundle + xcodebuild)").action(async () => {
5281
- const opts = iosBuildCmd.opts();
5757
+ program.command("build [platform]").description("Build app. Platform: ios | android (default: both)").option("-e, --embeddable", "Output embeddable bundle + code for existing apps. Use with --release.").option("-d, --debug", "Debug build with dev client embedded (default)").option("-r, --release", "Release build without dev client").option("-i, --install", "Install after building").action(async (platform, opts) => {
5282
5758
  validateDebugRelease(opts.debug, opts.release);
5283
5759
  const release = opts.release === true;
5284
5760
  if (opts.embeddable) {
5285
5761
  await buildEmbeddable({ release: true });
5286
5762
  return;
5287
5763
  }
5288
- await build_default2({ install: opts.install, release });
5764
+ const p = parsePlatform(platform ?? "all") ?? "all";
5765
+ if (p === "android" || p === "all") {
5766
+ await build_default({ install: opts.install, release });
5767
+ }
5768
+ if (p === "ios" || p === "all") {
5769
+ await build_default2({ install: opts.install, release });
5770
+ }
5289
5771
  });
5290
- var linkCmd = program.command("link").option("-i, --ios", "Link iOS native modules").option("-a, --android", "Link Android native modules").option("-b, --both", "Link both iOS and Android native modules").option("-s, --silent", "Run in silent mode without outputting messages").description("Link native modules to the project").action(() => {
5291
- const opts = linkCmd.opts();
5772
+ program.command("link [platform]").description("Link native modules. Platform: ios | android | both (default: both)").option("-s, --silent", "Run in silent mode (e.g. for postinstall)").action((platform, opts) => {
5292
5773
  if (opts.silent) {
5293
5774
  console.log = () => {
5294
5775
  };
@@ -5297,59 +5778,133 @@ var linkCmd = program.command("link").option("-i, --ios", "Link iOS native modul
5297
5778
  console.warn = () => {
5298
5779
  };
5299
5780
  }
5300
- if (opts.ios) {
5781
+ const p = parsePlatform(platform ?? "both") ?? "both";
5782
+ if (p === "ios") {
5301
5783
  autolink_default2();
5302
5784
  return;
5303
5785
  }
5304
- if (opts.android) {
5786
+ if (p === "android") {
5305
5787
  autolink_default();
5306
5788
  return;
5307
5789
  }
5308
5790
  autolink_default2();
5309
5791
  autolink_default();
5310
5792
  });
5311
- program.command("start").option("-v, --verbose", "Show all logs (native + JS); default shows JS only").description("Start dev server with HMR and WebSocket support (Expo-like)").action(async (opts) => {
5312
- await start_default({ verbose: opts.verbose });
5313
- });
5314
- var buildCmd = program.command("build").option("-p, --platform <platform>", "android, ios, or all (default: all)", "all").option("-e, --embeddable", "Output bundle + code snippets to embeddable/ for adding LynxView to an existing app. Use with --release.").option("-d, --debug", "Debug build with dev client embedded (default)").option("-r, --release", "Release build without dev client").option("-i, --install", "Install after building").description("Build app (unified: delegates to android/ios build)").action(async () => {
5315
- const opts = buildCmd.opts();
5793
+ program.command("bundle [platform]").description("Build Lynx bundle and copy to native project. Platform: ios | android (default: both)").option("-d, --debug", "Debug bundle with dev client embedded (default)").option("-r, --release", "Release bundle without dev client").action(async (platform, opts) => {
5316
5794
  validateDebugRelease(opts.debug, opts.release);
5317
5795
  const release = opts.release === true;
5318
- if (opts.embeddable) {
5319
- await buildEmbeddable({ release: true });
5796
+ const p = parsePlatform(platform ?? "both") ?? "both";
5797
+ if (p === "android" || p === "all") await bundle_default({ release });
5798
+ if (p === "ios" || p === "all") bundle_default2({ release });
5799
+ });
5800
+ program.command("inject <platform>").description("Inject tamer-host templates into an existing project. Platform: ios | android").option("-f, --force", "Overwrite existing files").action(async (platform, opts) => {
5801
+ const p = platform?.toLowerCase();
5802
+ if (p === "ios") {
5803
+ await injectHostIos({ force: opts.force });
5320
5804
  return;
5321
5805
  }
5322
- const p = opts.platform?.toLowerCase();
5323
- const platform = p === "ios" || p === "android" ? p : "all";
5324
- if (platform === "android" || platform === "all") {
5325
- await build_default({ install: opts.install, release });
5806
+ if (p === "android") {
5807
+ await injectHostAndroid({ force: opts.force });
5808
+ return;
5326
5809
  }
5327
- if (platform === "ios" || platform === "all") {
5328
- await build_default2({ install: opts.install, release });
5810
+ console.error(`Invalid inject platform: ${platform}. Use ios | android`);
5811
+ process.exit(1);
5812
+ });
5813
+ program.command("sync [platform]").description("Sync dev client files from tamer.config.json. Platform: android (default)").action(async (platform) => {
5814
+ const p = (platform ?? "android").toLowerCase();
5815
+ if (p !== "android") {
5816
+ console.error("sync only supports android.");
5817
+ process.exit(1);
5329
5818
  }
5819
+ await syncDevClient_default();
5820
+ });
5821
+ program.command("start").option("-v, --verbose", "Show all logs (native + JS); default shows JS only").description("Start dev server with HMR and WebSocket support (Expo-like)").action(async (opts) => {
5822
+ await start_default({ verbose: opts.verbose });
5330
5823
  });
5331
- program.command("build-dev-app").option("-p, --platform <platform>", "Platform: android, ios, or all (default)", "all").option("-i, --install", "Install APK to connected device and launch app after building").description("(Deprecated) Use: t4l build -p android -d --install").action(async (opts) => {
5332
- console.warn("\u26A0\uFE0F build-dev-app is deprecated. Use: t4l build -p android -d [--install]");
5333
- const p = opts.platform?.toLowerCase();
5334
- const platform = p === "ios" || p === "android" ? p : "all";
5335
- if (platform === "android" || platform === "all") {
5824
+ program.command("build-dev-app").option("-p, --platform <platform>", "Platform: android, ios, or all (default)", "all").option("-i, --install", "Install APK to connected device and launch app after building").description("(Deprecated) Use: t4l build android -d [--install]").action(async (opts) => {
5825
+ console.warn("\u26A0\uFE0F build-dev-app is deprecated. Use: t4l build android -d [--install]");
5826
+ const p = parsePlatform(opts.platform ?? "all") ?? "all";
5827
+ if (p === "android" || p === "all") {
5336
5828
  await build_default({ install: opts.install, release: false });
5337
5829
  }
5338
- if (platform === "ios" || platform === "all") {
5830
+ if (p === "ios" || p === "all") {
5339
5831
  await build_default2({ install: opts.install, release: false });
5340
5832
  }
5341
5833
  });
5342
5834
  program.command("add [packages...]").description("Add @tamer4lynx packages to the Lynx project. Future: will track versions for compatibility (Expo-style).").action((packages) => add(packages));
5343
- program.command("add-core").description("Add core packages (app-shell, screen, router, insets, transports, input/text-input, system-ui, icons)").action(() => addCore());
5344
- program.command("create").description("Create a new Lynx extension project (RFC-compliant)").action(() => create_default3());
5835
+ program.command("add-core").description("Add core packages (app-shell, screen, router, insets, transports, system-ui, icons)").action(() => addCore());
5345
5836
  program.command("codegen").description("Generate code from @lynxmodule declarations").action(() => {
5346
5837
  codegen_default();
5347
5838
  });
5839
+ program.command("android <subcommand>").description("(Legacy) Use: t4l <command> android. e.g. t4l create android").option("-d, --debug", "Create: host project. Bundle/build: debug with dev client.").option("-r, --release", "Create: dev-app project. Bundle/build: release without dev client.").option("-i, --install", "Install after build").option("-e, --embeddable", "Build embeddable").option("-f, --force", "Force (inject)").action(async (subcommand, opts) => {
5840
+ const sub = subcommand?.toLowerCase();
5841
+ if (sub === "create") {
5842
+ if (opts.debug && opts.release) {
5843
+ console.error("Cannot use --debug and --release together.");
5844
+ process.exit(1);
5845
+ }
5846
+ await create_default({ target: opts.release ? "dev-app" : "host" });
5847
+ return;
5848
+ }
5849
+ if (sub === "link") {
5850
+ autolink_default();
5851
+ return;
5852
+ }
5853
+ if (sub === "bundle") {
5854
+ validateDebugRelease(opts.debug, opts.release);
5855
+ await bundle_default({ release: opts.release === true });
5856
+ return;
5857
+ }
5858
+ if (sub === "build") {
5859
+ validateDebugRelease(opts.debug, opts.release);
5860
+ if (opts.embeddable) await buildEmbeddable({ release: true });
5861
+ else await build_default({ install: opts.install, release: opts.release === true });
5862
+ return;
5863
+ }
5864
+ if (sub === "sync") {
5865
+ await syncDevClient_default();
5866
+ return;
5867
+ }
5868
+ if (sub === "inject") {
5869
+ await injectHostAndroid({ force: opts.force });
5870
+ return;
5871
+ }
5872
+ console.error(`Unknown android subcommand: ${subcommand}. Use: create | link | bundle | build | sync | inject`);
5873
+ process.exit(1);
5874
+ });
5875
+ program.command("ios <subcommand>").description("(Legacy) Use: t4l <command> ios. e.g. t4l create ios").option("-d, --debug", "Debug (bundle/build)").option("-r, --release", "Release (bundle/build)").option("-i, --install", "Install after build").option("-e, --embeddable", "Build embeddable").option("-f, --force", "Force (inject)").action(async (subcommand, opts) => {
5876
+ const sub = subcommand?.toLowerCase();
5877
+ if (sub === "create") {
5878
+ create_default2();
5879
+ return;
5880
+ }
5881
+ if (sub === "link") {
5882
+ autolink_default2();
5883
+ return;
5884
+ }
5885
+ if (sub === "bundle") {
5886
+ validateDebugRelease(opts.debug, opts.release);
5887
+ bundle_default2({ release: opts.release === true });
5888
+ return;
5889
+ }
5890
+ if (sub === "build") {
5891
+ validateDebugRelease(opts.debug, opts.release);
5892
+ if (opts.embeddable) await buildEmbeddable({ release: true });
5893
+ else await build_default2({ install: opts.install, release: opts.release === true });
5894
+ return;
5895
+ }
5896
+ if (sub === "inject") {
5897
+ await injectHostIos({ force: opts.force });
5898
+ return;
5899
+ }
5900
+ console.error(`Unknown ios subcommand: ${subcommand}. Use: create | link | bundle | build | inject`);
5901
+ process.exit(1);
5902
+ });
5348
5903
  program.command("autolink-toggle").alias("autolink").description("Toggle autolink on/off in tamer.config.json (controls postinstall linking)").action(async () => {
5349
- const configPath = path24.join(process.cwd(), "tamer.config.json");
5904
+ const configPath = path25.join(process.cwd(), "tamer.config.json");
5350
5905
  let config = {};
5351
- if (fs23.existsSync(configPath)) {
5352
- config = JSON.parse(fs23.readFileSync(configPath, "utf8"));
5906
+ if (fs24.existsSync(configPath)) {
5907
+ config = JSON.parse(fs24.readFileSync(configPath, "utf8"));
5353
5908
  }
5354
5909
  if (config.autolink) {
5355
5910
  delete config.autolink;
@@ -5358,7 +5913,7 @@ program.command("autolink-toggle").alias("autolink").description("Toggle autolin
5358
5913
  config.autolink = true;
5359
5914
  console.log("Autolink enabled in tamer.config.json");
5360
5915
  }
5361
- fs23.writeFileSync(configPath, JSON.stringify(config, null, 2));
5916
+ fs24.writeFileSync(configPath, JSON.stringify(config, null, 2));
5362
5917
  console.log(`Updated ${configPath}`);
5363
5918
  });
5364
5919
  if (process.argv.length <= 2 || process.argv.length === 3 && process.argv[2] === "init") {