@tamer4lynx/cli 0.0.17 → 0.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +1041 -769
  2. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -7,41 +7,64 @@ process.on("warning", (w) => {
7
7
  });
8
8
 
9
9
  // index.ts
10
- import fs29 from "fs";
11
- import path30 from "path";
12
- import { fileURLToPath } from "url";
10
+ import fs31 from "fs";
11
+ import path32 from "path";
13
12
  import { program } from "commander";
14
13
 
14
+ // src/common/cliVersion.ts
15
+ import fs from "fs";
16
+ import path from "path";
17
+ import { fileURLToPath } from "url";
18
+ function getCliVersion() {
19
+ let dir = path.dirname(fileURLToPath(import.meta.url));
20
+ for (let i = 0; i < 12; i++) {
21
+ const pkgPath = path.join(dir, "package.json");
22
+ if (fs.existsSync(pkgPath)) {
23
+ try {
24
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
25
+ if (pkg.name === "@tamer4lynx/cli" && typeof pkg.version === "string") {
26
+ return pkg.version;
27
+ }
28
+ } catch {
29
+ }
30
+ }
31
+ const next = path.dirname(dir);
32
+ if (next === dir) break;
33
+ dir = next;
34
+ }
35
+ return "0.0.0";
36
+ }
37
+
15
38
  // src/android/create.ts
16
- import fs4 from "fs";
17
- import path4 from "path";
39
+ import fs5 from "fs";
40
+ import path5 from "path";
18
41
  import os2 from "os";
19
42
 
20
43
  // src/android/getGradle.ts
21
- import fs from "fs";
22
- import path from "path";
44
+ import fs2 from "fs";
45
+ import path2 from "path";
23
46
  import os from "os";
24
47
  import { execSync } from "child_process";
25
48
  import fetch2 from "node-fetch";
26
49
  import AdmZip from "adm-zip";
27
50
  async function downloadGradle(gradleVersion) {
28
51
  const gradleBaseUrl = `https://services.gradle.org/distributions/gradle-${gradleVersion}-bin.zip`;
29
- const downloadDir = path.join(process.cwd(), "gradle");
30
- const zipPath = path.join(downloadDir, `gradle-${gradleVersion}.zip`);
31
- const extractedPath = path.join(downloadDir, `gradle-${gradleVersion}`);
32
- if (fs.existsSync(extractedPath)) {
52
+ const downloadDir = path2.join(process.cwd(), "gradle");
53
+ const zipPath = path2.join(downloadDir, `gradle-${gradleVersion}.zip`);
54
+ const extractedPath = path2.join(downloadDir, `gradle-${gradleVersion}`);
55
+ if (fs2.existsSync(extractedPath)) {
33
56
  console.log(`\u2705 Gradle ${gradleVersion} already exists at ${extractedPath}. Skipping download.`);
34
57
  return;
35
58
  }
36
- if (!fs.existsSync(downloadDir)) {
37
- fs.mkdirSync(downloadDir, { recursive: true });
59
+ if (!fs2.existsSync(downloadDir)) {
60
+ fs2.mkdirSync(downloadDir, { recursive: true });
38
61
  }
39
62
  console.log(`\u{1F4E5} Downloading Gradle ${gradleVersion} from ${gradleBaseUrl}...`);
40
63
  const response = await fetch2(gradleBaseUrl);
41
64
  if (!response.ok || !response.body) {
42
65
  throw new Error(`Failed to download: ${response.statusText}`);
43
66
  }
44
- const fileStream = fs.createWriteStream(zipPath);
67
+ const fileStream = fs2.createWriteStream(zipPath);
45
68
  await new Promise((resolve, reject) => {
46
69
  response.body?.pipe(fileStream);
47
70
  response.body?.on("error", reject);
@@ -55,26 +78,26 @@ async function downloadGradle(gradleVersion) {
55
78
  console.error(`\u274C Failed to extract Gradle zip: ${err}`);
56
79
  throw err;
57
80
  }
58
- fs.unlinkSync(zipPath);
81
+ fs2.unlinkSync(zipPath);
59
82
  console.log(`\u2705 Gradle ${gradleVersion} extracted to ${extractedPath}`);
60
83
  }
61
84
  async function setupGradleWrapper(rootDir, gradleVersion) {
62
85
  try {
63
86
  console.log("\u{1F4E6} Setting up Gradle wrapper...");
64
87
  await downloadGradle(gradleVersion);
65
- const gradleBinDir = path.join(
88
+ const gradleBinDir = path2.join(
66
89
  process.cwd(),
67
90
  "gradle",
68
91
  `gradle-${gradleVersion}`,
69
92
  "bin"
70
93
  );
71
94
  const gradleExecutable = os.platform() === "win32" ? "gradle.bat" : "gradle";
72
- const gradleExecutablePath = path.join(gradleBinDir, gradleExecutable);
73
- if (!fs.existsSync(gradleExecutablePath)) {
95
+ const gradleExecutablePath = path2.join(gradleBinDir, gradleExecutable);
96
+ if (!fs2.existsSync(gradleExecutablePath)) {
74
97
  throw new Error(`Gradle executable not found at ${gradleExecutablePath}`);
75
98
  }
76
99
  if (os.platform() !== "win32") {
77
- fs.chmodSync(gradleExecutablePath, "755");
100
+ fs2.chmodSync(gradleExecutablePath, "755");
78
101
  }
79
102
  console.log(`\u{1F680} Executing Gradle wrapper in: ${rootDir}`);
80
103
  execSync(`"${gradleExecutablePath}" wrapper`, {
@@ -89,8 +112,8 @@ async function setupGradleWrapper(rootDir, gradleVersion) {
89
112
  }
90
113
 
91
114
  // src/common/hostConfig.ts
92
- import fs2 from "fs";
93
- import path2 from "path";
115
+ import fs3 from "fs";
116
+ import path3 from "path";
94
117
  var DEFAULT_ABI_FILTERS = ["armeabi-v7a", "arm64-v8a"];
95
118
  var TAMER_CONFIG = "tamer.config.json";
96
119
  var LYNX_CONFIG_FILES = ["lynx.config.ts", "lynx.config.js", "lynx.config.mjs"];
@@ -99,27 +122,27 @@ var DEFAULT_IOS_DIR = "ios";
99
122
  var DEFAULT_BUNDLE_FILE = "main.lynx.bundle";
100
123
  var DEFAULT_BUNDLE_ROOT = "dist";
101
124
  function findProjectRoot(start2) {
102
- let dir = path2.resolve(start2);
103
- const root = path2.parse(dir).root;
125
+ let dir = path3.resolve(start2);
126
+ const root = path3.parse(dir).root;
104
127
  while (dir !== root) {
105
- const p = path2.join(dir, TAMER_CONFIG);
106
- if (fs2.existsSync(p)) return dir;
107
- dir = path2.dirname(dir);
128
+ const p = path3.join(dir, TAMER_CONFIG);
129
+ if (fs3.existsSync(p)) return dir;
130
+ dir = path3.dirname(dir);
108
131
  }
109
132
  return start2;
110
133
  }
111
134
  function loadTamerConfig(cwd) {
112
- const p = path2.join(cwd, TAMER_CONFIG);
113
- if (!fs2.existsSync(p)) return null;
135
+ const p = path3.join(cwd, TAMER_CONFIG);
136
+ if (!fs3.existsSync(p)) return null;
114
137
  try {
115
- return JSON.parse(fs2.readFileSync(p, "utf8"));
138
+ return JSON.parse(fs3.readFileSync(p, "utf8"));
116
139
  } catch {
117
140
  return null;
118
141
  }
119
142
  }
120
143
  function extractDistPathRoot(configPath) {
121
144
  try {
122
- const content = fs2.readFileSync(configPath, "utf8");
145
+ const content = fs3.readFileSync(configPath, "utf8");
123
146
  const rootMatch = content.match(/distPath\s*:\s*\{\s*root\s*:\s*['"]([^'"]+)['"]/);
124
147
  if (rootMatch?.[1]) return rootMatch[1];
125
148
  const rootMatch2 = content.match(/root\s*:\s*['"]([^'"]+)['"]/);
@@ -130,16 +153,16 @@ function extractDistPathRoot(configPath) {
130
153
  }
131
154
  function findLynxConfigInDir(dir) {
132
155
  for (const name of LYNX_CONFIG_FILES) {
133
- const p = path2.join(dir, name);
134
- if (fs2.existsSync(p)) return p;
156
+ const p = path3.join(dir, name);
157
+ if (fs3.existsSync(p)) return p;
135
158
  }
136
159
  return null;
137
160
  }
138
161
  function hasRspeedy(pkgDir) {
139
- const pkgJsonPath = path2.join(pkgDir, "package.json");
140
- if (!fs2.existsSync(pkgJsonPath)) return false;
162
+ const pkgJsonPath = path3.join(pkgDir, "package.json");
163
+ if (!fs3.existsSync(pkgJsonPath)) return false;
141
164
  try {
142
- const pkg = JSON.parse(fs2.readFileSync(pkgJsonPath, "utf8"));
165
+ const pkg = JSON.parse(fs3.readFileSync(pkgJsonPath, "utf8"));
143
166
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
144
167
  return Object.keys(deps).some((k) => k === "@lynx-js/rspeedy");
145
168
  } catch {
@@ -148,17 +171,17 @@ function hasRspeedy(pkgDir) {
148
171
  }
149
172
  function discoverLynxProject(cwd, explicitPath) {
150
173
  if (explicitPath) {
151
- const resolved = path2.isAbsolute(explicitPath) ? explicitPath : path2.join(cwd, explicitPath);
152
- if (fs2.existsSync(resolved)) {
174
+ const resolved = path3.isAbsolute(explicitPath) ? explicitPath : path3.join(cwd, explicitPath);
175
+ if (fs3.existsSync(resolved)) {
153
176
  const lynxConfig2 = findLynxConfigInDir(resolved);
154
177
  const bundleRoot = lynxConfig2 ? extractDistPathRoot(lynxConfig2) ?? DEFAULT_BUNDLE_ROOT : DEFAULT_BUNDLE_ROOT;
155
178
  return { dir: resolved, bundleRoot };
156
179
  }
157
180
  }
158
- const rootPkgPath = path2.join(cwd, "package.json");
159
- if (fs2.existsSync(rootPkgPath)) {
181
+ const rootPkgPath = path3.join(cwd, "package.json");
182
+ if (fs3.existsSync(rootPkgPath)) {
160
183
  try {
161
- const rootPkg = JSON.parse(fs2.readFileSync(rootPkgPath, "utf8"));
184
+ const rootPkg = JSON.parse(fs3.readFileSync(rootPkgPath, "utf8"));
162
185
  const lynxConfig2 = findLynxConfigInDir(cwd);
163
186
  if (lynxConfig2 || (rootPkg.dependencies?.["@lynx-js/rspeedy"] || rootPkg.devDependencies?.["@lynx-js/rspeedy"])) {
164
187
  const bundleRoot = lynxConfig2 ? extractDistPathRoot(lynxConfig2) ?? DEFAULT_BUNDLE_ROOT : DEFAULT_BUNDLE_ROOT;
@@ -169,12 +192,12 @@ function discoverLynxProject(cwd, explicitPath) {
169
192
  for (const ws of workspaces) {
170
193
  const isGlob = typeof ws === "string" && ws.includes("*");
171
194
  const dirsToCheck = isGlob ? (() => {
172
- const parentDir = path2.join(cwd, ws.replace(/\/\*$/, ""));
173
- if (!fs2.existsSync(parentDir)) return [];
174
- return fs2.readdirSync(parentDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => path2.join(parentDir, e.name));
175
- })() : [path2.join(cwd, ws)];
195
+ const parentDir = path3.join(cwd, ws.replace(/\/\*$/, ""));
196
+ if (!fs3.existsSync(parentDir)) return [];
197
+ return fs3.readdirSync(parentDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => path3.join(parentDir, e.name));
198
+ })() : [path3.join(cwd, ws)];
176
199
  for (const pkgDir of dirsToCheck) {
177
- if (!fs2.existsSync(pkgDir)) continue;
200
+ if (!fs3.existsSync(pkgDir)) continue;
178
201
  const lynxConfig3 = findLynxConfigInDir(pkgDir);
179
202
  if (lynxConfig3 || hasRspeedy(pkgDir)) {
180
203
  const bundleRoot = lynxConfig3 ? extractDistPathRoot(lynxConfig3) ?? DEFAULT_BUNDLE_ROOT : DEFAULT_BUNDLE_ROOT;
@@ -194,30 +217,30 @@ function discoverLynxProject(cwd, explicitPath) {
194
217
  return null;
195
218
  }
196
219
  function findRepoRoot(start2) {
197
- let dir = path2.resolve(start2);
198
- const root = path2.parse(dir).root;
220
+ let dir = path3.resolve(start2);
221
+ const root = path3.parse(dir).root;
199
222
  while (dir !== root) {
200
- const pkgPath = path2.join(dir, "package.json");
201
- if (fs2.existsSync(pkgPath)) {
223
+ const pkgPath = path3.join(dir, "package.json");
224
+ if (fs3.existsSync(pkgPath)) {
202
225
  try {
203
- const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf8"));
226
+ const pkg = JSON.parse(fs3.readFileSync(pkgPath, "utf8"));
204
227
  if (pkg.workspaces) return dir;
205
228
  } catch {
206
229
  }
207
230
  }
208
- dir = path2.dirname(dir);
231
+ dir = path3.dirname(dir);
209
232
  }
210
233
  return start2;
211
234
  }
212
235
  function findDevAppPackage(projectRoot) {
213
236
  const candidates = [
214
- path2.join(projectRoot, "node_modules", "@tamer4lynx", "tamer-dev-app"),
215
- path2.join(projectRoot, "node_modules", "tamer-dev-app"),
216
- path2.join(projectRoot, "packages", "tamer-dev-app"),
217
- path2.join(path2.dirname(projectRoot), "tamer-dev-app")
237
+ path3.join(projectRoot, "node_modules", "@tamer4lynx", "tamer-dev-app"),
238
+ path3.join(projectRoot, "node_modules", "tamer-dev-app"),
239
+ path3.join(projectRoot, "packages", "tamer-dev-app"),
240
+ path3.join(path3.dirname(projectRoot), "tamer-dev-app")
218
241
  ];
219
242
  for (const pkg of candidates) {
220
- if (fs2.existsSync(pkg) && fs2.existsSync(path2.join(pkg, "package.json"))) {
243
+ if (fs3.existsSync(pkg) && fs3.existsSync(path3.join(pkg, "package.json"))) {
221
244
  return pkg;
222
245
  }
223
246
  }
@@ -225,13 +248,13 @@ function findDevAppPackage(projectRoot) {
225
248
  }
226
249
  function findDevClientPackage(projectRoot) {
227
250
  const candidates = [
228
- path2.join(projectRoot, "node_modules", "@tamer4lynx", "tamer-dev-client"),
229
- path2.join(projectRoot, "node_modules", "tamer-dev-client"),
230
- path2.join(projectRoot, "packages", "tamer-dev-client"),
231
- path2.join(path2.dirname(projectRoot), "tamer-dev-client")
251
+ path3.join(projectRoot, "node_modules", "@tamer4lynx", "tamer-dev-client"),
252
+ path3.join(projectRoot, "node_modules", "tamer-dev-client"),
253
+ path3.join(projectRoot, "packages", "tamer-dev-client"),
254
+ path3.join(path3.dirname(projectRoot), "tamer-dev-client")
232
255
  ];
233
256
  for (const pkg of candidates) {
234
- if (fs2.existsSync(pkg) && fs2.existsSync(path2.join(pkg, "package.json"))) {
257
+ if (fs3.existsSync(pkg) && fs3.existsSync(path3.join(pkg, "package.json"))) {
235
258
  return pkg;
236
259
  }
237
260
  }
@@ -239,11 +262,11 @@ function findDevClientPackage(projectRoot) {
239
262
  }
240
263
  function findTamerHostPackage(projectRoot) {
241
264
  const candidates = [
242
- path2.join(projectRoot, "node_modules", "tamer-host"),
243
- path2.join(projectRoot, "packages", "tamer-host")
265
+ path3.join(projectRoot, "node_modules", "tamer-host"),
266
+ path3.join(projectRoot, "packages", "tamer-host")
244
267
  ];
245
268
  for (const pkg of candidates) {
246
- if (fs2.existsSync(pkg) && fs2.existsSync(path2.join(pkg, "package.json"))) {
269
+ if (fs3.existsSync(pkg) && fs3.existsSync(path3.join(pkg, "package.json"))) {
247
270
  return pkg;
248
271
  }
249
272
  }
@@ -261,18 +284,18 @@ function resolveHostPaths(cwd = process.cwd()) {
261
284
  const lynxProjectDir = discovered?.dir ?? cwd;
262
285
  const bundleRoot = paths.lynxBundleRoot ?? discovered?.bundleRoot ?? DEFAULT_BUNDLE_ROOT;
263
286
  const bundleFile = paths.lynxBundleFile ?? DEFAULT_BUNDLE_FILE;
264
- const lynxBundlePath = path2.join(lynxProjectDir, bundleRoot, bundleFile);
265
- const androidDir = path2.join(projectRoot, androidDirRel);
287
+ const lynxBundlePath = path3.join(lynxProjectDir, bundleRoot, bundleFile);
288
+ const androidDir = path3.join(projectRoot, androidDirRel);
266
289
  const devMode = resolveDevMode(config);
267
290
  const devClientPkg = findDevClientPackage(projectRoot);
268
- const devClientBundlePath = devClientPkg ? path2.join(devClientPkg, DEFAULT_BUNDLE_ROOT, "dev-client.lynx.bundle") : void 0;
291
+ const devClientBundlePath = devClientPkg ? path3.join(devClientPkg, DEFAULT_BUNDLE_ROOT, "dev-client.lynx.bundle") : void 0;
269
292
  return {
270
293
  projectRoot,
271
294
  androidDir,
272
- iosDir: path2.join(projectRoot, iosDirRel),
273
- androidAppDir: path2.join(projectRoot, androidDirRel, "app"),
274
- androidAssetsDir: path2.join(projectRoot, androidDirRel, "app", "src", "main", "assets"),
275
- androidKotlinDir: path2.join(projectRoot, androidDirRel, "app", "src", "main", "kotlin", packageName.replace(/\./g, "/")),
295
+ iosDir: path3.join(projectRoot, iosDirRel),
296
+ androidAppDir: path3.join(projectRoot, androidDirRel, "app"),
297
+ androidAssetsDir: path3.join(projectRoot, androidDirRel, "app", "src", "main", "assets"),
298
+ androidKotlinDir: path3.join(projectRoot, androidDirRel, "app", "src", "main", "kotlin", packageName.replace(/\./g, "/")),
276
299
  lynxProjectDir,
277
300
  lynxBundlePath,
278
301
  lynxBundleFile: bundleFile,
@@ -347,32 +370,32 @@ function normalizeAndroidAdaptiveColor(input) {
347
370
  function resolveIconPaths(projectRoot, config) {
348
371
  const raw = config.icon;
349
372
  if (!raw) return null;
350
- const join = (p) => path2.isAbsolute(p) ? p : path2.join(projectRoot, p);
373
+ const join = (p) => path3.isAbsolute(p) ? p : path3.join(projectRoot, p);
351
374
  if (typeof raw === "string") {
352
375
  const p = join(raw);
353
- return fs2.existsSync(p) ? { source: p } : null;
376
+ return fs3.existsSync(p) ? { source: p } : null;
354
377
  }
355
378
  const out = {};
356
379
  if (raw.source) {
357
380
  const p = join(raw.source);
358
- if (fs2.existsSync(p)) out.source = p;
381
+ if (fs3.existsSync(p)) out.source = p;
359
382
  }
360
383
  if (raw.android) {
361
384
  const p = join(raw.android);
362
- if (fs2.existsSync(p)) out.android = p;
385
+ if (fs3.existsSync(p)) out.android = p;
363
386
  }
364
387
  if (raw.ios) {
365
388
  const p = join(raw.ios);
366
- if (fs2.existsSync(p)) out.ios = p;
389
+ if (fs3.existsSync(p)) out.ios = p;
367
390
  }
368
391
  const ad = raw.androidAdaptive;
369
392
  if (ad?.foreground) {
370
393
  const fg = join(ad.foreground);
371
- if (fs2.existsSync(fg)) {
394
+ if (fs3.existsSync(fg)) {
372
395
  let usedAdaptive = false;
373
396
  if (ad.background) {
374
397
  const bg = join(ad.background);
375
- if (fs2.existsSync(bg)) {
398
+ if (fs3.existsSync(bg)) {
376
399
  out.androidAdaptiveForeground = fg;
377
400
  out.androidAdaptiveBackground = bg;
378
401
  usedAdaptive = true;
@@ -395,19 +418,19 @@ function resolveIconPaths(projectRoot, config) {
395
418
  }
396
419
 
397
420
  // src/common/syncAppIcons.ts
398
- import fs3 from "fs";
399
- import path3 from "path";
421
+ import fs4 from "fs";
422
+ import path4 from "path";
400
423
  function purgeAdaptiveForegroundArtifacts(drawableDir) {
401
424
  for (const base of ["ic_launcher_fg_src", "ic_launcher_fg_bm", "ic_launcher_fg_sc"]) {
402
425
  for (const ext of [".png", ".webp", ".jpg", ".jpeg", ".xml"]) {
403
426
  try {
404
- fs3.unlinkSync(path3.join(drawableDir, base + ext));
427
+ fs4.unlinkSync(path4.join(drawableDir, base + ext));
405
428
  } catch {
406
429
  }
407
430
  }
408
431
  }
409
432
  try {
410
- fs3.unlinkSync(path3.join(drawableDir, "ic_launcher_foreground.xml"));
433
+ fs4.unlinkSync(path4.join(drawableDir, "ic_launcher_foreground.xml"));
411
434
  } catch {
412
435
  }
413
436
  }
@@ -420,15 +443,15 @@ function writeAdaptiveForegroundLayer(drawableDir, fgSourcePath, fgExt, layout)
420
443
  purgeAdaptiveForegroundArtifacts(drawableDir);
421
444
  for (const ext of [".png", ".webp", ".jpg", ".jpeg"]) {
422
445
  try {
423
- fs3.unlinkSync(path3.join(drawableDir, `ic_launcher_foreground${ext}`));
446
+ fs4.unlinkSync(path4.join(drawableDir, `ic_launcher_foreground${ext}`));
424
447
  } catch {
425
448
  }
426
449
  }
427
450
  if (!layout) {
428
- fs3.copyFileSync(fgSourcePath, path3.join(drawableDir, `ic_launcher_foreground${fgExt}`));
451
+ fs4.copyFileSync(fgSourcePath, path4.join(drawableDir, `ic_launcher_foreground${fgExt}`));
429
452
  return;
430
453
  }
431
- fs3.copyFileSync(fgSourcePath, path3.join(drawableDir, `ic_launcher_fg_src${fgExt}`));
454
+ fs4.copyFileSync(fgSourcePath, path4.join(drawableDir, `ic_launcher_fg_src${fgExt}`));
432
455
  const CANVAS_DP = 108;
433
456
  const scale = layout.scale ?? 1;
434
457
  const shrinkDp = (1 - scale) / 2 * CANVAS_DP;
@@ -452,11 +475,11 @@ function writeAdaptiveForegroundLayer(drawableDir, fgSourcePath, fgExt, layout)
452
475
  android:bottom="${insetBottom}dp"
453
476
  android:drawable="@drawable/ic_launcher_fg_src" />
454
477
  </layer-list>`;
455
- fs3.writeFileSync(path3.join(drawableDir, "ic_launcher_foreground.xml"), layerXml);
478
+ fs4.writeFileSync(path4.join(drawableDir, "ic_launcher_foreground.xml"), layerXml);
456
479
  }
457
480
  function ensureAndroidManifestLauncherIcon(manifestPath) {
458
- if (!fs3.existsSync(manifestPath)) return;
459
- let content = fs3.readFileSync(manifestPath, "utf8");
481
+ if (!fs4.existsSync(manifestPath)) return;
482
+ let content = fs4.readFileSync(manifestPath, "utf8");
460
483
  if (content.includes("android:icon=")) return;
461
484
  const next = content.replace(/<application(\s[^>]*)>/, (full, attrs) => {
462
485
  if (String(attrs).includes("android:icon")) return full;
@@ -465,30 +488,30 @@ function ensureAndroidManifestLauncherIcon(manifestPath) {
465
488
  android:roundIcon="@mipmap/ic_launcher">`;
466
489
  });
467
490
  if (next !== content) {
468
- fs3.writeFileSync(manifestPath, next, "utf8");
491
+ fs4.writeFileSync(manifestPath, next, "utf8");
469
492
  console.log("\u2705 Added android:icon / roundIcon to AndroidManifest.xml");
470
493
  }
471
494
  }
472
495
  function applyAndroidLauncherIcons(resDir, iconPaths) {
473
496
  if (!iconPaths) return false;
474
- fs3.mkdirSync(resDir, { recursive: true });
497
+ fs4.mkdirSync(resDir, { recursive: true });
475
498
  const fgAd = iconPaths.androidAdaptiveForeground;
476
499
  const bgAd = iconPaths.androidAdaptiveBackground;
477
500
  const bgColor = iconPaths.androidAdaptiveBackgroundColor;
478
501
  if (fgAd && (bgAd || bgColor)) {
479
- const drawableDir = path3.join(resDir, "drawable");
480
- fs3.mkdirSync(drawableDir, { recursive: true });
481
- const fgExt = path3.extname(fgAd) || ".png";
502
+ const drawableDir = path4.join(resDir, "drawable");
503
+ fs4.mkdirSync(drawableDir, { recursive: true });
504
+ const fgExt = path4.extname(fgAd) || ".png";
482
505
  writeAdaptiveForegroundLayer(drawableDir, fgAd, fgExt, iconPaths.androidAdaptiveForegroundLayout);
483
506
  if (bgColor) {
484
507
  for (const ext of [".png", ".webp", ".jpg", ".jpeg"]) {
485
508
  try {
486
- fs3.unlinkSync(path3.join(drawableDir, `ic_launcher_background${ext}`));
509
+ fs4.unlinkSync(path4.join(drawableDir, `ic_launcher_background${ext}`));
487
510
  } catch {
488
511
  }
489
512
  }
490
513
  try {
491
- fs3.unlinkSync(path3.join(drawableDir, "ic_launcher_background.xml"));
514
+ fs4.unlinkSync(path4.join(drawableDir, "ic_launcher_background.xml"));
492
515
  } catch {
493
516
  }
494
517
  const shapeXml = `<?xml version="1.0" encoding="utf-8"?>
@@ -496,44 +519,44 @@ function applyAndroidLauncherIcons(resDir, iconPaths) {
496
519
  <solid android:color="${bgColor}" />
497
520
  </shape>
498
521
  `;
499
- fs3.writeFileSync(path3.join(drawableDir, "ic_launcher_background.xml"), shapeXml);
522
+ fs4.writeFileSync(path4.join(drawableDir, "ic_launcher_background.xml"), shapeXml);
500
523
  } else {
501
524
  try {
502
- fs3.unlinkSync(path3.join(drawableDir, "ic_launcher_background.xml"));
525
+ fs4.unlinkSync(path4.join(drawableDir, "ic_launcher_background.xml"));
503
526
  } catch {
504
527
  }
505
- const bgExt = path3.extname(bgAd) || ".png";
506
- fs3.copyFileSync(bgAd, path3.join(drawableDir, `ic_launcher_background${bgExt}`));
528
+ const bgExt = path4.extname(bgAd) || ".png";
529
+ fs4.copyFileSync(bgAd, path4.join(drawableDir, `ic_launcher_background${bgExt}`));
507
530
  }
508
- const anyDpi = path3.join(resDir, "mipmap-anydpi-v26");
509
- fs3.mkdirSync(anyDpi, { recursive: true });
531
+ const anyDpi = path4.join(resDir, "mipmap-anydpi-v26");
532
+ fs4.mkdirSync(anyDpi, { recursive: true });
510
533
  const adaptiveXml = `<?xml version="1.0" encoding="utf-8"?>
511
534
  <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
512
535
  <background android:drawable="@drawable/ic_launcher_background" />
513
536
  <foreground android:drawable="@drawable/ic_launcher_foreground" />
514
537
  </adaptive-icon>
515
538
  `;
516
- fs3.writeFileSync(path3.join(anyDpi, "ic_launcher.xml"), adaptiveXml);
517
- fs3.writeFileSync(path3.join(anyDpi, "ic_launcher_round.xml"), adaptiveXml);
539
+ fs4.writeFileSync(path4.join(anyDpi, "ic_launcher.xml"), adaptiveXml);
540
+ fs4.writeFileSync(path4.join(anyDpi, "ic_launcher_round.xml"), adaptiveXml);
518
541
  const legacySrc = iconPaths.source ?? fgAd;
519
- const legacyExt = path3.extname(legacySrc) || ".png";
542
+ const legacyExt = path4.extname(legacySrc) || ".png";
520
543
  const mipmapDensities = ["mdpi", "hdpi", "xhdpi", "xxhdpi", "xxxhdpi"];
521
544
  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}`));
545
+ const dir = path4.join(resDir, `mipmap-${d}`);
546
+ fs4.mkdirSync(dir, { recursive: true });
547
+ fs4.copyFileSync(legacySrc, path4.join(dir, `ic_launcher${legacyExt}`));
525
548
  }
526
549
  return true;
527
550
  }
528
551
  if (iconPaths.android) {
529
552
  const src = iconPaths.android;
530
- const entries = fs3.readdirSync(src, { withFileTypes: true });
553
+ const entries = fs4.readdirSync(src, { withFileTypes: true });
531
554
  for (const e of entries) {
532
- const dest = path3.join(resDir, e.name);
555
+ const dest = path4.join(resDir, e.name);
533
556
  if (e.isDirectory()) {
534
- fs3.cpSync(path3.join(src, e.name), dest, { recursive: true });
557
+ fs4.cpSync(path4.join(src, e.name), dest, { recursive: true });
535
558
  } else {
536
- fs3.copyFileSync(path3.join(src, e.name), dest);
559
+ fs4.copyFileSync(path4.join(src, e.name), dest);
537
560
  }
538
561
  }
539
562
  return true;
@@ -541,9 +564,9 @@ function applyAndroidLauncherIcons(resDir, iconPaths) {
541
564
  if (iconPaths.source) {
542
565
  const mipmapDensities = ["mdpi", "hdpi", "xhdpi", "xxhdpi", "xxxhdpi"];
543
566
  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"));
567
+ const dir = path4.join(resDir, `mipmap-${d}`);
568
+ fs4.mkdirSync(dir, { recursive: true });
569
+ fs4.copyFileSync(iconPaths.source, path4.join(dir, "ic_launcher.png"));
547
570
  }
548
571
  return true;
549
572
  }
@@ -551,25 +574,25 @@ function applyAndroidLauncherIcons(resDir, iconPaths) {
551
574
  }
552
575
  function applyIosAppIconAssets(appIconDir, iconPaths) {
553
576
  if (!iconPaths) return false;
554
- fs3.mkdirSync(appIconDir, { recursive: true });
577
+ fs4.mkdirSync(appIconDir, { recursive: true });
555
578
  if (iconPaths.ios) {
556
- const entries = fs3.readdirSync(iconPaths.ios, { withFileTypes: true });
579
+ const entries = fs4.readdirSync(iconPaths.ios, { withFileTypes: true });
557
580
  for (const e of entries) {
558
- const dest = path3.join(appIconDir, e.name);
581
+ const dest = path4.join(appIconDir, e.name);
559
582
  if (e.isDirectory()) {
560
- fs3.cpSync(path3.join(iconPaths.ios, e.name), dest, { recursive: true });
583
+ fs4.cpSync(path4.join(iconPaths.ios, e.name), dest, { recursive: true });
561
584
  } else {
562
- fs3.copyFileSync(path3.join(iconPaths.ios, e.name), dest);
585
+ fs4.copyFileSync(path4.join(iconPaths.ios, e.name), dest);
563
586
  }
564
587
  }
565
588
  return true;
566
589
  }
567
590
  if (iconPaths.source) {
568
- const ext = path3.extname(iconPaths.source) || ".png";
591
+ const ext = path4.extname(iconPaths.source) || ".png";
569
592
  const icon1024 = `Icon-1024${ext}`;
570
- fs3.copyFileSync(iconPaths.source, path3.join(appIconDir, icon1024));
571
- fs3.writeFileSync(
572
- path3.join(appIconDir, "Contents.json"),
593
+ fs4.copyFileSync(iconPaths.source, path4.join(appIconDir, icon1024));
594
+ fs4.writeFileSync(
595
+ path4.join(appIconDir, "Contents.json"),
573
596
  JSON.stringify(
574
597
  {
575
598
  images: [{ filename: icon1024, idiom: "universal", platform: "ios", size: "1024x1024" }],
@@ -1121,7 +1144,7 @@ object DevServerPrefs {
1121
1144
 
1122
1145
  // src/android/create.ts
1123
1146
  function readAndSubstituteTemplate(templatePath, vars) {
1124
- const raw = fs4.readFileSync(templatePath, "utf-8");
1147
+ const raw = fs5.readFileSync(templatePath, "utf-8");
1125
1148
  return Object.entries(vars).reduce(
1126
1149
  (s, [k, v]) => s.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), v),
1127
1150
  raw
@@ -1132,7 +1155,7 @@ var create = async (opts = {}) => {
1132
1155
  const origCwd = process.cwd();
1133
1156
  if (target === "dev-app") {
1134
1157
  const devAppDir = findDevAppPackage(origCwd) ?? findDevAppPackage(findRepoRoot(origCwd));
1135
- if (!devAppDir || !fs4.existsSync(path4.join(devAppDir, "tamer.config.json"))) {
1158
+ if (!devAppDir || !fs5.existsSync(path5.join(devAppDir, "tamer.config.json"))) {
1136
1159
  console.error("\u274C tamer-dev-app not found. Add @tamer4lynx/tamer-dev-app to dependencies.");
1137
1160
  process.exit(1);
1138
1161
  }
@@ -1162,30 +1185,30 @@ var create = async (opts = {}) => {
1162
1185
  const packagePath = packageName.replace(/\./g, "/");
1163
1186
  const gradleVersion = "8.14.2";
1164
1187
  const androidDir = config.paths?.androidDir ?? "android";
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");
1188
+ const rootDir = path5.join(process.cwd(), androidDir);
1189
+ const appDir = path5.join(rootDir, "app");
1190
+ const mainDir = path5.join(appDir, "src", "main");
1191
+ const javaDir = path5.join(mainDir, "java", packagePath);
1192
+ const kotlinDir = path5.join(mainDir, "kotlin", packagePath);
1193
+ const kotlinGeneratedDir = path5.join(kotlinDir, "generated");
1194
+ const assetsDir = path5.join(mainDir, "assets");
1195
+ const themesDir = path5.join(mainDir, "res", "values");
1196
+ const gradleDir = path5.join(rootDir, "gradle");
1174
1197
  function writeFile2(filePath, content, options) {
1175
- fs4.mkdirSync(path4.dirname(filePath), { recursive: true });
1176
- fs4.writeFileSync(
1198
+ fs5.mkdirSync(path5.dirname(filePath), { recursive: true });
1199
+ fs5.writeFileSync(
1177
1200
  filePath,
1178
1201
  content.trimStart(),
1179
1202
  options?.encoding ?? "utf8"
1180
1203
  );
1181
1204
  }
1182
- if (fs4.existsSync(rootDir)) {
1205
+ if (fs5.existsSync(rootDir)) {
1183
1206
  console.log(`\u{1F9F9} Removing existing directory: ${rootDir}`);
1184
- fs4.rmSync(rootDir, { recursive: true, force: true });
1207
+ fs5.rmSync(rootDir, { recursive: true, force: true });
1185
1208
  }
1186
1209
  console.log(`\u{1F680} Creating a new Tamer4Lynx project in: ${rootDir}`);
1187
1210
  writeFile2(
1188
- path4.join(gradleDir, "libs.versions.toml"),
1211
+ path5.join(gradleDir, "libs.versions.toml"),
1189
1212
  `
1190
1213
  [versions]
1191
1214
  agp = "8.9.1"
@@ -1246,7 +1269,7 @@ kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
1246
1269
  `
1247
1270
  );
1248
1271
  writeFile2(
1249
- path4.join(rootDir, "settings.gradle.kts"),
1272
+ path5.join(rootDir, "settings.gradle.kts"),
1250
1273
  `
1251
1274
  pluginManagement {
1252
1275
  repositories {
@@ -1280,7 +1303,7 @@ println("If you have native modules please run tamer android link")
1280
1303
  `
1281
1304
  );
1282
1305
  writeFile2(
1283
- path4.join(rootDir, "build.gradle.kts"),
1306
+ path5.join(rootDir, "build.gradle.kts"),
1284
1307
  `
1285
1308
  // Top-level build file where you can add configuration options common to all sub-projects/modules.
1286
1309
  plugins {
@@ -1292,7 +1315,7 @@ plugins {
1292
1315
  `
1293
1316
  );
1294
1317
  writeFile2(
1295
- path4.join(rootDir, "gradle.properties"),
1318
+ path5.join(rootDir, "gradle.properties"),
1296
1319
  `
1297
1320
  org.gradle.jvmargs=-Xmx2048m
1298
1321
  android.useAndroidX=true
@@ -1301,7 +1324,7 @@ android.enableJetifier=true
1301
1324
  `
1302
1325
  );
1303
1326
  writeFile2(
1304
- path4.join(appDir, "build.gradle.kts"),
1327
+ path5.join(appDir, "build.gradle.kts"),
1305
1328
  `
1306
1329
  plugins {
1307
1330
  alias(libs.plugins.android.application)
@@ -1393,7 +1416,7 @@ dependencies {
1393
1416
  `
1394
1417
  );
1395
1418
  writeFile2(
1396
- path4.join(themesDir, "themes.xml"),
1419
+ path5.join(themesDir, "themes.xml"),
1397
1420
  `
1398
1421
  <resources>
1399
1422
  <style name="Theme.MyApp" parent="Theme.AppCompat.Light.NoActionBar">
@@ -1427,7 +1450,7 @@ dependencies {
1427
1450
  const iconPaths = resolveIconPaths(process.cwd(), config);
1428
1451
  const manifestIconAttrs = iconPaths ? ' android:icon="@mipmap/ic_launcher"\n android:roundIcon="@mipmap/ic_launcher"\n' : "";
1429
1452
  writeFile2(
1430
- path4.join(mainDir, "AndroidManifest.xml"),
1453
+ path5.join(mainDir, "AndroidManifest.xml"),
1431
1454
  `
1432
1455
  <manifest xmlns:android="http://schemas.android.com/apk/res/android">
1433
1456
  ${manifestPermissions}
@@ -1441,7 +1464,7 @@ ${manifestIconAttrs} android:usesCleartextTraffic="true"
1441
1464
  `
1442
1465
  );
1443
1466
  writeFile2(
1444
- path4.join(kotlinGeneratedDir, "GeneratedLynxExtensions.kt"),
1467
+ path5.join(kotlinGeneratedDir, "GeneratedLynxExtensions.kt"),
1445
1468
  `
1446
1469
  package ${packageName}.generated
1447
1470
 
@@ -1469,14 +1492,14 @@ object GeneratedLynxExtensions {
1469
1492
  const hostPkg = findTamerHostPackage(process.cwd());
1470
1493
  const devClientPkg = findDevClientPackage(process.cwd());
1471
1494
  if (!hasDevLauncher && hostPkg) {
1472
- const templateDir = path4.join(hostPkg, "android", "templates");
1495
+ const templateDir = path5.join(hostPkg, "android", "templates");
1473
1496
  for (const [src, dst] of [
1474
- ["App.java", path4.join(javaDir, "App.java")],
1475
- ["TemplateProvider.java", path4.join(javaDir, "TemplateProvider.java")],
1476
- ["MainActivity.kt", path4.join(kotlinDir, "MainActivity.kt")]
1497
+ ["App.java", path5.join(javaDir, "App.java")],
1498
+ ["TemplateProvider.java", path5.join(javaDir, "TemplateProvider.java")],
1499
+ ["MainActivity.kt", path5.join(kotlinDir, "MainActivity.kt")]
1477
1500
  ]) {
1478
- const srcPath = path4.join(templateDir, src);
1479
- if (fs4.existsSync(srcPath)) {
1501
+ const srcPath = path5.join(templateDir, src);
1502
+ if (fs5.existsSync(srcPath)) {
1480
1503
  writeFile2(dst, readAndSubstituteTemplate(srcPath, templateVars));
1481
1504
  }
1482
1505
  }
@@ -1485,34 +1508,34 @@ object GeneratedLynxExtensions {
1485
1508
  fetchAndPatchApplication(vars),
1486
1509
  fetchAndPatchTemplateProvider(vars)
1487
1510
  ]);
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));
1511
+ writeFile2(path5.join(javaDir, "App.java"), applicationSource);
1512
+ writeFile2(path5.join(javaDir, "TemplateProvider.java"), templateProviderSource);
1513
+ writeFile2(path5.join(kotlinDir, "MainActivity.kt"), getStandaloneMainActivity(vars));
1491
1514
  if (hasDevLauncher) {
1492
1515
  if (devClientPkg) {
1493
- const templateDir = path4.join(devClientPkg, "android", "templates");
1516
+ const templateDir = path5.join(devClientPkg, "android", "templates");
1494
1517
  for (const [src, dst] of [
1495
- ["ProjectActivity.kt", path4.join(kotlinDir, "ProjectActivity.kt")],
1496
- ["DevClientManager.kt", path4.join(kotlinDir, "DevClientManager.kt")],
1497
- ["DevServerPrefs.kt", path4.join(kotlinDir, "DevServerPrefs.kt")]
1518
+ ["ProjectActivity.kt", path5.join(kotlinDir, "ProjectActivity.kt")],
1519
+ ["DevClientManager.kt", path5.join(kotlinDir, "DevClientManager.kt")],
1520
+ ["DevServerPrefs.kt", path5.join(kotlinDir, "DevServerPrefs.kt")]
1498
1521
  ]) {
1499
- const srcPath = path4.join(templateDir, src);
1500
- if (fs4.existsSync(srcPath)) {
1522
+ const srcPath = path5.join(templateDir, src);
1523
+ if (fs5.existsSync(srcPath)) {
1501
1524
  writeFile2(dst, readAndSubstituteTemplate(srcPath, templateVars));
1502
1525
  }
1503
1526
  }
1504
1527
  } else {
1505
- writeFile2(path4.join(kotlinDir, "ProjectActivity.kt"), getProjectActivity(vars));
1528
+ writeFile2(path5.join(kotlinDir, "ProjectActivity.kt"), getProjectActivity(vars));
1506
1529
  const devClientManagerSource = getDevClientManager(vars);
1507
1530
  if (devClientManagerSource) {
1508
- writeFile2(path4.join(kotlinDir, "DevClientManager.kt"), devClientManagerSource);
1509
- writeFile2(path4.join(kotlinDir, "DevServerPrefs.kt"), getDevServerPrefs(vars));
1531
+ writeFile2(path5.join(kotlinDir, "DevClientManager.kt"), devClientManagerSource);
1532
+ writeFile2(path5.join(kotlinDir, "DevServerPrefs.kt"), getDevServerPrefs(vars));
1510
1533
  }
1511
1534
  }
1512
1535
  }
1513
1536
  }
1514
1537
  if (iconPaths) {
1515
- const resDir = path4.join(mainDir, "res");
1538
+ const resDir = path5.join(mainDir, "res");
1516
1539
  if (applyAndroidLauncherIcons(resDir, iconPaths)) {
1517
1540
  if (iconPaths.androidAdaptiveForeground && (iconPaths.androidAdaptiveBackground || iconPaths.androidAdaptiveBackgroundColor)) {
1518
1541
  console.log("\u2705 Android adaptive launcher from tamer.config.json icon.androidAdaptive");
@@ -1523,23 +1546,23 @@ object GeneratedLynxExtensions {
1523
1546
  }
1524
1547
  }
1525
1548
  }
1526
- fs4.mkdirSync(assetsDir, { recursive: true });
1527
- fs4.writeFileSync(path4.join(assetsDir, ".gitkeep"), "");
1549
+ fs5.mkdirSync(assetsDir, { recursive: true });
1550
+ fs5.writeFileSync(path5.join(assetsDir, ".gitkeep"), "");
1528
1551
  console.log(`\u2705 Android Kotlin project created at ${rootDir}`);
1529
1552
  async function finalizeProjectSetup() {
1530
1553
  if (androidSdk) {
1531
1554
  try {
1532
1555
  const sdkDirContent = `sdk.dir=${androidSdk.replace(/\\/g, "/")}`;
1533
- writeFile2(path4.join(rootDir, "local.properties"), sdkDirContent);
1556
+ writeFile2(path5.join(rootDir, "local.properties"), sdkDirContent);
1534
1557
  console.log("\u{1F4E6} Created local.properties from tamer.config.json.");
1535
1558
  } catch (err) {
1536
1559
  console.error(`\u274C Failed to create local.properties: ${err.message}`);
1537
1560
  }
1538
1561
  } else {
1539
- const localPropsPath = path4.join(process.cwd(), "local.properties");
1540
- if (fs4.existsSync(localPropsPath)) {
1562
+ const localPropsPath = path5.join(process.cwd(), "local.properties");
1563
+ if (fs5.existsSync(localPropsPath)) {
1541
1564
  try {
1542
- fs4.copyFileSync(localPropsPath, path4.join(rootDir, "local.properties"));
1565
+ fs5.copyFileSync(localPropsPath, path5.join(rootDir, "local.properties"));
1543
1566
  console.log("\u{1F4E6} Copied existing local.properties to the android project.");
1544
1567
  } catch (err) {
1545
1568
  console.error("\u274C Failed to copy local.properties:", err);
@@ -1558,33 +1581,33 @@ object GeneratedLynxExtensions {
1558
1581
  var create_default = create;
1559
1582
 
1560
1583
  // src/android/autolink.ts
1561
- import fs7 from "fs";
1562
- import path7 from "path";
1584
+ import fs9 from "fs";
1585
+ import path9 from "path";
1563
1586
  import { execSync as execSync2 } from "child_process";
1564
1587
 
1565
1588
  // src/common/discoverModules.ts
1566
- import fs6 from "fs";
1567
- import path6 from "path";
1589
+ import fs7 from "fs";
1590
+ import path7 from "path";
1568
1591
 
1569
1592
  // src/common/config.ts
1570
- import fs5 from "fs";
1571
- import path5 from "path";
1593
+ import fs6 from "fs";
1594
+ import path6 from "path";
1572
1595
  var LYNX_EXT_JSON = "lynx.ext.json";
1573
1596
  var TAMER_JSON = "tamer.json";
1574
1597
  function loadLynxExtJson(packagePath) {
1575
- const p = path5.join(packagePath, LYNX_EXT_JSON);
1576
- if (!fs5.existsSync(p)) return null;
1598
+ const p = path6.join(packagePath, LYNX_EXT_JSON);
1599
+ if (!fs6.existsSync(p)) return null;
1577
1600
  try {
1578
- return JSON.parse(fs5.readFileSync(p, "utf8"));
1601
+ return JSON.parse(fs6.readFileSync(p, "utf8"));
1579
1602
  } catch {
1580
1603
  return null;
1581
1604
  }
1582
1605
  }
1583
1606
  function loadTamerJson(packagePath) {
1584
- const p = path5.join(packagePath, TAMER_JSON);
1585
- if (!fs5.existsSync(p)) return null;
1607
+ const p = path6.join(packagePath, TAMER_JSON);
1608
+ if (!fs6.existsSync(p)) return null;
1586
1609
  try {
1587
- return JSON.parse(fs5.readFileSync(p, "utf8"));
1610
+ return JSON.parse(fs6.readFileSync(p, "utf8"));
1588
1611
  } catch {
1589
1612
  return null;
1590
1613
  }
@@ -1637,7 +1660,7 @@ function loadExtensionConfig(packagePath) {
1637
1660
  return normalized;
1638
1661
  }
1639
1662
  function hasExtensionConfig(packagePath) {
1640
- return fs5.existsSync(path5.join(packagePath, LYNX_EXT_JSON)) || fs5.existsSync(path5.join(packagePath, TAMER_JSON));
1663
+ return fs6.existsSync(path6.join(packagePath, LYNX_EXT_JSON)) || fs6.existsSync(path6.join(packagePath, TAMER_JSON));
1641
1664
  }
1642
1665
  function getAndroidModuleClassNames(config) {
1643
1666
  if (!config) return [];
@@ -1655,15 +1678,15 @@ function getIosElements(config) {
1655
1678
  return config?.elements ?? {};
1656
1679
  }
1657
1680
  function getNodeModulesPath(projectRoot) {
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") {
1681
+ let nodeModulesPath = path6.join(projectRoot, "node_modules");
1682
+ const workspaceRoot = path6.join(projectRoot, "..", "..");
1683
+ const rootNodeModules = path6.join(workspaceRoot, "node_modules");
1684
+ if (fs6.existsSync(path6.join(workspaceRoot, "package.json")) && fs6.existsSync(rootNodeModules) && path6.basename(path6.dirname(projectRoot)) === "packages") {
1662
1685
  nodeModulesPath = rootNodeModules;
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)) {
1686
+ } else if (!fs6.existsSync(nodeModulesPath)) {
1687
+ const altRoot = path6.join(projectRoot, "..", "..");
1688
+ const altNodeModules = path6.join(altRoot, "node_modules");
1689
+ if (fs6.existsSync(path6.join(altRoot, "package.json")) && fs6.existsSync(altNodeModules)) {
1667
1690
  nodeModulesPath = altNodeModules;
1668
1691
  }
1669
1692
  }
@@ -1672,8 +1695,8 @@ function getNodeModulesPath(projectRoot) {
1672
1695
  function discoverNativeExtensions(projectRoot) {
1673
1696
  const nodeModulesPath = getNodeModulesPath(projectRoot);
1674
1697
  const result = [];
1675
- if (!fs5.existsSync(nodeModulesPath)) return result;
1676
- const packageDirs = fs5.readdirSync(nodeModulesPath);
1698
+ if (!fs6.existsSync(nodeModulesPath)) return result;
1699
+ const packageDirs = fs6.readdirSync(nodeModulesPath);
1677
1700
  const check = (name, packagePath) => {
1678
1701
  if (!hasExtensionConfig(packagePath)) return;
1679
1702
  const config = loadExtensionConfig(packagePath);
@@ -1683,11 +1706,11 @@ function discoverNativeExtensions(projectRoot) {
1683
1706
  }
1684
1707
  };
1685
1708
  for (const dirName of packageDirs) {
1686
- const fullPath = path5.join(nodeModulesPath, dirName);
1709
+ const fullPath = path6.join(nodeModulesPath, dirName);
1687
1710
  if (dirName.startsWith("@")) {
1688
1711
  try {
1689
- for (const scopedDirName of fs5.readdirSync(fullPath)) {
1690
- check(`${dirName}/${scopedDirName}`, path5.join(fullPath, scopedDirName));
1712
+ for (const scopedDirName of fs6.readdirSync(fullPath)) {
1713
+ check(`${dirName}/${scopedDirName}`, path6.join(fullPath, scopedDirName));
1691
1714
  }
1692
1715
  } catch {
1693
1716
  }
@@ -1700,29 +1723,80 @@ function discoverNativeExtensions(projectRoot) {
1700
1723
 
1701
1724
  // src/common/discoverModules.ts
1702
1725
  function resolveNodeModulesPath(projectRoot) {
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") {
1726
+ let nodeModulesPath = path7.join(projectRoot, "node_modules");
1727
+ const workspaceRoot = path7.join(projectRoot, "..", "..");
1728
+ const rootNodeModules = path7.join(workspaceRoot, "node_modules");
1729
+ if (fs7.existsSync(path7.join(workspaceRoot, "package.json")) && fs7.existsSync(rootNodeModules) && path7.basename(path7.dirname(projectRoot)) === "packages") {
1707
1730
  nodeModulesPath = rootNodeModules;
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)) {
1731
+ } else if (!fs7.existsSync(nodeModulesPath)) {
1732
+ const altRoot = path7.join(projectRoot, "..", "..");
1733
+ const altNodeModules = path7.join(altRoot, "node_modules");
1734
+ if (fs7.existsSync(path7.join(altRoot, "package.json")) && fs7.existsSync(altNodeModules)) {
1712
1735
  nodeModulesPath = altNodeModules;
1713
1736
  }
1714
1737
  }
1715
1738
  return nodeModulesPath;
1716
1739
  }
1740
+ function resolvePackageRoot(projectRoot, packageName) {
1741
+ const nodeModulesPath = resolveNodeModulesPath(projectRoot);
1742
+ const hasPkg = (dir) => fs7.existsSync(path7.join(dir, "package.json"));
1743
+ if (packageName.startsWith("@")) {
1744
+ const parts = packageName.split("/");
1745
+ if (parts.length !== 2 || !parts[0].startsWith("@")) return null;
1746
+ const direct2 = path7.join(nodeModulesPath, parts[0], parts[1]);
1747
+ if (hasPkg(direct2)) return direct2;
1748
+ const underDevClient = path7.join(
1749
+ nodeModulesPath,
1750
+ "@tamer4lynx",
1751
+ "tamer-dev-client",
1752
+ "node_modules",
1753
+ parts[0],
1754
+ parts[1]
1755
+ );
1756
+ if (hasPkg(underDevClient)) return underDevClient;
1757
+ return null;
1758
+ }
1759
+ const direct = path7.join(nodeModulesPath, packageName);
1760
+ return hasPkg(direct) ? direct : null;
1761
+ }
1762
+ function collectTransitiveTamer4LynxPackageNames(projectRoot) {
1763
+ const hostPjPath = path7.join(projectRoot, "package.json");
1764
+ if (!fs7.existsSync(hostPjPath)) return null;
1765
+ const hostPj = JSON.parse(fs7.readFileSync(hostPjPath, "utf8"));
1766
+ if (hostPj.name !== "@tamer4lynx/tamer-dev-app") return null;
1767
+ const visited = /* @__PURE__ */ new Set();
1768
+ const queue = [];
1769
+ const enqueue = (deps) => {
1770
+ if (!deps) return;
1771
+ for (const name of Object.keys(deps)) {
1772
+ if (name.startsWith("@tamer4lynx/")) queue.push(name);
1773
+ }
1774
+ };
1775
+ enqueue(hostPj.dependencies);
1776
+ enqueue(hostPj.optionalDependencies);
1777
+ while (queue.length) {
1778
+ const name = queue.shift();
1779
+ if (visited.has(name)) continue;
1780
+ visited.add(name);
1781
+ const pkgRoot = resolvePackageRoot(projectRoot, name);
1782
+ if (!pkgRoot || !fs7.existsSync(path7.join(pkgRoot, "package.json"))) continue;
1783
+ const sub = JSON.parse(fs7.readFileSync(path7.join(pkgRoot, "package.json"), "utf8"));
1784
+ const merged = { ...sub.dependencies, ...sub.optionalDependencies };
1785
+ for (const dep of Object.keys(merged)) {
1786
+ if (dep.startsWith("@tamer4lynx/") && !visited.has(dep)) queue.push(dep);
1787
+ }
1788
+ }
1789
+ return visited;
1790
+ }
1717
1791
  function discoverModules(projectRoot) {
1718
1792
  const nodeModulesPath = resolveNodeModulesPath(projectRoot);
1719
1793
  const packages = [];
1720
- if (!fs6.existsSync(nodeModulesPath)) {
1794
+ if (!fs7.existsSync(nodeModulesPath)) {
1721
1795
  return [];
1722
1796
  }
1723
- const packageDirs = fs6.readdirSync(nodeModulesPath);
1797
+ const packageDirs = fs7.readdirSync(nodeModulesPath);
1724
1798
  for (const dirName of packageDirs) {
1725
- const fullPath = path6.join(nodeModulesPath, dirName);
1799
+ const fullPath = path7.join(nodeModulesPath, dirName);
1726
1800
  const checkPackage = (name, packagePath) => {
1727
1801
  if (!hasExtensionConfig(packagePath)) return;
1728
1802
  const config = loadExtensionConfig(packagePath);
@@ -1731,9 +1805,9 @@ function discoverModules(projectRoot) {
1731
1805
  };
1732
1806
  if (dirName.startsWith("@")) {
1733
1807
  try {
1734
- const scopedDirs = fs6.readdirSync(fullPath);
1808
+ const scopedDirs = fs7.readdirSync(fullPath);
1735
1809
  for (const scopedDirName of scopedDirs) {
1736
- const scopedPackagePath = path6.join(fullPath, scopedDirName);
1810
+ const scopedPackagePath = path7.join(fullPath, scopedDirName);
1737
1811
  checkPackage(`${dirName}/${scopedDirName}`, scopedPackagePath);
1738
1812
  }
1739
1813
  } catch (e) {
@@ -1744,6 +1818,10 @@ function discoverModules(projectRoot) {
1744
1818
  checkPackage(dirName, fullPath);
1745
1819
  }
1746
1820
  }
1821
+ const allowed = collectTransitiveTamer4LynxPackageNames(projectRoot);
1822
+ if (allowed) {
1823
+ return packages.filter((p) => allowed.has(p.name));
1824
+ }
1747
1825
  return packages;
1748
1826
  }
1749
1827
 
@@ -1795,6 +1873,8 @@ ${allModuleClasses.map((c) => ` "${c.replace(/\\/g, "\\\\").replace(/
1795
1873
  fun onHostViewChanged(view: android.view.View?) {
1796
1874
  ${hostViewLines}
1797
1875
  }` : "\n fun onHostViewChanged(view: android.view.View?) {}\n";
1876
+ const devToolBootstrapPrefix = hasDevClient ? " com.nanofuxion.tamerdevclient.LynxDevToolBootstrap.configure(context)\n" : "";
1877
+ const devToolBootstrapSuffix = hasDevClient ? " com.nanofuxion.tamerdevclient.LynxDevToolBootstrap.enableLynxDebugFlags()\n" : "";
1798
1878
  return `package ${projectPackage}.generated
1799
1879
 
1800
1880
  import android.content.Context
@@ -1806,7 +1886,7 @@ ${elementImports}
1806
1886
 
1807
1887
  object GeneratedLynxExtensions {
1808
1888
  fun register(context: Context) {
1809
- ${allRegistrations}${devClientSupportedBlock}
1889
+ ${devToolBootstrapPrefix}${allRegistrations}${devClientSupportedBlock}${devToolBootstrapSuffix}
1810
1890
  }
1811
1891
 
1812
1892
  fun configureViewBuilder(viewBuilder: LynxViewBuilder) {
@@ -1894,6 +1974,69 @@ ${createDelayedBody}
1894
1974
  `;
1895
1975
  }
1896
1976
 
1977
+ // src/android/cleanTamerAndroidCaches.ts
1978
+ import fs8 from "fs";
1979
+ import path8 from "path";
1980
+ var MARKER_REL = path8.join(".tamer", "android-lib-versions.json");
1981
+ function readTamerPackageVersions(projectRoot) {
1982
+ const out = {};
1983
+ const nm = path8.join(projectRoot, "node_modules", "@tamer4lynx");
1984
+ if (!fs8.existsSync(nm)) return out;
1985
+ for (const name of fs8.readdirSync(nm, { withFileTypes: true })) {
1986
+ if (!name.isDirectory()) continue;
1987
+ const pj = path8.join(nm, name.name, "package.json");
1988
+ if (!fs8.existsSync(pj)) continue;
1989
+ try {
1990
+ const v = JSON.parse(fs8.readFileSync(pj, "utf-8")).version;
1991
+ if (typeof v === "string") out[name.name] = v;
1992
+ } catch {
1993
+ }
1994
+ }
1995
+ return out;
1996
+ }
1997
+ function versionsEqual(a, b) {
1998
+ const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
1999
+ for (const k of keys) {
2000
+ if (a[k] !== b[k]) return false;
2001
+ }
2002
+ return true;
2003
+ }
2004
+ function cleanTamerAndroidLibBuildsIfVersionsChanged(projectRoot) {
2005
+ const markerPath = path8.join(projectRoot, MARKER_REL);
2006
+ let previous = {};
2007
+ if (fs8.existsSync(markerPath)) {
2008
+ try {
2009
+ previous = JSON.parse(fs8.readFileSync(markerPath, "utf-8"));
2010
+ } catch {
2011
+ previous = {};
2012
+ }
2013
+ }
2014
+ const current = readTamerPackageVersions(projectRoot);
2015
+ if (versionsEqual(previous, current)) return;
2016
+ const nm = path8.join(projectRoot, "node_modules", "@tamer4lynx");
2017
+ if (fs8.existsSync(nm)) {
2018
+ for (const name of fs8.readdirSync(nm, { withFileTypes: true })) {
2019
+ if (!name.isDirectory()) continue;
2020
+ const buildDir = path8.join(nm, name.name, "android", "build");
2021
+ if (fs8.existsSync(buildDir)) {
2022
+ fs8.rmSync(buildDir, { recursive: true, force: true });
2023
+ }
2024
+ }
2025
+ }
2026
+ fs8.mkdirSync(path8.dirname(markerPath), { recursive: true });
2027
+ fs8.writeFileSync(markerPath, JSON.stringify(current, null, 0), "utf-8");
2028
+ console.log(
2029
+ "\u2139\uFE0F Cleared @tamer4lynx Android library build dirs (linked package versions changed)."
2030
+ );
2031
+ }
2032
+ function invalidateTamerAndroidLibVersionMarker(projectRoot) {
2033
+ const markerPath = path8.join(projectRoot, MARKER_REL);
2034
+ try {
2035
+ if (fs8.existsSync(markerPath)) fs8.unlinkSync(markerPath);
2036
+ } catch {
2037
+ }
2038
+ }
2039
+
1897
2040
  // src/android/autolink.ts
1898
2041
  var TAMER_DEV_CLIENT_FALLBACK = {
1899
2042
  name: "@tamer4lynx/tamer-dev-client",
@@ -1939,11 +2082,11 @@ var autolink = (opts) => {
1939
2082
  const packageName = config.android.packageName;
1940
2083
  const projectRoot = resolved.projectRoot;
1941
2084
  function updateGeneratedSection(filePath, newContent, startMarker, endMarker) {
1942
- if (!fs7.existsSync(filePath)) {
2085
+ if (!fs9.existsSync(filePath)) {
1943
2086
  console.warn(`\u26A0\uFE0F File not found, skipping update: ${filePath}`);
1944
2087
  return;
1945
2088
  }
1946
- let fileContent = fs7.readFileSync(filePath, "utf8");
2089
+ let fileContent = fs9.readFileSync(filePath, "utf8");
1947
2090
  const escapedStartMarker = startMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1948
2091
  const escapedEndMarker = endMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1949
2092
  const regex = new RegExp(`${escapedStartMarker}[\\s\\S]*?${escapedEndMarker}`, "g");
@@ -1953,16 +2096,16 @@ ${endMarker}`;
1953
2096
  if (regex.test(fileContent)) {
1954
2097
  fileContent = fileContent.replace(regex, replacementBlock);
1955
2098
  } else {
1956
- console.warn(`\u26A0\uFE0F Could not find autolink markers in ${path7.basename(filePath)}. Appending to the end of the file.`);
2099
+ console.warn(`\u26A0\uFE0F Could not find autolink markers in ${path9.basename(filePath)}. Appending to the end of the file.`);
1957
2100
  fileContent += `
1958
2101
  ${replacementBlock}
1959
2102
  `;
1960
2103
  }
1961
- fs7.writeFileSync(filePath, fileContent);
1962
- console.log(`\u2705 Updated autolinked section in ${path7.basename(filePath)}`);
2104
+ fs9.writeFileSync(filePath, fileContent);
2105
+ console.log(`\u2705 Updated autolinked section in ${path9.basename(filePath)}`);
1963
2106
  }
1964
2107
  function updateSettingsGradle(packages) {
1965
- const settingsFilePath = path7.join(appAndroidPath, "settings.gradle.kts");
2108
+ const settingsFilePath = path9.join(appAndroidPath, "settings.gradle.kts");
1966
2109
  let scriptContent = `// This section is automatically generated by Tamer4Lynx.
1967
2110
  // Manual edits will be overwritten.`;
1968
2111
  const androidPackages = packages.filter((p) => p.config.android);
@@ -1970,7 +2113,7 @@ ${replacementBlock}
1970
2113
  androidPackages.forEach((pkg) => {
1971
2114
  const gradleProjectName = pkg.name.replace(/^@/, "").replace(/\//g, "_");
1972
2115
  const sourceDir = pkg.config.android?.sourceDir || "android";
1973
- const projectPath = path7.resolve(pkg.packagePath, sourceDir).replace(/\\/g, "/");
2116
+ const projectPath = path9.resolve(pkg.packagePath, sourceDir).replace(/\\/g, "/");
1974
2117
  scriptContent += `
1975
2118
  include(":${gradleProjectName}")`;
1976
2119
  scriptContent += `
@@ -1983,7 +2126,7 @@ println("No native modules found by Tamer4Lynx autolinker.")`;
1983
2126
  updateGeneratedSection(settingsFilePath, scriptContent.trim(), "// GENERATED AUTOLINK START", "// GENERATED AUTOLINK END");
1984
2127
  }
1985
2128
  function updateAppBuildGradle(packages) {
1986
- const appBuildGradlePath = path7.join(appAndroidPath, "app", "build.gradle.kts");
2129
+ const appBuildGradlePath = path9.join(appAndroidPath, "app", "build.gradle.kts");
1987
2130
  const androidPackages = packages.filter((p) => p.config.android);
1988
2131
  const implementationLines = androidPackages.map((p) => {
1989
2132
  const gradleProjectName = p.name.replace(/^@/, "").replace(/\//g, "_");
@@ -2001,35 +2144,35 @@ ${implementationLines || " // No native dependencies found to link."}`;
2001
2144
  }
2002
2145
  function generateKotlinExtensionsFile(packages, projectPackage) {
2003
2146
  const packagePath = projectPackage.replace(/\./g, "/");
2004
- const generatedDir = path7.join(appAndroidPath, "app", "src", "main", "kotlin", packagePath, "generated");
2005
- const kotlinExtensionsPath = path7.join(generatedDir, "GeneratedLynxExtensions.kt");
2147
+ const generatedDir = path9.join(appAndroidPath, "app", "src", "main", "kotlin", packagePath, "generated");
2148
+ const kotlinExtensionsPath = path9.join(generatedDir, "GeneratedLynxExtensions.kt");
2006
2149
  const content = `/**
2007
2150
  * This file is generated by the Tamer4Lynx autolinker.
2008
2151
  * Do not edit this file manually.
2009
2152
  */
2010
2153
  ${generateLynxExtensionsKotlin(packages, projectPackage)}`;
2011
- fs7.mkdirSync(generatedDir, { recursive: true });
2012
- fs7.writeFileSync(kotlinExtensionsPath, content.trimStart());
2154
+ fs9.mkdirSync(generatedDir, { recursive: true });
2155
+ fs9.writeFileSync(kotlinExtensionsPath, content.trimStart());
2013
2156
  console.log(`\u2705 Generated Kotlin extensions at ${kotlinExtensionsPath}`);
2014
2157
  }
2015
2158
  function generateActivityLifecycleFile(packages, projectPackage) {
2016
2159
  const packageKotlinPath = projectPackage.replace(/\./g, "/");
2017
- const generatedDir = path7.join(appAndroidPath, "app", "src", "main", "kotlin", packageKotlinPath, "generated");
2018
- const outputPath = path7.join(generatedDir, "GeneratedActivityLifecycle.kt");
2160
+ const generatedDir = path9.join(appAndroidPath, "app", "src", "main", "kotlin", packageKotlinPath, "generated");
2161
+ const outputPath = path9.join(generatedDir, "GeneratedActivityLifecycle.kt");
2019
2162
  const content = `/**
2020
2163
  * This file is generated by the Tamer4Lynx autolinker.
2021
2164
  * Do not edit this file manually.
2022
2165
  */
2023
2166
  ${generateActivityLifecycleKotlin(packages, projectPackage)}`;
2024
- fs7.mkdirSync(generatedDir, { recursive: true });
2025
- fs7.writeFileSync(outputPath, content);
2167
+ fs9.mkdirSync(generatedDir, { recursive: true });
2168
+ fs9.writeFileSync(outputPath, content);
2026
2169
  console.log(`\u2705 Generated activity lifecycle patches at ${outputPath}`);
2027
2170
  }
2028
2171
  function syncDeepLinkIntentFilters() {
2029
2172
  const deepLinks = config.android?.deepLinks;
2030
2173
  if (!deepLinks || deepLinks.length === 0) return;
2031
- const manifestPath = path7.join(appAndroidPath, "app", "src", "main", "AndroidManifest.xml");
2032
- if (!fs7.existsSync(manifestPath)) return;
2174
+ const manifestPath = path9.join(appAndroidPath, "app", "src", "main", "AndroidManifest.xml");
2175
+ if (!fs9.existsSync(manifestPath)) return;
2033
2176
  const intentFilters = deepLinks.map((link) => {
2034
2177
  const dataAttrs = [
2035
2178
  `android:scheme="${link.scheme}"`,
@@ -2054,9 +2197,9 @@ ${generateActivityLifecycleKotlin(packages, projectPackage)}`;
2054
2197
  console.log("\u{1F50E} Finding Lynx extension packages (lynx.ext.json / tamer.json)...");
2055
2198
  let packages = discoverModules(projectRoot).filter((p) => p.config.android);
2056
2199
  const includeDevClient = opts?.includeDevClient === true;
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;
2200
+ const devClientScoped = path9.join(projectRoot, "node_modules", "@tamer4lynx", "tamer-dev-client");
2201
+ const devClientFlat = path9.join(projectRoot, "node_modules", "tamer-dev-client");
2202
+ const devClientPath = fs9.existsSync(path9.join(devClientScoped, "android")) ? devClientScoped : fs9.existsSync(path9.join(devClientFlat, "android")) ? devClientFlat : null;
2060
2203
  const hasDevClient = packages.some((p) => p.name === "@tamer4lynx/tamer-dev-client" || p.name === "tamer-dev-client");
2061
2204
  if (includeDevClient && devClientPath && !hasDevClient) {
2062
2205
  packages = [{
@@ -2080,11 +2223,12 @@ ${generateActivityLifecycleKotlin(packages, projectPackage)}`;
2080
2223
  ensureXElementDeps();
2081
2224
  ensureReleaseSigning();
2082
2225
  runGradleSync();
2226
+ invalidateTamerAndroidLibVersionMarker(projectRoot);
2083
2227
  console.log("\u2728 Autolinking complete.");
2084
2228
  }
2085
2229
  function runGradleSync() {
2086
- const gradlew = path7.join(appAndroidPath, process.platform === "win32" ? "gradlew.bat" : "gradlew");
2087
- if (!fs7.existsSync(gradlew)) return;
2230
+ const gradlew = path9.join(appAndroidPath, process.platform === "win32" ? "gradlew.bat" : "gradlew");
2231
+ if (!fs9.existsSync(gradlew)) return;
2088
2232
  try {
2089
2233
  console.log("\u2139\uFE0F Running Gradle sync in android directory...");
2090
2234
  execSync2(process.platform === "win32" ? "gradlew.bat projects" : "./gradlew projects", {
@@ -2098,14 +2242,14 @@ ${generateActivityLifecycleKotlin(packages, projectPackage)}`;
2098
2242
  }
2099
2243
  }
2100
2244
  function syncVersionCatalog(packages) {
2101
- const libsTomlPath = path7.join(appAndroidPath, "gradle", "libs.versions.toml");
2102
- if (!fs7.existsSync(libsTomlPath)) return;
2245
+ const libsTomlPath = path9.join(appAndroidPath, "gradle", "libs.versions.toml");
2246
+ if (!fs9.existsSync(libsTomlPath)) return;
2103
2247
  const requiredAliases = /* @__PURE__ */ new Set();
2104
2248
  const requiredPluginAliases = /* @__PURE__ */ new Set();
2105
2249
  for (const pkg of packages) {
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");
2250
+ const buildPath = path9.join(pkg.packagePath, pkg.config.android?.sourceDir || "android", "build.gradle.kts");
2251
+ if (!fs9.existsSync(buildPath)) continue;
2252
+ const content = fs9.readFileSync(buildPath, "utf8");
2109
2253
  for (const m of content.matchAll(/libs\.([\w.]+)/g)) {
2110
2254
  const alias = m[1];
2111
2255
  if (alias && alias in REQUIRED_CATALOG_ENTRIES) requiredAliases.add(alias);
@@ -2116,7 +2260,7 @@ ${generateActivityLifecycleKotlin(packages, projectPackage)}`;
2116
2260
  }
2117
2261
  }
2118
2262
  if (requiredAliases.size === 0 && requiredPluginAliases.size === 0) return;
2119
- let toml = fs7.readFileSync(libsTomlPath, "utf8");
2263
+ let toml = fs9.readFileSync(libsTomlPath, "utf8");
2120
2264
  let updated = false;
2121
2265
  for (const alias of requiredAliases) {
2122
2266
  const entry = REQUIRED_CATALOG_ENTRIES[alias];
@@ -2140,14 +2284,14 @@ ${generateActivityLifecycleKotlin(packages, projectPackage)}`;
2140
2284
  updated = true;
2141
2285
  }
2142
2286
  if (updated) {
2143
- fs7.writeFileSync(libsTomlPath, toml);
2287
+ fs9.writeFileSync(libsTomlPath, toml);
2144
2288
  console.log("\u2705 Synced version catalog (libs.versions.toml) for linked modules.");
2145
2289
  }
2146
2290
  }
2147
2291
  function ensureXElementDeps() {
2148
- const libsTomlPath = path7.join(appAndroidPath, "gradle", "libs.versions.toml");
2149
- if (fs7.existsSync(libsTomlPath)) {
2150
- let toml = fs7.readFileSync(libsTomlPath, "utf8");
2292
+ const libsTomlPath = path9.join(appAndroidPath, "gradle", "libs.versions.toml");
2293
+ if (fs9.existsSync(libsTomlPath)) {
2294
+ let toml = fs9.readFileSync(libsTomlPath, "utf8");
2151
2295
  let updated = false;
2152
2296
  if (!toml.includes("lynx-xelement =")) {
2153
2297
  toml = toml.replace(
@@ -2159,13 +2303,13 @@ lynx-xelement-input = { module = "org.lynxsdk.lynx:xelement-input", version.ref
2159
2303
  updated = true;
2160
2304
  }
2161
2305
  if (updated) {
2162
- fs7.writeFileSync(libsTomlPath, toml);
2306
+ fs9.writeFileSync(libsTomlPath, toml);
2163
2307
  console.log("\u2705 Added XElement entries to version catalog.");
2164
2308
  }
2165
2309
  }
2166
- const appBuildPath = path7.join(appAndroidPath, "app", "build.gradle.kts");
2167
- if (fs7.existsSync(appBuildPath)) {
2168
- let content = fs7.readFileSync(appBuildPath, "utf8");
2310
+ const appBuildPath = path9.join(appAndroidPath, "app", "build.gradle.kts");
2311
+ if (fs9.existsSync(appBuildPath)) {
2312
+ let content = fs9.readFileSync(appBuildPath, "utf8");
2169
2313
  if (!content.includes("lynx.xelement")) {
2170
2314
  content = content.replace(
2171
2315
  /(implementation\(libs\.lynx\.service\.http\))/,
@@ -2173,15 +2317,15 @@ lynx-xelement-input = { module = "org.lynxsdk.lynx:xelement-input", version.ref
2173
2317
  implementation(libs.lynx.xelement)
2174
2318
  implementation(libs.lynx.xelement.input)`
2175
2319
  );
2176
- fs7.writeFileSync(appBuildPath, content);
2320
+ fs9.writeFileSync(appBuildPath, content);
2177
2321
  console.log("\u2705 Added XElement dependencies to app build.gradle.kts.");
2178
2322
  }
2179
2323
  }
2180
2324
  }
2181
2325
  function ensureReleaseSigning() {
2182
- const appBuildPath = path7.join(appAndroidPath, "app", "build.gradle.kts");
2183
- if (!fs7.existsSync(appBuildPath)) return;
2184
- let content = fs7.readFileSync(appBuildPath, "utf8");
2326
+ const appBuildPath = path9.join(appAndroidPath, "app", "build.gradle.kts");
2327
+ if (!fs9.existsSync(appBuildPath)) return;
2328
+ let content = fs9.readFileSync(appBuildPath, "utf8");
2185
2329
  if (content.includes('signingConfig = signingConfigs.getByName("debug")')) return;
2186
2330
  const releaseBlock = /(release\s*\{)([\s\S]*?)(\n \}\s*\n(\s*\}|\s*compileOptions))/;
2187
2331
  const match = content.match(releaseBlock);
@@ -2192,13 +2336,13 @@ lynx-xelement-input = { module = "org.lynxsdk.lynx:xelement-input", version.ref
2192
2336
  signingConfig = signingConfigs.getByName("debug")
2193
2337
  $3`
2194
2338
  );
2195
- fs7.writeFileSync(appBuildPath, content);
2339
+ fs9.writeFileSync(appBuildPath, content);
2196
2340
  console.log("\u2705 Set release signing to debug so installRelease works without a keystore.");
2197
2341
  }
2198
2342
  }
2199
2343
  function syncManifestPermissions(packages) {
2200
- const manifestPath = path7.join(appAndroidPath, "app", "src", "main", "AndroidManifest.xml");
2201
- if (!fs7.existsSync(manifestPath)) return;
2344
+ const manifestPath = path9.join(appAndroidPath, "app", "src", "main", "AndroidManifest.xml");
2345
+ if (!fs9.existsSync(manifestPath)) return;
2202
2346
  const allPermissions = /* @__PURE__ */ new Set();
2203
2347
  for (const pkg of packages) {
2204
2348
  const perms = pkg.config.android?.permissions;
@@ -2210,7 +2354,7 @@ lynx-xelement-input = { module = "org.lynxsdk.lynx:xelement-input", version.ref
2210
2354
  }
2211
2355
  }
2212
2356
  if (allPermissions.size === 0) return;
2213
- let manifest = fs7.readFileSync(manifestPath, "utf8");
2357
+ let manifest = fs9.readFileSync(manifestPath, "utf8");
2214
2358
  const existingMatch = [...manifest.matchAll(/<uses-permission android:name="(android\.permission\.\w+)"\s*\/>/g)];
2215
2359
  const existing = new Set(existingMatch.map((m) => m[1]));
2216
2360
  const toAdd = [...allPermissions].filter((p) => !existing.has(p));
@@ -2221,7 +2365,7 @@ lynx-xelement-input = { module = "org.lynxsdk.lynx:xelement-input", version.ref
2221
2365
  `${newLines}
2222
2366
  $1$2`
2223
2367
  );
2224
- fs7.writeFileSync(manifestPath, manifest);
2368
+ fs9.writeFileSync(manifestPath, manifest);
2225
2369
  console.log(`\u2705 Synced manifest permissions: ${toAdd.map((p) => p.split(".").pop()).join(", ")}`);
2226
2370
  }
2227
2371
  run();
@@ -2229,26 +2373,26 @@ $1$2`
2229
2373
  var autolink_default = autolink;
2230
2374
 
2231
2375
  // src/android/bundle.ts
2232
- import fs11 from "fs";
2233
- import path11 from "path";
2376
+ import fs13 from "fs";
2377
+ import path13 from "path";
2234
2378
  import { execSync as execSync3 } from "child_process";
2235
2379
 
2236
2380
  // src/common/copyDistAssets.ts
2237
- import fs8 from "fs";
2238
- import path8 from "path";
2381
+ import fs10 from "fs";
2382
+ import path10 from "path";
2239
2383
  var SKIP = /* @__PURE__ */ new Set([".rspeedy", "stats.json"]);
2240
2384
  function copyDistAssets(distDir, destDir, bundleFile) {
2241
- if (!fs8.existsSync(distDir)) return;
2242
- for (const entry of fs8.readdirSync(distDir)) {
2385
+ if (!fs10.existsSync(distDir)) return;
2386
+ for (const entry of fs10.readdirSync(distDir)) {
2243
2387
  if (SKIP.has(entry)) continue;
2244
- const src = path8.join(distDir, entry);
2245
- const dest = path8.join(destDir, entry);
2246
- const stat = fs8.statSync(src);
2388
+ const src = path10.join(distDir, entry);
2389
+ const dest = path10.join(destDir, entry);
2390
+ const stat = fs10.statSync(src);
2247
2391
  if (stat.isDirectory()) {
2248
- fs8.mkdirSync(dest, { recursive: true });
2392
+ fs10.mkdirSync(dest, { recursive: true });
2249
2393
  copyDistAssets(src, dest, bundleFile);
2250
2394
  } else {
2251
- fs8.copyFileSync(src, dest);
2395
+ fs10.copyFileSync(src, dest);
2252
2396
  if (entry !== bundleFile) {
2253
2397
  console.log(`\u2728 Copied asset: ${entry}`);
2254
2398
  }
@@ -2257,8 +2401,8 @@ function copyDistAssets(distDir, destDir, bundleFile) {
2257
2401
  }
2258
2402
 
2259
2403
  // src/common/tsconfigUtils.ts
2260
- import fs9 from "fs";
2261
- import path9 from "path";
2404
+ import fs11 from "fs";
2405
+ import path11 from "path";
2262
2406
  function stripJsonCommentsAndTrailingCommas(str) {
2263
2407
  return str.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "").replace(/,\s*([\]}])/g, "$1");
2264
2408
  }
@@ -2270,19 +2414,19 @@ function parseTsconfigJson(raw) {
2270
2414
  }
2271
2415
  }
2272
2416
  function readTsconfig(filePath) {
2273
- if (!fs9.existsSync(filePath)) return null;
2417
+ if (!fs11.existsSync(filePath)) return null;
2274
2418
  try {
2275
- return parseTsconfigJson(fs9.readFileSync(filePath, "utf-8"));
2419
+ return parseTsconfigJson(fs11.readFileSync(filePath, "utf-8"));
2276
2420
  } catch {
2277
2421
  return null;
2278
2422
  }
2279
2423
  }
2280
2424
  function resolveExtends(tsconfig, dir) {
2281
2425
  if (!tsconfig.extends) return tsconfig;
2282
- const basePath = path9.resolve(dir, tsconfig.extends);
2426
+ const basePath = path11.resolve(dir, tsconfig.extends);
2283
2427
  const base = readTsconfig(basePath);
2284
2428
  if (!base) return tsconfig;
2285
- const baseDir = path9.dirname(basePath);
2429
+ const baseDir = path11.dirname(basePath);
2286
2430
  const resolved = resolveExtends(base, baseDir);
2287
2431
  return {
2288
2432
  ...resolved,
@@ -2291,13 +2435,13 @@ function resolveExtends(tsconfig, dir) {
2291
2435
  };
2292
2436
  }
2293
2437
  function fixTsconfigReferencesForBuild(tsconfigPath) {
2294
- const dir = path9.dirname(tsconfigPath);
2438
+ const dir = path11.dirname(tsconfigPath);
2295
2439
  const tsconfig = readTsconfig(tsconfigPath);
2296
2440
  if (!tsconfig?.references?.length) return false;
2297
2441
  const refsWithNoEmit = [];
2298
2442
  for (const ref of tsconfig.references) {
2299
- const refPath = path9.resolve(dir, ref.path);
2300
- const refConfigPath = refPath.endsWith(".json") ? refPath : path9.join(refPath, "tsconfig.json");
2443
+ const refPath = path11.resolve(dir, ref.path);
2444
+ const refConfigPath = refPath.endsWith(".json") ? refPath : path11.join(refPath, "tsconfig.json");
2301
2445
  const refConfig = readTsconfig(refConfigPath);
2302
2446
  if (refConfig?.compilerOptions?.noEmit === true) {
2303
2447
  refsWithNoEmit.push(refConfigPath);
@@ -2312,16 +2456,16 @@ function fixTsconfigReferencesForBuild(tsconfigPath) {
2312
2456
  const includes = [];
2313
2457
  const compilerOpts = { ...tsconfig.compilerOptions };
2314
2458
  for (const ref of tsconfig.references) {
2315
- const refPath = path9.resolve(dir, ref.path);
2316
- const refConfigPath = refPath.endsWith(".json") ? refPath : path9.join(refPath, "tsconfig.json");
2459
+ const refPath = path11.resolve(dir, ref.path);
2460
+ const refConfigPath = refPath.endsWith(".json") ? refPath : path11.join(refPath, "tsconfig.json");
2317
2461
  const refConfig = readTsconfig(refConfigPath);
2318
2462
  if (!refConfig) continue;
2319
- const refDir = path9.dirname(refConfigPath);
2463
+ const refDir = path11.dirname(refConfigPath);
2320
2464
  const resolved = resolveExtends(refConfig, refDir);
2321
2465
  const inc = resolved.include;
2322
2466
  if (inc) {
2323
2467
  const arr = Array.isArray(inc) ? inc : [inc];
2324
- const baseDir = path9.relative(dir, refDir);
2468
+ const baseDir = path11.relative(dir, refDir);
2325
2469
  for (const p of arr) {
2326
2470
  const clean = typeof p === "string" && p.startsWith("./") ? p.slice(2) : p;
2327
2471
  includes.push(!baseDir || baseDir === "." ? clean : `${baseDir}/${clean}`);
@@ -2339,7 +2483,7 @@ function fixTsconfigReferencesForBuild(tsconfigPath) {
2339
2483
  if (includes.length > 0) merged.include = [...new Set(includes)];
2340
2484
  compilerOpts.noEmit = true;
2341
2485
  merged.compilerOptions = compilerOpts;
2342
- fs9.writeFileSync(tsconfigPath, JSON.stringify(merged, null, 2));
2486
+ fs11.writeFileSync(tsconfigPath, JSON.stringify(merged, null, 2));
2343
2487
  return true;
2344
2488
  }
2345
2489
  function addTamerTypesInclude(tsconfigPath, tamerTypesInclude) {
@@ -2350,23 +2494,23 @@ function addTamerTypesInclude(tsconfigPath, tamerTypesInclude) {
2350
2494
  if (arr.some((p) => (typeof p === "string" ? p : "").includes("tamer-"))) return false;
2351
2495
  arr.push(tamerTypesInclude);
2352
2496
  tsconfig.include = arr;
2353
- fs9.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
2497
+ fs11.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
2354
2498
  return true;
2355
2499
  }
2356
2500
 
2357
2501
  // src/android/syncDevClient.ts
2358
- import fs10 from "fs";
2359
- import path10 from "path";
2502
+ import fs12 from "fs";
2503
+ import path12 from "path";
2360
2504
  function readAndSubstituteTemplate2(templatePath, vars) {
2361
- const raw = fs10.readFileSync(templatePath, "utf-8");
2505
+ const raw = fs12.readFileSync(templatePath, "utf-8");
2362
2506
  return Object.entries(vars).reduce(
2363
2507
  (s, [k, v]) => s.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), v),
2364
2508
  raw
2365
2509
  );
2366
2510
  }
2367
2511
  function patchAppLogService(appPath) {
2368
- if (!fs10.existsSync(appPath)) return;
2369
- const raw = fs10.readFileSync(appPath, "utf-8");
2512
+ if (!fs12.existsSync(appPath)) return;
2513
+ const raw = fs12.readFileSync(appPath, "utf-8");
2370
2514
  const patched = raw.replace(
2371
2515
  /private void initLynxService\(\)\s*\{[\s\S]*?\n\s*}\s*\n\s*private void initFresco\(\)/,
2372
2516
  `private void initLynxService() {
@@ -2385,7 +2529,7 @@ function patchAppLogService(appPath) {
2385
2529
  private void initFresco()`
2386
2530
  );
2387
2531
  if (patched !== raw) {
2388
- fs10.writeFileSync(appPath, patched);
2532
+ fs12.writeFileSync(appPath, patched);
2389
2533
  }
2390
2534
  }
2391
2535
  async function syncDevClient(opts) {
@@ -2400,9 +2544,9 @@ async function syncDevClient(opts) {
2400
2544
  const packageName = config.android?.packageName;
2401
2545
  const appName = config.android?.appName;
2402
2546
  const packagePath = packageName.replace(/\./g, "/");
2403
- const javaDir = path10.join(rootDir, "app", "src", "main", "java", packagePath);
2404
- const kotlinDir = path10.join(rootDir, "app", "src", "main", "kotlin", packagePath);
2405
- if (!fs10.existsSync(javaDir) || !fs10.existsSync(kotlinDir)) {
2547
+ const javaDir = path12.join(rootDir, "app", "src", "main", "java", packagePath);
2548
+ const kotlinDir = path12.join(rootDir, "app", "src", "main", "kotlin", packagePath);
2549
+ if (!fs12.existsSync(javaDir) || !fs12.existsSync(kotlinDir)) {
2406
2550
  console.error("\u274C Android project not found. Run `tamer android create` first.");
2407
2551
  process.exit(1);
2408
2552
  }
@@ -2418,14 +2562,14 @@ async function syncDevClient(opts) {
2418
2562
  const [templateProviderSource] = await Promise.all([
2419
2563
  fetchAndPatchTemplateProvider(vars)
2420
2564
  ]);
2421
- fs10.writeFileSync(path10.join(javaDir, "TemplateProvider.java"), templateProviderSource);
2422
- fs10.writeFileSync(path10.join(kotlinDir, "MainActivity.kt"), getStandaloneMainActivity(vars));
2423
- patchAppLogService(path10.join(javaDir, "App.java"));
2424
- const appDir = path10.join(rootDir, "app");
2425
- const mainDir = path10.join(appDir, "src", "main");
2426
- const manifestPath = path10.join(mainDir, "AndroidManifest.xml");
2565
+ fs12.writeFileSync(path12.join(javaDir, "TemplateProvider.java"), templateProviderSource);
2566
+ fs12.writeFileSync(path12.join(kotlinDir, "MainActivity.kt"), getStandaloneMainActivity(vars));
2567
+ patchAppLogService(path12.join(javaDir, "App.java"));
2568
+ const appDir = path12.join(rootDir, "app");
2569
+ const mainDir = path12.join(appDir, "src", "main");
2570
+ const manifestPath = path12.join(mainDir, "AndroidManifest.xml");
2427
2571
  if (hasDevClient) {
2428
- const templateDir = path10.join(devClientPkg, "android", "templates");
2572
+ const templateDir = path12.join(devClientPkg, "android", "templates");
2429
2573
  const templateVars = { PACKAGE_NAME: packageName, APP_NAME: appName };
2430
2574
  const devClientFiles = [
2431
2575
  "DevClientManager.kt",
@@ -2434,13 +2578,13 @@ async function syncDevClient(opts) {
2434
2578
  "PortraitCaptureActivity.kt"
2435
2579
  ];
2436
2580
  for (const f of devClientFiles) {
2437
- const src = path10.join(templateDir, f);
2438
- if (fs10.existsSync(src)) {
2581
+ const src = path12.join(templateDir, f);
2582
+ if (fs12.existsSync(src)) {
2439
2583
  const content = readAndSubstituteTemplate2(src, templateVars);
2440
- fs10.writeFileSync(path10.join(kotlinDir, f), content);
2584
+ fs12.writeFileSync(path12.join(kotlinDir, f), content);
2441
2585
  }
2442
2586
  }
2443
- let manifest = fs10.readFileSync(manifestPath, "utf-8");
2587
+ let manifest = fs12.readFileSync(manifestPath, "utf-8");
2444
2588
  const projectActivityEntry = ' <activity android:name=".ProjectActivity" android:exported="false" android:taskAffinity="" android:launchMode="singleTask" android:documentLaunchMode="always" android:windowSoftInputMode="adjustResize" />';
2445
2589
  const portraitCaptureEntry = ' <activity android:name=".PortraitCaptureActivity" android:screenOrientation="portrait" android:stateNotNeeded="true" android:theme="@style/zxing_CaptureTheme" android:windowSoftInputMode="stateAlwaysHidden" />';
2446
2590
  if (!manifest.includes("ProjectActivity")) {
@@ -2462,16 +2606,16 @@ $1$2`);
2462
2606
  '$1 android:windowSoftInputMode="adjustResize"$2'
2463
2607
  );
2464
2608
  }
2465
- fs10.writeFileSync(manifestPath, manifest);
2609
+ fs12.writeFileSync(manifestPath, manifest);
2466
2610
  console.log("\u2705 Synced dev client (TemplateProvider, MainActivity, ProjectActivity, DevClientManager)");
2467
2611
  } else {
2468
2612
  for (const f of ["DevClientManager.kt", "DevServerPrefs.kt", "ProjectActivity.kt", "PortraitCaptureActivity.kt", "DevLauncherActivity.kt"]) {
2469
2613
  try {
2470
- fs10.rmSync(path10.join(kotlinDir, f));
2614
+ fs12.rmSync(path12.join(kotlinDir, f));
2471
2615
  } catch {
2472
2616
  }
2473
2617
  }
2474
- let manifest = fs10.readFileSync(manifestPath, "utf-8");
2618
+ let manifest = fs12.readFileSync(manifestPath, "utf-8");
2475
2619
  manifest = manifest.replace(/\s*<activity android:name="\.ProjectActivity"[^\/]*\/>\n?/g, "");
2476
2620
  manifest = manifest.replace(/\s*<activity android:name="\.PortraitCaptureActivity"[^\/]*\/>\n?/g, "");
2477
2621
  const mainActivityTag = manifest.match(/<activity[^>]*android:name="\.MainActivity"[^>]*>/);
@@ -2481,7 +2625,7 @@ $1$2`);
2481
2625
  '$1 android:windowSoftInputMode="adjustResize"$2'
2482
2626
  );
2483
2627
  }
2484
- fs10.writeFileSync(manifestPath, manifest);
2628
+ fs12.writeFileSync(manifestPath, manifest);
2485
2629
  console.log("\u2705 Synced (dev client disabled - use -d for debug build with dev client)");
2486
2630
  }
2487
2631
  }
@@ -2505,15 +2649,15 @@ async function bundleAndDeploy(opts = {}) {
2505
2649
  await syncDevClient_default({ includeDevClient });
2506
2650
  const iconPaths = resolveIconPaths(projectRoot, resolved.config);
2507
2651
  if (iconPaths) {
2508
- const resDir = path11.join(resolved.androidAppDir, "src", "main", "res");
2652
+ const resDir = path13.join(resolved.androidAppDir, "src", "main", "res");
2509
2653
  if (applyAndroidLauncherIcons(resDir, iconPaths)) {
2510
2654
  console.log("\u2705 Synced Android launcher icon(s) from tamer.config.json");
2511
- ensureAndroidManifestLauncherIcon(path11.join(resolved.androidAppDir, "src", "main", "AndroidManifest.xml"));
2655
+ ensureAndroidManifestLauncherIcon(path13.join(resolved.androidAppDir, "src", "main", "AndroidManifest.xml"));
2512
2656
  }
2513
2657
  }
2514
2658
  try {
2515
- const lynxTsconfig = path11.join(lynxProjectDir, "tsconfig.json");
2516
- if (fs11.existsSync(lynxTsconfig)) {
2659
+ const lynxTsconfig = path13.join(lynxProjectDir, "tsconfig.json");
2660
+ if (fs13.existsSync(lynxTsconfig)) {
2517
2661
  fixTsconfigReferencesForBuild(lynxTsconfig);
2518
2662
  }
2519
2663
  console.log("\u{1F4E6} Building Lynx bundle...");
@@ -2523,8 +2667,8 @@ async function bundleAndDeploy(opts = {}) {
2523
2667
  console.error("\u274C Build process failed.");
2524
2668
  process.exit(1);
2525
2669
  }
2526
- if (includeDevClient && devClientBundlePath && !fs11.existsSync(devClientBundlePath)) {
2527
- const devClientDir = path11.dirname(path11.dirname(devClientBundlePath));
2670
+ if (includeDevClient && devClientBundlePath && !fs13.existsSync(devClientBundlePath)) {
2671
+ const devClientDir = path13.dirname(path13.dirname(devClientBundlePath));
2528
2672
  try {
2529
2673
  console.log("\u{1F4E6} Building dev launcher (tamer-dev-client)...");
2530
2674
  execSync3("npm run build", { stdio: "inherit", cwd: devClientDir });
@@ -2535,22 +2679,22 @@ async function bundleAndDeploy(opts = {}) {
2535
2679
  }
2536
2680
  }
2537
2681
  try {
2538
- fs11.mkdirSync(destinationDir, { recursive: true });
2682
+ fs13.mkdirSync(destinationDir, { recursive: true });
2539
2683
  if (release) {
2540
- const devClientAsset = path11.join(destinationDir, "dev-client.lynx.bundle");
2541
- if (fs11.existsSync(devClientAsset)) {
2542
- fs11.rmSync(devClientAsset);
2684
+ const devClientAsset = path13.join(destinationDir, "dev-client.lynx.bundle");
2685
+ if (fs13.existsSync(devClientAsset)) {
2686
+ fs13.rmSync(devClientAsset);
2543
2687
  console.log(`\u2728 Removed dev-client.lynx.bundle from assets (production build)`);
2544
2688
  }
2545
- } else if (includeDevClient && devClientBundlePath && fs11.existsSync(devClientBundlePath)) {
2546
- fs11.copyFileSync(devClientBundlePath, path11.join(destinationDir, "dev-client.lynx.bundle"));
2689
+ } else if (includeDevClient && devClientBundlePath && fs13.existsSync(devClientBundlePath)) {
2690
+ fs13.copyFileSync(devClientBundlePath, path13.join(destinationDir, "dev-client.lynx.bundle"));
2547
2691
  console.log(`\u2728 Copied dev-client.lynx.bundle to assets`);
2548
2692
  }
2549
- if (!fs11.existsSync(lynxBundlePath)) {
2693
+ if (!fs13.existsSync(lynxBundlePath)) {
2550
2694
  console.error(`\u274C Build output not found at: ${lynxBundlePath}`);
2551
2695
  process.exit(1);
2552
2696
  }
2553
- const distDir = path11.dirname(lynxBundlePath);
2697
+ const distDir = path13.dirname(lynxBundlePath);
2554
2698
  copyDistAssets(distDir, destinationDir, resolved.lynxBundleFile);
2555
2699
  console.log(`\u2728 Copied ${resolved.lynxBundleFile} to assets`);
2556
2700
  } catch (error) {
@@ -2561,7 +2705,7 @@ async function bundleAndDeploy(opts = {}) {
2561
2705
  var bundle_default = bundleAndDeploy;
2562
2706
 
2563
2707
  // src/android/build.ts
2564
- import path12 from "path";
2708
+ import path14 from "path";
2565
2709
  import { execSync as execSync4 } from "child_process";
2566
2710
  async function buildApk(opts = {}) {
2567
2711
  let resolved;
@@ -2572,12 +2716,17 @@ async function buildApk(opts = {}) {
2572
2716
  }
2573
2717
  const release = opts.release === true || opts.production === true;
2574
2718
  await bundle_default({ release, production: opts.production });
2575
- const androidDir = resolved.androidDir;
2576
- const gradlew = path12.join(androidDir, process.platform === "win32" ? "gradlew.bat" : "gradlew");
2719
+ const { androidDir, projectRoot } = resolved;
2720
+ cleanTamerAndroidLibBuildsIfVersionsChanged(projectRoot);
2721
+ const gradlew = path14.join(androidDir, process.platform === "win32" ? "gradlew.bat" : "gradlew");
2577
2722
  const variant = release ? "Release" : "Debug";
2578
2723
  const task = opts.install ? `install${variant}` : `assemble${variant}`;
2579
2724
  console.log(`
2580
2725
  \u{1F528} Building ${variant.toLowerCase()} APK${opts.install ? " and installing" : ""}...`);
2726
+ if (opts.clean === true) {
2727
+ console.log("\u2139\uFE0F Running Gradle clean (--clean)...");
2728
+ execSync4(`"${gradlew}" clean`, { stdio: "inherit", cwd: androidDir });
2729
+ }
2581
2730
  execSync4(`"${gradlew}" ${task}`, { stdio: "inherit", cwd: androidDir });
2582
2731
  console.log(`\u2705 APK ${opts.install ? "installed" : "built"} successfully.`);
2583
2732
  if (opts.install) {
@@ -2598,13 +2747,13 @@ async function buildApk(opts = {}) {
2598
2747
  var build_default = buildApk;
2599
2748
 
2600
2749
  // src/ios/create.ts
2601
- import fs13 from "fs";
2602
- import path14 from "path";
2750
+ import fs15 from "fs";
2751
+ import path16 from "path";
2603
2752
 
2604
2753
  // src/ios/getPod.ts
2605
2754
  import { execSync as execSync5 } from "child_process";
2606
- import fs12 from "fs";
2607
- import path13 from "path";
2755
+ import fs14 from "fs";
2756
+ import path15 from "path";
2608
2757
  function isCocoaPodsInstalled() {
2609
2758
  try {
2610
2759
  execSync5("command -v pod >/dev/null 2>&1");
@@ -2626,8 +2775,8 @@ async function setupCocoaPods(rootDir) {
2626
2775
  }
2627
2776
  try {
2628
2777
  console.log("\u{1F4E6} CocoaPods is installed. Proceeding with dependency installation...");
2629
- const podfilePath = path13.join(rootDir, "Podfile");
2630
- if (!fs12.existsSync(podfilePath)) {
2778
+ const podfilePath = path15.join(rootDir, "Podfile");
2779
+ if (!fs14.existsSync(podfilePath)) {
2631
2780
  throw new Error(`Podfile not found at ${podfilePath}`);
2632
2781
  }
2633
2782
  console.log(`\u{1F680} Executing pod install in: ${rootDir}`);
@@ -2653,7 +2802,7 @@ async function setupCocoaPods(rootDir) {
2653
2802
  // src/ios/create.ts
2654
2803
  import { randomBytes } from "crypto";
2655
2804
  function readAndSubstituteTemplate3(templatePath, vars) {
2656
- const raw = fs13.readFileSync(templatePath, "utf-8");
2805
+ const raw = fs15.readFileSync(templatePath, "utf-8");
2657
2806
  return Object.entries(vars).reduce(
2658
2807
  (s, [k, v]) => s.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), v),
2659
2808
  raw
@@ -2676,17 +2825,17 @@ var create2 = () => {
2676
2825
  process.exit(1);
2677
2826
  }
2678
2827
  const iosDir = config.paths?.iosDir ?? "ios";
2679
- const rootDir = path14.join(process.cwd(), iosDir);
2680
- const projectDir = path14.join(rootDir, appName);
2681
- const xcodeprojDir = path14.join(rootDir, `${appName}.xcodeproj`);
2828
+ const rootDir = path16.join(process.cwd(), iosDir);
2829
+ const projectDir = path16.join(rootDir, appName);
2830
+ const xcodeprojDir = path16.join(rootDir, `${appName}.xcodeproj`);
2682
2831
  const bridgingHeader = `${appName}-Bridging-Header.h`;
2683
2832
  function writeFile2(filePath, content) {
2684
- fs13.mkdirSync(path14.dirname(filePath), { recursive: true });
2685
- fs13.writeFileSync(filePath, content.trimStart(), "utf8");
2833
+ fs15.mkdirSync(path16.dirname(filePath), { recursive: true });
2834
+ fs15.writeFileSync(filePath, content.trimStart(), "utf8");
2686
2835
  }
2687
- if (fs13.existsSync(rootDir)) {
2836
+ if (fs15.existsSync(rootDir)) {
2688
2837
  console.log(`\u{1F9F9} Removing existing directory: ${rootDir}`);
2689
- fs13.rmSync(rootDir, { recursive: true, force: true });
2838
+ fs15.rmSync(rootDir, { recursive: true, force: true });
2690
2839
  }
2691
2840
  console.log(`\u{1F680} Creating a new Tamer4Lynx project in: ${rootDir}`);
2692
2841
  const ids = {
@@ -2722,9 +2871,11 @@ var create2 = () => {
2722
2871
  targetDebugConfig: generateId(),
2723
2872
  targetReleaseConfig: generateId()
2724
2873
  };
2725
- writeFile2(path14.join(rootDir, "Podfile"), `
2874
+ writeFile2(path16.join(rootDir, "Podfile"), `
2726
2875
  source 'https://cdn.cocoapods.org/'
2727
2876
 
2877
+ install! 'cocoapods', :incremental_installation => true, :generate_multiple_pod_projects => true
2878
+
2728
2879
  platform :ios, '13.0'
2729
2880
 
2730
2881
  target '${appName}' do
@@ -2807,15 +2958,15 @@ end
2807
2958
  const hostPkg = findTamerHostPackage(process.cwd());
2808
2959
  const templateVars = { PACKAGE_NAME: bundleId, APP_NAME: appName, BUNDLE_ID: bundleId };
2809
2960
  if (hostPkg) {
2810
- const templateDir = path14.join(hostPkg, "ios", "templates");
2961
+ const templateDir = path16.join(hostPkg, "ios", "templates");
2811
2962
  for (const f of ["AppDelegate.swift", "SceneDelegate.swift", "ViewController.swift", "LynxProvider.swift", "LynxInitProcessor.swift"]) {
2812
- const srcPath = path14.join(templateDir, f);
2813
- if (fs13.existsSync(srcPath)) {
2814
- writeFile2(path14.join(projectDir, f), readAndSubstituteTemplate3(srcPath, templateVars));
2963
+ const srcPath = path16.join(templateDir, f);
2964
+ if (fs15.existsSync(srcPath)) {
2965
+ writeFile2(path16.join(projectDir, f), readAndSubstituteTemplate3(srcPath, templateVars));
2815
2966
  }
2816
2967
  }
2817
2968
  } else {
2818
- writeFile2(path14.join(projectDir, "AppDelegate.swift"), `
2969
+ writeFile2(path16.join(projectDir, "AppDelegate.swift"), `
2819
2970
  import UIKit
2820
2971
 
2821
2972
  @UIApplicationMain
@@ -2830,7 +2981,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
2830
2981
  }
2831
2982
  }
2832
2983
  `);
2833
- writeFile2(path14.join(projectDir, "SceneDelegate.swift"), `
2984
+ writeFile2(path16.join(projectDir, "SceneDelegate.swift"), `
2834
2985
  import UIKit
2835
2986
 
2836
2987
  class SceneDelegate: UIResponder, UIWindowSceneDelegate {
@@ -2844,7 +2995,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
2844
2995
  }
2845
2996
  }
2846
2997
  `);
2847
- writeFile2(path14.join(projectDir, "ViewController.swift"), `
2998
+ writeFile2(path16.join(projectDir, "ViewController.swift"), `
2848
2999
  import UIKit
2849
3000
  import Lynx
2850
3001
  import tamerinsets
@@ -2917,7 +3068,7 @@ class ViewController: UIViewController {
2917
3068
  }
2918
3069
  }
2919
3070
  `);
2920
- writeFile2(path14.join(projectDir, "LynxProvider.swift"), `
3071
+ writeFile2(path16.join(projectDir, "LynxProvider.swift"), `
2921
3072
  import Foundation
2922
3073
 
2923
3074
  class LynxProvider: NSObject, LynxTemplateProvider {
@@ -2936,7 +3087,7 @@ class LynxProvider: NSObject, LynxTemplateProvider {
2936
3087
  }
2937
3088
  }
2938
3089
  `);
2939
- writeFile2(path14.join(projectDir, "LynxInitProcessor.swift"), `
3090
+ writeFile2(path16.join(projectDir, "LynxInitProcessor.swift"), `
2940
3091
  // Copyright 2024 The Lynx Authors. All rights reserved.
2941
3092
  // Licensed under the Apache License Version 2.0 that can be found in the
2942
3093
  // LICENSE file in the root directory of this source tree.
@@ -2976,7 +3127,7 @@ final class LynxInitProcessor {
2976
3127
  }
2977
3128
  `);
2978
3129
  }
2979
- writeFile2(path14.join(projectDir, bridgingHeader), `
3130
+ writeFile2(path16.join(projectDir, bridgingHeader), `
2980
3131
  #import <Lynx/LynxConfig.h>
2981
3132
  #import <Lynx/LynxEnv.h>
2982
3133
  #import <Lynx/LynxTemplateProvider.h>
@@ -2985,7 +3136,7 @@ final class LynxInitProcessor {
2985
3136
  #import <SDWebImage/SDWebImage.h>
2986
3137
  #import <SDWebImageWebPCoder/SDWebImageWebPCoder.h>
2987
3138
  `);
2988
- writeFile2(path14.join(projectDir, "Info.plist"), `
3139
+ writeFile2(path16.join(projectDir, "Info.plist"), `
2989
3140
  <?xml version="1.0" encoding="UTF-8"?>
2990
3141
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2991
3142
  <plist version="1.0">
@@ -3043,21 +3194,21 @@ final class LynxInitProcessor {
3043
3194
  </dict>
3044
3195
  </plist>
3045
3196
  `);
3046
- const appIconDir = path14.join(projectDir, "Assets.xcassets", "AppIcon.appiconset");
3047
- fs13.mkdirSync(appIconDir, { recursive: true });
3197
+ const appIconDir = path16.join(projectDir, "Assets.xcassets", "AppIcon.appiconset");
3198
+ fs15.mkdirSync(appIconDir, { recursive: true });
3048
3199
  const iconPaths = resolveIconPaths(process.cwd(), config);
3049
3200
  if (applyIosAppIconAssets(appIconDir, iconPaths)) {
3050
3201
  console.log(iconPaths?.ios ? "\u2705 Copied iOS icon from tamer.config.json icon.ios" : "\u2705 Copied app icon from tamer.config.json icon.source");
3051
3202
  } else {
3052
- writeFile2(path14.join(appIconDir, "Contents.json"), `
3203
+ writeFile2(path16.join(appIconDir, "Contents.json"), `
3053
3204
  {
3054
3205
  "images" : [ { "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ],
3055
3206
  "info" : { "author" : "xcode", "version" : 1 }
3056
3207
  }
3057
3208
  `);
3058
3209
  }
3059
- fs13.mkdirSync(xcodeprojDir, { recursive: true });
3060
- writeFile2(path14.join(xcodeprojDir, "project.pbxproj"), `
3210
+ fs15.mkdirSync(xcodeprojDir, { recursive: true });
3211
+ writeFile2(path16.join(xcodeprojDir, "project.pbxproj"), `
3061
3212
  // !$*UTF8*$!
3062
3213
  {
3063
3214
  archiveVersion = 1;
@@ -3343,8 +3494,8 @@ final class LynxInitProcessor {
3343
3494
  var create_default2 = create2;
3344
3495
 
3345
3496
  // src/ios/autolink.ts
3346
- import fs15 from "fs";
3347
- import path16 from "path";
3497
+ import fs17 from "fs";
3498
+ import path18 from "path";
3348
3499
  import { execSync as execSync6 } from "child_process";
3349
3500
 
3350
3501
  // src/common/hostNativeModulesManifest.ts
@@ -3355,8 +3506,8 @@ function buildHostNativeModulesManifestJson(moduleClassNames) {
3355
3506
  }
3356
3507
 
3357
3508
  // src/ios/syncHost.ts
3358
- import fs14 from "fs";
3359
- import path15 from "path";
3509
+ import fs16 from "fs";
3510
+ import path17 from "path";
3360
3511
  import crypto from "crypto";
3361
3512
  function deterministicUUID(seed) {
3362
3513
  return crypto.createHash("sha256").update(seed).digest("hex").substring(0, 24).toUpperCase();
@@ -3404,7 +3555,7 @@ function getLaunchScreenStoryboard() {
3404
3555
  `;
3405
3556
  }
3406
3557
  function addLaunchScreenToXcodeProject(pbxprojPath, appName) {
3407
- let content = fs14.readFileSync(pbxprojPath, "utf8");
3558
+ let content = fs16.readFileSync(pbxprojPath, "utf8");
3408
3559
  if (content.includes("LaunchScreen.storyboard")) return;
3409
3560
  const baseFileRefUUID = deterministicUUID(`launchScreenBase:${appName}`);
3410
3561
  const variantGroupUUID = deterministicUUID(`launchScreenGroup:${appName}`);
@@ -3441,11 +3592,11 @@ function addLaunchScreenToXcodeProject(pbxprojPath, appName) {
3441
3592
  );
3442
3593
  content = content.replace(groupPattern, `$1
3443
3594
  ${variantGroupUUID} /* LaunchScreen.storyboard */,`);
3444
- fs14.writeFileSync(pbxprojPath, content, "utf8");
3595
+ fs16.writeFileSync(pbxprojPath, content, "utf8");
3445
3596
  console.log("\u2705 Registered LaunchScreen.storyboard in Xcode project");
3446
3597
  }
3447
3598
  function addSwiftSourceToXcodeProject(pbxprojPath, appName, filename) {
3448
- let content = fs14.readFileSync(pbxprojPath, "utf8");
3599
+ let content = fs16.readFileSync(pbxprojPath, "utf8");
3449
3600
  const escaped = filename.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3450
3601
  if (new RegExp(`path = "?${escaped}"?;`).test(content)) return;
3451
3602
  const fileRefUUID = deterministicUUID(`fileRef:${appName}:${filename}`);
@@ -3470,11 +3621,11 @@ function addSwiftSourceToXcodeProject(pbxprojPath, appName, filename) {
3470
3621
  );
3471
3622
  content = content.replace(groupPattern, `$1
3472
3623
  ${fileRefUUID} /* ${filename} */,`);
3473
- fs14.writeFileSync(pbxprojPath, content, "utf8");
3624
+ fs16.writeFileSync(pbxprojPath, content, "utf8");
3474
3625
  console.log(`\u2705 Registered ${filename} in Xcode project sources`);
3475
3626
  }
3476
3627
  function addResourceToXcodeProject(pbxprojPath, appName, filename) {
3477
- let content = fs14.readFileSync(pbxprojPath, "utf8");
3628
+ let content = fs16.readFileSync(pbxprojPath, "utf8");
3478
3629
  const escaped = filename.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3479
3630
  if (new RegExp(`path = "?${escaped}"?;`).test(content)) return;
3480
3631
  const fileRefUUID = deterministicUUID(`fileRef:${appName}:${filename}`);
@@ -3499,12 +3650,12 @@ function addResourceToXcodeProject(pbxprojPath, appName, filename) {
3499
3650
  );
3500
3651
  content = content.replace(groupPattern, `$1
3501
3652
  ${fileRefUUID} /* ${filename} */,`);
3502
- fs14.writeFileSync(pbxprojPath, content, "utf8");
3653
+ fs16.writeFileSync(pbxprojPath, content, "utf8");
3503
3654
  console.log(`\u2705 Registered ${filename} in Xcode project resources`);
3504
3655
  }
3505
3656
  function writeFile(filePath, content) {
3506
- fs14.mkdirSync(path15.dirname(filePath), { recursive: true });
3507
- fs14.writeFileSync(filePath, content, "utf8");
3657
+ fs16.mkdirSync(path17.dirname(filePath), { recursive: true });
3658
+ fs16.writeFileSync(filePath, content, "utf8");
3508
3659
  }
3509
3660
  function getAppDelegateSwift() {
3510
3661
  return `import UIKit
@@ -3721,8 +3872,8 @@ class ViewController: UIViewController {
3721
3872
  `;
3722
3873
  }
3723
3874
  function patchInfoPlist(infoPlistPath) {
3724
- if (!fs14.existsSync(infoPlistPath)) return;
3725
- let content = fs14.readFileSync(infoPlistPath, "utf8");
3875
+ if (!fs16.existsSync(infoPlistPath)) return;
3876
+ let content = fs16.readFileSync(infoPlistPath, "utf8");
3726
3877
  content = content.replace(/\s*<key>UIMainStoryboardFile<\/key>\s*<string>[^<]*<\/string>/g, "");
3727
3878
  if (!content.includes("UILaunchStoryboardName")) {
3728
3879
  content = content.replace("</dict>\n</plist>", ` <key>UILaunchStoryboardName</key>
@@ -3754,7 +3905,7 @@ function patchInfoPlist(infoPlistPath) {
3754
3905
  </plist>`);
3755
3906
  console.log("\u2705 Added UIApplicationSceneManifest to Info.plist");
3756
3907
  }
3757
- fs14.writeFileSync(infoPlistPath, content, "utf8");
3908
+ fs16.writeFileSync(infoPlistPath, content, "utf8");
3758
3909
  }
3759
3910
  function getSimpleLynxProviderSwift() {
3760
3911
  return `import Foundation
@@ -3779,9 +3930,9 @@ class LynxProvider: NSObject, LynxTemplateProvider {
3779
3930
  }
3780
3931
  function readTemplateOrFallback(devClientPkg, templateName, fallback, vars = {}) {
3781
3932
  if (devClientPkg) {
3782
- const tplPath = path15.join(devClientPkg, "ios", "templates", templateName);
3783
- if (fs14.existsSync(tplPath)) {
3784
- let content = fs14.readFileSync(tplPath, "utf8");
3933
+ const tplPath = path17.join(devClientPkg, "ios", "templates", templateName);
3934
+ if (fs16.existsSync(tplPath)) {
3935
+ let content = fs16.readFileSync(tplPath, "utf8");
3785
3936
  for (const [k, v] of Object.entries(vars)) {
3786
3937
  content = content.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), v);
3787
3938
  }
@@ -3799,19 +3950,19 @@ function syncHostIos(opts) {
3799
3950
  if (!appName) {
3800
3951
  throw new Error('"ios.appName" must be defined in tamer.config.json');
3801
3952
  }
3802
- const projectDir = path15.join(resolved.iosDir, appName);
3803
- const infoPlistPath = path15.join(projectDir, "Info.plist");
3804
- if (!fs14.existsSync(projectDir)) {
3953
+ const projectDir = path17.join(resolved.iosDir, appName);
3954
+ const infoPlistPath = path17.join(projectDir, "Info.plist");
3955
+ if (!fs16.existsSync(projectDir)) {
3805
3956
  throw new Error(`iOS project not found at ${projectDir}. Run \`tamer ios create\` first.`);
3806
3957
  }
3807
- const pbxprojPath = path15.join(resolved.iosDir, `${appName}.xcodeproj`, "project.pbxproj");
3808
- const baseLprojDir = path15.join(projectDir, "Base.lproj");
3809
- const launchScreenPath = path15.join(baseLprojDir, "LaunchScreen.storyboard");
3958
+ const pbxprojPath = path17.join(resolved.iosDir, `${appName}.xcodeproj`, "project.pbxproj");
3959
+ const baseLprojDir = path17.join(projectDir, "Base.lproj");
3960
+ const launchScreenPath = path17.join(baseLprojDir, "LaunchScreen.storyboard");
3810
3961
  patchInfoPlist(infoPlistPath);
3811
- writeFile(path15.join(projectDir, "AppDelegate.swift"), getAppDelegateSwift());
3812
- writeFile(path15.join(projectDir, "SceneDelegate.swift"), getSceneDelegateSwift());
3813
- if (!fs14.existsSync(launchScreenPath)) {
3814
- fs14.mkdirSync(baseLprojDir, { recursive: true });
3962
+ writeFile(path17.join(projectDir, "AppDelegate.swift"), getAppDelegateSwift());
3963
+ writeFile(path17.join(projectDir, "SceneDelegate.swift"), getSceneDelegateSwift());
3964
+ if (!fs16.existsSync(launchScreenPath)) {
3965
+ fs16.mkdirSync(baseLprojDir, { recursive: true });
3815
3966
  writeFile(launchScreenPath, getLaunchScreenStoryboard());
3816
3967
  addLaunchScreenToXcodeProject(pbxprojPath, appName);
3817
3968
  }
@@ -3820,33 +3971,33 @@ function syncHostIos(opts) {
3820
3971
  const devClientPkg2 = findDevClientPackage(resolved.projectRoot);
3821
3972
  const segment = resolved.lynxProjectDir.split("/").filter(Boolean).pop() ?? "";
3822
3973
  const tplVars = { PROJECT_BUNDLE_SEGMENT: segment };
3823
- writeFile(path15.join(projectDir, "ViewController.swift"), getDevViewControllerSwift());
3824
- writeFile(path15.join(projectDir, "LynxProvider.swift"), getSimpleLynxProviderSwift());
3974
+ writeFile(path17.join(projectDir, "ViewController.swift"), getDevViewControllerSwift());
3975
+ writeFile(path17.join(projectDir, "LynxProvider.swift"), getSimpleLynxProviderSwift());
3825
3976
  addSwiftSourceToXcodeProject(pbxprojPath, appName, "LynxProvider.swift");
3826
3977
  const devTPContent = readTemplateOrFallback(devClientPkg2, "DevTemplateProvider.swift", "", tplVars);
3827
3978
  if (devTPContent) {
3828
- writeFile(path15.join(projectDir, "DevTemplateProvider.swift"), devTPContent);
3979
+ writeFile(path17.join(projectDir, "DevTemplateProvider.swift"), devTPContent);
3829
3980
  addSwiftSourceToXcodeProject(pbxprojPath, appName, "DevTemplateProvider.swift");
3830
3981
  }
3831
3982
  const projectVCContent = readTemplateOrFallback(devClientPkg2, "ProjectViewController.swift", "", tplVars);
3832
3983
  if (projectVCContent) {
3833
- writeFile(path15.join(projectDir, "ProjectViewController.swift"), projectVCContent);
3984
+ writeFile(path17.join(projectDir, "ProjectViewController.swift"), projectVCContent);
3834
3985
  addSwiftSourceToXcodeProject(pbxprojPath, appName, "ProjectViewController.swift");
3835
3986
  }
3836
3987
  const devCMContent = readTemplateOrFallback(devClientPkg2, "DevClientManager.swift", "", tplVars);
3837
3988
  if (devCMContent) {
3838
- writeFile(path15.join(projectDir, "DevClientManager.swift"), devCMContent);
3989
+ writeFile(path17.join(projectDir, "DevClientManager.swift"), devCMContent);
3839
3990
  addSwiftSourceToXcodeProject(pbxprojPath, appName, "DevClientManager.swift");
3840
3991
  }
3841
3992
  const qrContent = readTemplateOrFallback(devClientPkg2, "QRScannerViewController.swift", "", tplVars);
3842
3993
  if (qrContent) {
3843
- writeFile(path15.join(projectDir, "QRScannerViewController.swift"), qrContent);
3994
+ writeFile(path17.join(projectDir, "QRScannerViewController.swift"), qrContent);
3844
3995
  addSwiftSourceToXcodeProject(pbxprojPath, appName, "QRScannerViewController.swift");
3845
3996
  }
3846
3997
  console.log("\u2705 Synced iOS host app (embedded dev mode) \u2014 ViewController, DevTemplateProvider, ProjectViewController, DevClientManager, QRScannerViewController");
3847
3998
  } else {
3848
- writeFile(path15.join(projectDir, "ViewController.swift"), getViewControllerSwift());
3849
- writeFile(path15.join(projectDir, "LynxProvider.swift"), getSimpleLynxProviderSwift());
3999
+ writeFile(path17.join(projectDir, "ViewController.swift"), getViewControllerSwift());
4000
+ writeFile(path17.join(projectDir, "LynxProvider.swift"), getSimpleLynxProviderSwift());
3850
4001
  addSwiftSourceToXcodeProject(pbxprojPath, appName, "LynxProvider.swift");
3851
4002
  console.log("\u2705 Synced iOS host app controller files");
3852
4003
  }
@@ -3854,7 +4005,7 @@ function syncHostIos(opts) {
3854
4005
  var syncHost_default = syncHostIos;
3855
4006
 
3856
4007
  // src/ios/autolink.ts
3857
- var autolink2 = () => {
4008
+ var autolink2 = (syncHostOpts) => {
3858
4009
  let resolved;
3859
4010
  try {
3860
4011
  resolved = resolveHostPaths();
@@ -3865,11 +4016,11 @@ var autolink2 = () => {
3865
4016
  const projectRoot = resolved.projectRoot;
3866
4017
  const iosProjectPath = resolved.iosDir;
3867
4018
  function updateGeneratedSection(filePath, newContent, startMarker, endMarker) {
3868
- if (!fs15.existsSync(filePath)) {
4019
+ if (!fs17.existsSync(filePath)) {
3869
4020
  console.warn(`\u26A0\uFE0F File not found, skipping update: ${filePath}`);
3870
4021
  return;
3871
4022
  }
3872
- let fileContent = fs15.readFileSync(filePath, "utf8");
4023
+ let fileContent = fs17.readFileSync(filePath, "utf8");
3873
4024
  const escapedStartMarker = startMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3874
4025
  const escapedEndMarker = endMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3875
4026
  const regex = new RegExp(`${escapedStartMarker}[\\s\\S]*?${escapedEndMarker}`, "g");
@@ -3889,33 +4040,33 @@ ${replacementBlock}
3889
4040
  `;
3890
4041
  }
3891
4042
  } else {
3892
- console.warn(`\u26A0\uFE0F Could not find autolink markers in ${path16.basename(filePath)}. Appending to the end of the file.`);
4043
+ console.warn(`\u26A0\uFE0F Could not find autolink markers in ${path18.basename(filePath)}. Appending to the end of the file.`);
3893
4044
  fileContent += `
3894
4045
  ${replacementBlock}
3895
4046
  `;
3896
4047
  }
3897
- fs15.writeFileSync(filePath, fileContent, "utf8");
3898
- console.log(`\u2705 Updated autolinked section in ${path16.basename(filePath)}`);
4048
+ fs17.writeFileSync(filePath, fileContent, "utf8");
4049
+ console.log(`\u2705 Updated autolinked section in ${path18.basename(filePath)}`);
3899
4050
  }
3900
4051
  function resolvePodDirectory(pkg) {
3901
- const configuredDir = path16.join(pkg.packagePath, pkg.config.ios?.podspecPath || ".");
3902
- if (fs15.existsSync(configuredDir)) {
4052
+ const configuredDir = path18.join(pkg.packagePath, pkg.config.ios?.podspecPath || ".");
4053
+ if (fs17.existsSync(configuredDir)) {
3903
4054
  return configuredDir;
3904
4055
  }
3905
- const iosDir = path16.join(pkg.packagePath, "ios");
3906
- if (fs15.existsSync(iosDir)) {
4056
+ const iosDir = path18.join(pkg.packagePath, "ios");
4057
+ if (fs17.existsSync(iosDir)) {
3907
4058
  const stack = [iosDir];
3908
4059
  while (stack.length > 0) {
3909
4060
  const current = stack.pop();
3910
4061
  try {
3911
- const entries = fs15.readdirSync(current, { withFileTypes: true });
4062
+ const entries = fs17.readdirSync(current, { withFileTypes: true });
3912
4063
  const podspec = entries.find((entry) => entry.isFile() && entry.name.endsWith(".podspec"));
3913
4064
  if (podspec) {
3914
4065
  return current;
3915
4066
  }
3916
4067
  for (const entry of entries) {
3917
4068
  if (entry.isDirectory()) {
3918
- stack.push(path16.join(current, entry.name));
4069
+ stack.push(path18.join(current, entry.name));
3919
4070
  }
3920
4071
  }
3921
4072
  } catch {
@@ -3926,9 +4077,9 @@ ${replacementBlock}
3926
4077
  }
3927
4078
  function resolvePodName(pkg) {
3928
4079
  const fullPodspecDir = resolvePodDirectory(pkg);
3929
- if (fs15.existsSync(fullPodspecDir)) {
4080
+ if (fs17.existsSync(fullPodspecDir)) {
3930
4081
  try {
3931
- const files = fs15.readdirSync(fullPodspecDir);
4082
+ const files = fs17.readdirSync(fullPodspecDir);
3932
4083
  const podspecFile = files.find((f) => f.endsWith(".podspec"));
3933
4084
  if (podspecFile) return podspecFile.replace(".podspec", "");
3934
4085
  } catch {
@@ -3937,13 +4088,13 @@ ${replacementBlock}
3937
4088
  return pkg.name.split("/").pop().replace(/-/g, "");
3938
4089
  }
3939
4090
  function updatePodfile(packages) {
3940
- const podfilePath = path16.join(iosProjectPath, "Podfile");
4091
+ const podfilePath = path18.join(iosProjectPath, "Podfile");
3941
4092
  let scriptContent = ` # This section is automatically generated by Tamer4Lynx.
3942
4093
  # Manual edits will be overwritten.`;
3943
4094
  const iosPackages = packages.filter((p) => p.config.ios);
3944
4095
  if (iosPackages.length > 0) {
3945
4096
  iosPackages.forEach((pkg) => {
3946
- const relativePath = path16.relative(iosProjectPath, resolvePodDirectory(pkg));
4097
+ const relativePath = path18.relative(iosProjectPath, resolvePodDirectory(pkg));
3947
4098
  const podName = resolvePodName(pkg);
3948
4099
  scriptContent += `
3949
4100
  pod '${podName}', :path => '${relativePath}'`;
@@ -3955,9 +4106,9 @@ ${replacementBlock}
3955
4106
  updateGeneratedSection(podfilePath, scriptContent.trim(), "# GENERATED AUTOLINK DEPENDENCIES START", "# GENERATED AUTOLINK DEPENDENCIES END");
3956
4107
  }
3957
4108
  function ensureXElementPod() {
3958
- const podfilePath = path16.join(iosProjectPath, "Podfile");
3959
- if (!fs15.existsSync(podfilePath)) return;
3960
- let content = fs15.readFileSync(podfilePath, "utf8");
4109
+ const podfilePath = path18.join(iosProjectPath, "Podfile");
4110
+ if (!fs17.existsSync(podfilePath)) return;
4111
+ let content = fs17.readFileSync(podfilePath, "utf8");
3961
4112
  if (content.includes("pod 'XElement'")) return;
3962
4113
  const lynxVersionMatch = content.match(/pod\s+'Lynx',\s*'([^']+)'/);
3963
4114
  const lynxVersion = lynxVersionMatch?.[1] ?? "3.6.0";
@@ -3982,13 +4133,51 @@ ${replacementBlock}
3982
4133
  `;
3983
4134
  }
3984
4135
  }
3985
- fs15.writeFileSync(podfilePath, content, "utf8");
4136
+ fs17.writeFileSync(podfilePath, content, "utf8");
3986
4137
  console.log(`\u2705 Added XElement pod (v${lynxVersion}) to Podfile`);
3987
4138
  }
4139
+ function ensureLynxDevToolPods(packages) {
4140
+ const hasDevClient = packages.some(
4141
+ (p) => p.name === "@tamer4lynx/tamer-dev-client" || p.name === "tamer-dev-client"
4142
+ );
4143
+ if (!hasDevClient) return;
4144
+ const podfilePath = path18.join(iosProjectPath, "Podfile");
4145
+ if (!fs17.existsSync(podfilePath)) return;
4146
+ let content = fs17.readFileSync(podfilePath, "utf8");
4147
+ if (content.includes("pod 'LynxDevtool'") || content.includes('pod "LynxDevtool"')) return;
4148
+ const lynxVersionMatch = content.match(/pod\s+'Lynx',\s*'([^']+)'/);
4149
+ const lynxVersion = lynxVersionMatch?.[1] ?? "3.6.0";
4150
+ if (!content.includes("'Devtool'") && !content.includes('"Devtool"')) {
4151
+ content = content.replace(
4152
+ /(\s+'Http',\s*\n)(\s*\])/,
4153
+ "$1 'Devtool',\n$2"
4154
+ );
4155
+ }
4156
+ const devtoolLine = ` pod 'LynxDevtool', '${lynxVersion}'
4157
+
4158
+ `;
4159
+ if (content.includes("# GENERATED AUTOLINK DEPENDENCIES START")) {
4160
+ content = content.replace(/(# GENERATED AUTOLINK DEPENDENCIES START)/, `${devtoolLine}$1`);
4161
+ } else {
4162
+ const insertAfter = /pod\s+'LynxService'[^\n]*(?:\n\s*'[^']*',?\s*)*\]\s*/;
4163
+ const serviceMatch = content.match(insertAfter);
4164
+ if (serviceMatch) {
4165
+ const idx = serviceMatch.index + serviceMatch[0].length;
4166
+ content = content.slice(0, idx) + `
4167
+ pod 'LynxDevtool', '${lynxVersion}'` + content.slice(idx);
4168
+ } else {
4169
+ content += `
4170
+ pod 'LynxDevtool', '${lynxVersion}'
4171
+ `;
4172
+ }
4173
+ }
4174
+ fs17.writeFileSync(podfilePath, content, "utf8");
4175
+ console.log(`\u2705 Added Lynx DevTool pods (v${lynxVersion}) to Podfile`);
4176
+ }
3988
4177
  function ensureLynxPatchInPodfile() {
3989
- const podfilePath = path16.join(iosProjectPath, "Podfile");
3990
- if (!fs15.existsSync(podfilePath)) return;
3991
- let content = fs15.readFileSync(podfilePath, "utf8");
4178
+ const podfilePath = path18.join(iosProjectPath, "Podfile");
4179
+ if (!fs17.existsSync(podfilePath)) return;
4180
+ let content = fs17.readFileSync(podfilePath, "utf8");
3992
4181
  if (content.includes("content.gsub(/\\btypeof\\(/, '__typeof__(')")) return;
3993
4182
  const patch = `
3994
4183
  Dir.glob(File.join(installer.sandbox.root, 'Lynx/platform/darwin/**/*.{m,mm}')).each do |lynx_source|
@@ -4000,13 +4189,13 @@ ${replacementBlock}
4000
4189
  end`;
4001
4190
  content = content.replace(/(\n end\s*\n)(end\s*)$/, `$1${patch}
4002
4191
  $2`);
4003
- fs15.writeFileSync(podfilePath, content, "utf8");
4192
+ fs17.writeFileSync(podfilePath, content, "utf8");
4004
4193
  console.log("\u2705 Added Lynx typeof patch to Podfile post_install.");
4005
4194
  }
4006
4195
  function ensurePodBuildSettings() {
4007
- const podfilePath = path16.join(iosProjectPath, "Podfile");
4008
- if (!fs15.existsSync(podfilePath)) return;
4009
- let content = fs15.readFileSync(podfilePath, "utf8");
4196
+ const podfilePath = path18.join(iosProjectPath, "Podfile");
4197
+ if (!fs17.existsSync(podfilePath)) return;
4198
+ let content = fs17.readFileSync(podfilePath, "utf8");
4010
4199
  let changed = false;
4011
4200
  if (!content.includes("CLANG_ENABLE_EXPLICIT_MODULES")) {
4012
4201
  content = content.replace(
@@ -4049,7 +4238,7 @@ $2`);
4049
4238
  changed = true;
4050
4239
  }
4051
4240
  if (changed) {
4052
- fs15.writeFileSync(podfilePath, content, "utf8");
4241
+ fs17.writeFileSync(podfilePath, content, "utf8");
4053
4242
  console.log("\u2705 Added Xcode compatibility build settings to Podfile post_install.");
4054
4243
  }
4055
4244
  }
@@ -4057,10 +4246,10 @@ $2`);
4057
4246
  const appNameFromConfig = resolved.config.ios?.appName;
4058
4247
  const candidatePaths = [];
4059
4248
  if (appNameFromConfig) {
4060
- candidatePaths.push(path16.join(iosProjectPath, appNameFromConfig, "LynxInitProcessor.swift"));
4249
+ candidatePaths.push(path18.join(iosProjectPath, appNameFromConfig, "LynxInitProcessor.swift"));
4061
4250
  }
4062
- candidatePaths.push(path16.join(iosProjectPath, "LynxInitProcessor.swift"));
4063
- const found = candidatePaths.find((p) => fs15.existsSync(p));
4251
+ candidatePaths.push(path18.join(iosProjectPath, "LynxInitProcessor.swift"));
4252
+ const found = candidatePaths.find((p) => fs17.existsSync(p));
4064
4253
  const lynxInitPath = found ?? candidatePaths[0];
4065
4254
  const iosPackages = packages.filter((p) => getIosModuleClassNames(p.config.ios).length > 0 || Object.keys(getIosElements(p.config.ios)).length > 0);
4066
4255
  const seenModules = /* @__PURE__ */ new Set();
@@ -4099,7 +4288,7 @@ $2`);
4099
4288
  const podName = resolvePodName(pkg);
4100
4289
  return `import ${podName}`;
4101
4290
  }).join("\n");
4102
- const fileContent = fs15.readFileSync(filePath, "utf8");
4291
+ const fileContent = fs17.readFileSync(filePath, "utf8");
4103
4292
  if (fileContent.indexOf(startMarker) !== -1) {
4104
4293
  updateGeneratedSection(filePath, imports, startMarker, endMarker);
4105
4294
  return;
@@ -4136,8 +4325,8 @@ ${after}`;
4136
4325
  ${fileContent}`;
4137
4326
  }
4138
4327
  }
4139
- fs15.writeFileSync(filePath, newContent, "utf8");
4140
- console.log(`\u2705 Updated imports in ${path16.basename(filePath)}`);
4328
+ fs17.writeFileSync(filePath, newContent, "utf8");
4329
+ console.log(`\u2705 Updated imports in ${path18.basename(filePath)}`);
4141
4330
  }
4142
4331
  updateImportsSection(lynxInitPath, importPackages);
4143
4332
  if (importPackages.length === 0) {
@@ -4181,7 +4370,7 @@ ${androidNames.map((n) => ` "${n.replace(/\\/g, "\\\\").replace(/"/g,
4181
4370
  } else {
4182
4371
  devClientSupportedBody = " // @tamer4lynx/tamer-dev-client not linked";
4183
4372
  }
4184
- if (fs15.readFileSync(lynxInitPath, "utf8").includes("GENERATED DEV_CLIENT_SUPPORTED START")) {
4373
+ if (fs17.readFileSync(lynxInitPath, "utf8").includes("GENERATED DEV_CLIENT_SUPPORTED START")) {
4185
4374
  updateGeneratedSection(lynxInitPath, devClientSupportedBody, "// GENERATED DEV_CLIENT_SUPPORTED START", "// GENERATED DEV_CLIENT_SUPPORTED END");
4186
4375
  }
4187
4376
  }
@@ -4189,13 +4378,13 @@ ${androidNames.map((n) => ` "${n.replace(/\\/g, "\\\\").replace(/"/g,
4189
4378
  const appNameFromConfig = resolved.config.ios?.appName;
4190
4379
  const candidates = [];
4191
4380
  if (appNameFromConfig) {
4192
- candidates.push(path16.join(iosProjectPath, appNameFromConfig, "Info.plist"));
4381
+ candidates.push(path18.join(iosProjectPath, appNameFromConfig, "Info.plist"));
4193
4382
  }
4194
- candidates.push(path16.join(iosProjectPath, "Info.plist"));
4195
- return candidates.find((p) => fs15.existsSync(p)) ?? null;
4383
+ candidates.push(path18.join(iosProjectPath, "Info.plist"));
4384
+ return candidates.find((p) => fs17.existsSync(p)) ?? null;
4196
4385
  }
4197
4386
  function readPlistXml(plistPath) {
4198
- return fs15.readFileSync(plistPath, "utf8");
4387
+ return fs17.readFileSync(plistPath, "utf8");
4199
4388
  }
4200
4389
  function syncInfoPlistPermissions(packages) {
4201
4390
  const plistPath = findInfoPlist();
@@ -4226,7 +4415,7 @@ ${androidNames.map((n) => ` "${n.replace(/\\/g, "\\\\").replace(/"/g,
4226
4415
  added++;
4227
4416
  }
4228
4417
  if (added > 0) {
4229
- fs15.writeFileSync(plistPath, plist, "utf8");
4418
+ fs17.writeFileSync(plistPath, plist, "utf8");
4230
4419
  console.log(`\u2705 Synced ${added} Info.plist permission description(s)`);
4231
4420
  }
4232
4421
  }
@@ -4273,16 +4462,16 @@ ${schemesXml}
4273
4462
  $1`
4274
4463
  );
4275
4464
  }
4276
- fs15.writeFileSync(plistPath, plist, "utf8");
4465
+ fs17.writeFileSync(plistPath, plist, "utf8");
4277
4466
  console.log(`\u2705 Synced ${urlSchemes.length} iOS URL scheme(s) into Info.plist`);
4278
4467
  }
4279
4468
  function runPodInstall(forcePath) {
4280
- const podfilePath = forcePath ?? path16.join(iosProjectPath, "Podfile");
4281
- if (!fs15.existsSync(podfilePath)) {
4469
+ const podfilePath = forcePath ?? path18.join(iosProjectPath, "Podfile");
4470
+ if (!fs17.existsSync(podfilePath)) {
4282
4471
  console.log("\u2139\uFE0F No Podfile found in ios directory; skipping `pod install`.");
4283
4472
  return;
4284
4473
  }
4285
- const cwd = path16.dirname(podfilePath);
4474
+ const cwd = path18.dirname(podfilePath);
4286
4475
  try {
4287
4476
  console.log(`\u2139\uFE0F Running \`pod install\` in ${cwd}...`);
4288
4477
  try {
@@ -4305,9 +4494,10 @@ $1`
4305
4494
  } else {
4306
4495
  console.log("\u2139\uFE0F No Tamer4Lynx native packages found.");
4307
4496
  }
4308
- syncHost_default();
4497
+ syncHost_default(syncHostOpts);
4309
4498
  updatePodfile(packages);
4310
4499
  ensureXElementPod();
4500
+ ensureLynxDevToolPods(discoverModules(projectRoot));
4311
4501
  ensureLynxPatchInPodfile();
4312
4502
  ensurePodBuildSettings();
4313
4503
  updateLynxInitProcessor(packages);
@@ -4316,8 +4506,8 @@ $1`
4316
4506
  syncInfoPlistUrlSchemes();
4317
4507
  const appNameFromConfig = resolved.config.ios?.appName;
4318
4508
  if (appNameFromConfig) {
4319
- const appPodfile = path16.join(iosProjectPath, appNameFromConfig, "Podfile");
4320
- if (fs15.existsSync(appPodfile)) {
4509
+ const appPodfile = path18.join(iosProjectPath, appNameFromConfig, "Podfile");
4510
+ if (fs17.existsSync(appPodfile)) {
4321
4511
  runPodInstall(appPodfile);
4322
4512
  console.log("\u2728 Autolinking complete for iOS.");
4323
4513
  return;
@@ -4332,13 +4522,13 @@ $1`
4332
4522
  const appFolder = resolved.config.ios?.appName;
4333
4523
  if (!hasDevClient || !appFolder) return;
4334
4524
  const androidNames = getDedupedAndroidModuleClassNames(allPkgs);
4335
- const appDir = path16.join(iosProjectPath, appFolder);
4336
- fs15.mkdirSync(appDir, { recursive: true });
4337
- const manifestPath = path16.join(appDir, TAMER_HOST_NATIVE_MODULES_FILENAME);
4338
- fs15.writeFileSync(manifestPath, buildHostNativeModulesManifestJson(androidNames), "utf8");
4525
+ const appDir = path18.join(iosProjectPath, appFolder);
4526
+ fs17.mkdirSync(appDir, { recursive: true });
4527
+ const manifestPath = path18.join(appDir, TAMER_HOST_NATIVE_MODULES_FILENAME);
4528
+ fs17.writeFileSync(manifestPath, buildHostNativeModulesManifestJson(androidNames), "utf8");
4339
4529
  console.log(`\u2705 Wrote ${TAMER_HOST_NATIVE_MODULES_FILENAME} (native module ids for dev-client checks)`);
4340
- const pbxprojPath = path16.join(iosProjectPath, `${appFolder}.xcodeproj`, "project.pbxproj");
4341
- if (fs15.existsSync(pbxprojPath)) {
4530
+ const pbxprojPath = path18.join(iosProjectPath, `${appFolder}.xcodeproj`, "project.pbxproj");
4531
+ if (fs17.existsSync(pbxprojPath)) {
4342
4532
  addResourceToXcodeProject(pbxprojPath, appFolder, TAMER_HOST_NATIVE_MODULES_FILENAME);
4343
4533
  }
4344
4534
  }
@@ -4347,8 +4537,8 @@ $1`
4347
4537
  var autolink_default2 = autolink2;
4348
4538
 
4349
4539
  // src/ios/bundle.ts
4350
- import fs16 from "fs";
4351
- import path17 from "path";
4540
+ import fs18 from "fs";
4541
+ import path19 from "path";
4352
4542
  import { execSync as execSync7 } from "child_process";
4353
4543
  function bundleAndDeploy2(opts = {}) {
4354
4544
  const release = opts.release === true || opts.production === true;
@@ -4366,20 +4556,19 @@ function bundleAndDeploy2(opts = {}) {
4366
4556
  const includeDevClient = !release && !!devClientPkg;
4367
4557
  const appName = resolved.config.ios.appName;
4368
4558
  const sourceBundlePath = resolved.lynxBundlePath;
4369
- const destinationDir = path17.join(resolved.iosDir, appName);
4370
- const destinationBundlePath = path17.join(destinationDir, resolved.lynxBundleFile);
4371
- syncHost_default({ release, includeDevClient });
4372
- autolink_default2();
4559
+ const destinationDir = path19.join(resolved.iosDir, appName);
4560
+ const destinationBundlePath = path19.join(destinationDir, resolved.lynxBundleFile);
4561
+ autolink_default2({ release, includeDevClient });
4373
4562
  const iconPaths = resolveIconPaths(resolved.projectRoot, resolved.config);
4374
4563
  if (iconPaths) {
4375
- const appIconDir = path17.join(destinationDir, "Assets.xcassets", "AppIcon.appiconset");
4564
+ const appIconDir = path19.join(destinationDir, "Assets.xcassets", "AppIcon.appiconset");
4376
4565
  if (applyIosAppIconAssets(appIconDir, iconPaths)) {
4377
4566
  console.log("\u2705 Synced iOS AppIcon from tamer.config.json");
4378
4567
  }
4379
4568
  }
4380
4569
  try {
4381
- const lynxTsconfig = path17.join(resolved.lynxProjectDir, "tsconfig.json");
4382
- if (fs16.existsSync(lynxTsconfig)) {
4570
+ const lynxTsconfig = path19.join(resolved.lynxProjectDir, "tsconfig.json");
4571
+ if (fs18.existsSync(lynxTsconfig)) {
4383
4572
  fixTsconfigReferencesForBuild(lynxTsconfig);
4384
4573
  }
4385
4574
  console.log("\u{1F4E6} Building Lynx bundle...");
@@ -4390,40 +4579,40 @@ function bundleAndDeploy2(opts = {}) {
4390
4579
  process.exit(1);
4391
4580
  }
4392
4581
  try {
4393
- if (!fs16.existsSync(sourceBundlePath)) {
4582
+ if (!fs18.existsSync(sourceBundlePath)) {
4394
4583
  console.error(`\u274C Build output not found at: ${sourceBundlePath}`);
4395
4584
  process.exit(1);
4396
4585
  }
4397
- if (!fs16.existsSync(destinationDir)) {
4586
+ if (!fs18.existsSync(destinationDir)) {
4398
4587
  console.error(`Destination directory not found at: ${destinationDir}`);
4399
4588
  process.exit(1);
4400
4589
  }
4401
- const distDir = path17.dirname(sourceBundlePath);
4590
+ const distDir = path19.dirname(sourceBundlePath);
4402
4591
  console.log(`\u{1F69A} Copying bundle and assets to iOS project...`);
4403
4592
  copyDistAssets(distDir, destinationDir, resolved.lynxBundleFile);
4404
4593
  console.log(`\u2728 Successfully copied bundle to: ${destinationBundlePath}`);
4405
- const pbxprojPath = path17.join(resolved.iosDir, `${appName}.xcodeproj`, "project.pbxproj");
4406
- if (fs16.existsSync(pbxprojPath)) {
4594
+ const pbxprojPath = path19.join(resolved.iosDir, `${appName}.xcodeproj`, "project.pbxproj");
4595
+ if (fs18.existsSync(pbxprojPath)) {
4407
4596
  const skip = /* @__PURE__ */ new Set([".rspeedy", "stats.json"]);
4408
- for (const entry of fs16.readdirSync(distDir)) {
4409
- if (skip.has(entry) || fs16.statSync(path17.join(distDir, entry)).isDirectory()) continue;
4597
+ for (const entry of fs18.readdirSync(distDir)) {
4598
+ if (skip.has(entry) || fs18.statSync(path19.join(distDir, entry)).isDirectory()) continue;
4410
4599
  addResourceToXcodeProject(pbxprojPath, appName, entry);
4411
4600
  }
4412
4601
  }
4413
4602
  if (includeDevClient && devClientPkg) {
4414
- const devClientBundle = path17.join(destinationDir, "dev-client.lynx.bundle");
4603
+ const devClientBundle = path19.join(destinationDir, "dev-client.lynx.bundle");
4415
4604
  console.log("\u{1F4E6} Building dev-client bundle...");
4416
4605
  try {
4417
4606
  execSync7("npm run build", { stdio: "inherit", cwd: devClientPkg });
4418
4607
  } catch {
4419
4608
  console.warn("\u26A0\uFE0F dev-client build failed; skipping dev-client bundle");
4420
4609
  }
4421
- const builtBundle = path17.join(devClientPkg, "dist", "dev-client.lynx.bundle");
4422
- if (fs16.existsSync(builtBundle)) {
4423
- fs16.copyFileSync(builtBundle, devClientBundle);
4610
+ const builtBundle = path19.join(devClientPkg, "dist", "dev-client.lynx.bundle");
4611
+ if (fs18.existsSync(builtBundle)) {
4612
+ fs18.copyFileSync(builtBundle, devClientBundle);
4424
4613
  console.log("\u2728 Copied dev-client.lynx.bundle to iOS project");
4425
- const pbxprojPath2 = path17.join(resolved.iosDir, `${appName}.xcodeproj`, "project.pbxproj");
4426
- if (fs16.existsSync(pbxprojPath2)) {
4614
+ const pbxprojPath2 = path19.join(resolved.iosDir, `${appName}.xcodeproj`, "project.pbxproj");
4615
+ if (fs18.existsSync(pbxprojPath2)) {
4427
4616
  addResourceToXcodeProject(pbxprojPath2, appName, "dev-client.lynx.bundle");
4428
4617
  }
4429
4618
  }
@@ -4437,9 +4626,10 @@ function bundleAndDeploy2(opts = {}) {
4437
4626
  var bundle_default2 = bundleAndDeploy2;
4438
4627
 
4439
4628
  // src/ios/build.ts
4440
- import fs17 from "fs";
4441
- import path18 from "path";
4629
+ import fs19 from "fs";
4630
+ import path20 from "path";
4442
4631
  import os3 from "os";
4632
+ import { randomBytes as randomBytes2 } from "crypto";
4443
4633
  import { execSync as execSync8 } from "child_process";
4444
4634
  function hostArch() {
4445
4635
  return os3.arch() === "arm64" ? "arm64" : "x86_64";
@@ -4457,6 +4647,31 @@ function findBootedSimulator() {
4457
4647
  }
4458
4648
  return null;
4459
4649
  }
4650
+ function findFirstConnectedIosDeviceUdid() {
4651
+ const jsonPath = path20.join(os3.tmpdir(), `t4l-devicectl-${randomBytes2(8).toString("hex")}.json`);
4652
+ try {
4653
+ execSync8(`xcrun devicectl list devices --json-output "${jsonPath}"`, {
4654
+ stdio: ["pipe", "pipe", "pipe"]
4655
+ });
4656
+ if (!fs19.existsSync(jsonPath)) return null;
4657
+ const raw = fs19.readFileSync(jsonPath, "utf8");
4658
+ fs19.unlinkSync(jsonPath);
4659
+ const data = JSON.parse(raw);
4660
+ const devices = data.result?.devices ?? [];
4661
+ for (const d of devices) {
4662
+ const udid = d.hardwareProperties?.udid ?? d.identifier;
4663
+ if (typeof udid === "string" && udid.length >= 20) {
4664
+ return udid;
4665
+ }
4666
+ }
4667
+ } catch {
4668
+ try {
4669
+ if (fs19.existsSync(jsonPath)) fs19.unlinkSync(jsonPath);
4670
+ } catch {
4671
+ }
4672
+ }
4673
+ return null;
4674
+ }
4460
4675
  async function buildIpa(opts = {}) {
4461
4676
  const resolved = resolveHostPaths();
4462
4677
  if (!resolved.config.ios?.appName) {
@@ -4469,17 +4684,19 @@ async function buildIpa(opts = {}) {
4469
4684
  const configuration = release ? "Release" : "Debug";
4470
4685
  bundle_default2({ release, production: opts.production });
4471
4686
  const scheme = appName;
4472
- const workspacePath = path18.join(iosDir, `${appName}.xcworkspace`);
4473
- const projectPath = path18.join(iosDir, `${appName}.xcodeproj`);
4474
- const xcproject = fs17.existsSync(workspacePath) ? workspacePath : projectPath;
4687
+ const workspacePath = path20.join(iosDir, `${appName}.xcworkspace`);
4688
+ const projectPath = path20.join(iosDir, `${appName}.xcodeproj`);
4689
+ const xcproject = fs19.existsSync(workspacePath) ? workspacePath : projectPath;
4475
4690
  const flag = xcproject.endsWith(".xcworkspace") ? "-workspace" : "-project";
4476
- const derivedDataPath = path18.join(iosDir, "build");
4477
- const sdk = opts.install ? "iphonesimulator" : "iphoneos";
4478
- const signingArgs = opts.install ? "" : " CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO";
4479
- const archFlag = opts.install ? `-arch ${hostArch()} ` : "";
4691
+ const derivedDataPath = path20.join(iosDir, "build");
4692
+ const production = opts.production === true;
4693
+ const sdk = production ? "iphoneos" : opts.install ? "iphonesimulator" : "iphoneos";
4694
+ const signingArgs = production || opts.install ? "" : " CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO";
4695
+ const archFlag = opts.install && !production ? `-arch ${hostArch()} ` : "";
4480
4696
  const extraSettings = [
4481
4697
  "ONLY_ACTIVE_ARCH=YES",
4482
- "CLANG_ENABLE_EXPLICIT_MODULES=NO"
4698
+ "CLANG_ENABLE_EXPLICIT_MODULES=NO",
4699
+ ...configuration === "Debug" ? ["COMPILER_INDEX_STORE_ENABLE=NO"] : []
4483
4700
  ].join(" ");
4484
4701
  console.log(`
4485
4702
  \u{1F528} Building ${configuration} (${sdk})...`);
@@ -4489,38 +4706,77 @@ async function buildIpa(opts = {}) {
4489
4706
  );
4490
4707
  console.log(`\u2705 Build completed.`);
4491
4708
  if (opts.install) {
4492
- const appGlob = path18.join(
4493
- derivedDataPath,
4494
- "Build",
4495
- "Products",
4496
- `${configuration}-iphonesimulator`,
4497
- `${appName}.app`
4498
- );
4499
- if (!fs17.existsSync(appGlob)) {
4500
- console.error(`\u274C Built app not found at: ${appGlob}`);
4501
- process.exit(1);
4502
- }
4503
- const udid = findBootedSimulator();
4504
- if (!udid) {
4505
- console.error("\u274C No booted simulator found. Start one with: xcrun simctl boot <udid>");
4506
- process.exit(1);
4507
- }
4508
- console.log(`\u{1F4F2} Installing on simulator ${udid}...`);
4509
- execSync8(`xcrun simctl install "${udid}" "${appGlob}"`, { stdio: "inherit" });
4510
- if (bundleId) {
4511
- console.log(`\u{1F680} Launching ${bundleId}...`);
4512
- execSync8(`xcrun simctl launch "${udid}" "${bundleId}"`, { stdio: "inherit" });
4513
- console.log("\u2705 App launched.");
4709
+ if (production) {
4710
+ const appPath = path20.join(
4711
+ derivedDataPath,
4712
+ "Build",
4713
+ "Products",
4714
+ `${configuration}-iphoneos`,
4715
+ `${appName}.app`
4716
+ );
4717
+ if (!fs19.existsSync(appPath)) {
4718
+ console.error(`\u274C Built app not found at: ${appPath}`);
4719
+ process.exit(1);
4720
+ }
4721
+ const udid = findFirstConnectedIosDeviceUdid();
4722
+ if (!udid) {
4723
+ console.error(
4724
+ "\u274C No connected iOS device found. Connect an iPhone/iPad with a trusted cable, unlock it, and ensure Developer Mode is on (iOS 16+). Requires Xcode 15+ (`xcrun devicectl`)."
4725
+ );
4726
+ process.exit(1);
4727
+ }
4728
+ console.log(`\u{1F4F2} Installing on device ${udid}...`);
4729
+ execSync8(`xcrun devicectl device install app --device "${udid}" "${appPath}"`, {
4730
+ stdio: "inherit"
4731
+ });
4732
+ if (bundleId) {
4733
+ console.log(`\u{1F680} Launching ${bundleId}...`);
4734
+ try {
4735
+ execSync8(
4736
+ `xcrun devicectl device process launch --device "${udid}" "${bundleId}"`,
4737
+ { stdio: "inherit" }
4738
+ );
4739
+ console.log("\u2705 App launched.");
4740
+ } catch {
4741
+ console.log("\u2705 Installed. Launch manually on the device if auto-launch failed.");
4742
+ }
4743
+ } else {
4744
+ console.log('\u2705 App installed. (Set "ios.bundleId" in tamer.config.json to auto-launch.)');
4745
+ }
4514
4746
  } else {
4515
- console.log('\u2705 App installed. (Set "ios.bundleId" in tamer.config.json to auto-launch.)');
4747
+ const appGlob = path20.join(
4748
+ derivedDataPath,
4749
+ "Build",
4750
+ "Products",
4751
+ `${configuration}-iphonesimulator`,
4752
+ `${appName}.app`
4753
+ );
4754
+ if (!fs19.existsSync(appGlob)) {
4755
+ console.error(`\u274C Built app not found at: ${appGlob}`);
4756
+ process.exit(1);
4757
+ }
4758
+ const udid = findBootedSimulator();
4759
+ if (!udid) {
4760
+ console.error("\u274C No booted simulator found. Start one with: xcrun simctl boot <udid>");
4761
+ process.exit(1);
4762
+ }
4763
+ console.log(`\u{1F4F2} Installing on simulator ${udid}...`);
4764
+ execSync8(`xcrun simctl install "${udid}" "${appGlob}"`, { stdio: "inherit" });
4765
+ if (bundleId) {
4766
+ console.log(`\u{1F680} Launching ${bundleId}...`);
4767
+ execSync8(`xcrun simctl launch "${udid}" "${bundleId}"`, { stdio: "inherit" });
4768
+ console.log("\u2705 App launched.");
4769
+ } else {
4770
+ console.log('\u2705 App installed. (Set "ios.bundleId" in tamer.config.json to auto-launch.)');
4771
+ }
4516
4772
  }
4517
4773
  }
4518
4774
  }
4519
4775
  var build_default2 = buildIpa;
4520
4776
 
4521
4777
  // src/common/init.tsx
4522
- import fs18 from "fs";
4523
- import path19 from "path";
4778
+ import fs20 from "fs";
4779
+ import path21 from "path";
4524
4780
  import { useState as useState4, useEffect as useEffect2, useCallback as useCallback3 } from "react";
4525
4781
  import { render, Text as Text9, Box as Box8 } from "ink";
4526
4782
 
@@ -4678,7 +4934,9 @@ function Wizard({ step, total, title, children }) {
4678
4934
  import "react";
4679
4935
  import { Box as Box7, Text as Text8 } from "ink";
4680
4936
  import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
4937
+ var LOG_DISPLAY_MAX = 120;
4681
4938
  function ServerDashboard({
4939
+ cliVersion,
4682
4940
  projectName,
4683
4941
  port,
4684
4942
  lanIp,
@@ -4691,7 +4949,6 @@ function ServerDashboard({
4691
4949
  buildError,
4692
4950
  wsConnections,
4693
4951
  logLines,
4694
- showLogs,
4695
4952
  qrLines,
4696
4953
  phase,
4697
4954
  startError
@@ -4710,6 +4967,10 @@ function ServerDashboard({
4710
4967
  projectName,
4711
4968
  ")"
4712
4969
  ] }),
4970
+ /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
4971
+ "t4l v",
4972
+ cliVersion
4973
+ ] }),
4713
4974
  verbose ? /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Logs: verbose (native + JS)" }) : null,
4714
4975
  /* @__PURE__ */ jsxs8(Box7, { marginTop: 1, flexDirection: "row", columnGap: 3, alignItems: "flex-start", children: [
4715
4976
  qrLines.length > 0 ? /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", flexShrink: 0, children: [
@@ -4746,7 +5007,7 @@ function ServerDashboard({
4746
5007
  bonjour ? /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "mDNS: _tamer._tcp" }) : null,
4747
5008
  /* @__PURE__ */ jsxs8(Box7, { marginTop: 1, flexDirection: "column", children: [
4748
5009
  /* @__PURE__ */ jsx8(Text8, { bold: true, children: "Build" }),
4749
- buildPhase === "building" ? /* @__PURE__ */ jsx8(TuiSpinner, { label: "Building\u2026" }) : buildPhase === "error" ? /* @__PURE__ */ jsx8(Text8, { color: "red", children: buildError ?? "Build failed" }) : /* @__PURE__ */ jsx8(Text8, { color: "green", children: "Ready" })
5010
+ buildPhase === "building" ? /* @__PURE__ */ jsx8(TuiSpinner, { label: "building\u2026" }) : buildPhase === "error" ? /* @__PURE__ */ jsx8(Text8, { color: "red", children: buildError ?? "Build failed" }) : buildPhase === "success" ? /* @__PURE__ */ jsx8(Text8, { color: "green", children: "Lynx bundle ready" }) : /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "\u2014" })
4750
5011
  ] }),
4751
5012
  /* @__PURE__ */ jsxs8(Box7, { marginTop: 1, flexDirection: "column", children: [
4752
5013
  /* @__PURE__ */ jsx8(Text8, { bold: true, children: "Connections" }),
@@ -4759,15 +5020,15 @@ function ServerDashboard({
4759
5020
  }
4760
5021
  )
4761
5022
  ] }),
4762
- showLogs && logLines.length > 0 ? /* @__PURE__ */ jsxs8(Box7, { marginTop: 1, flexDirection: "column", borderStyle: "single", paddingX: 1, children: [
4763
- /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
4764
- "Build / output (last ",
4765
- logLines.length,
4766
- " lines)"
4767
- ] }),
4768
- logLines.slice(-12).map((line, i) => /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: line }, i))
4769
- ] }) : null,
4770
- /* @__PURE__ */ jsx8(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "r rebuild \xB7 l toggle logs \xB7 q quit" }) })
5023
+ /* @__PURE__ */ jsx8(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "r rebuild \xB7 c clear output \xB7 Ctrl+C or q quit" }) }),
5024
+ logLines.length > 0 ? /* @__PURE__ */ jsxs8(Box7, { marginTop: 1, flexDirection: "column", children: [
5025
+ logLines.length > LOG_DISPLAY_MAX ? /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
5026
+ "\u2026 ",
5027
+ logLines.length - LOG_DISPLAY_MAX,
5028
+ " earlier lines omitted"
5029
+ ] }) : null,
5030
+ logLines.slice(-LOG_DISPLAY_MAX).map((line, i) => /* @__PURE__ */ jsx8(Text8, { children: line }, i))
5031
+ ] }) : null
4771
5032
  ] });
4772
5033
  }
4773
5034
 
@@ -4833,22 +5094,22 @@ function InitWizard() {
4833
5094
  paths: { androidDir: "android", iosDir: "ios" }
4834
5095
  };
4835
5096
  if (lynxProject.trim()) config.lynxProject = lynxProject.trim();
4836
- const configPath = path19.join(process.cwd(), "tamer.config.json");
4837
- fs18.writeFileSync(configPath, JSON.stringify(config, null, 2));
5097
+ const configPath = path21.join(process.cwd(), "tamer.config.json");
5098
+ fs20.writeFileSync(configPath, JSON.stringify(config, null, 2));
4838
5099
  const lines = [`Generated tamer.config.json at ${configPath}`];
4839
5100
  const tamerTypesInclude = "node_modules/@tamer4lynx/tamer-*/src/**/*.d.ts";
4840
5101
  const tsconfigCandidates = lynxProject.trim() ? [
4841
- path19.join(process.cwd(), lynxProject.trim(), "tsconfig.json"),
4842
- path19.join(process.cwd(), "tsconfig.json")
4843
- ] : [path19.join(process.cwd(), "tsconfig.json")];
5102
+ path21.join(process.cwd(), lynxProject.trim(), "tsconfig.json"),
5103
+ path21.join(process.cwd(), "tsconfig.json")
5104
+ ] : [path21.join(process.cwd(), "tsconfig.json")];
4844
5105
  for (const tsconfigPath of tsconfigCandidates) {
4845
- if (!fs18.existsSync(tsconfigPath)) continue;
5106
+ if (!fs20.existsSync(tsconfigPath)) continue;
4846
5107
  try {
4847
5108
  if (fixTsconfigReferencesForBuild(tsconfigPath)) {
4848
- lines.push(`Flattened ${path19.relative(process.cwd(), tsconfigPath)} (fixed TS6310)`);
5109
+ lines.push(`Flattened ${path21.relative(process.cwd(), tsconfigPath)} (fixed TS6310)`);
4849
5110
  }
4850
5111
  if (addTamerTypesInclude(tsconfigPath, tamerTypesInclude)) {
4851
- lines.push(`Updated ${path19.relative(process.cwd(), tsconfigPath)} for tamer types`);
5112
+ lines.push(`Updated ${path21.relative(process.cwd(), tsconfigPath)} for tamer types`);
4852
5113
  }
4853
5114
  break;
4854
5115
  } catch (e) {
@@ -5008,8 +5269,8 @@ async function init() {
5008
5269
  }
5009
5270
 
5010
5271
  // src/common/create.ts
5011
- import fs19 from "fs";
5012
- import path20 from "path";
5272
+ import fs21 from "fs";
5273
+ import path22 from "path";
5013
5274
  import readline from "readline";
5014
5275
  var rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false });
5015
5276
  function ask(question) {
@@ -5071,13 +5332,13 @@ async function create3(opts) {
5071
5332
  const simpleModuleName = extName.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("") + "Module";
5072
5333
  const fullModuleClassName = `${packageName}.${simpleModuleName}`;
5073
5334
  const cwd = process.cwd();
5074
- const root = path20.join(cwd, extName);
5075
- if (fs19.existsSync(root)) {
5335
+ const root = path22.join(cwd, extName);
5336
+ if (fs21.existsSync(root)) {
5076
5337
  console.error(`\u274C Directory ${extName} already exists.`);
5077
5338
  rl.close();
5078
5339
  process.exit(1);
5079
5340
  }
5080
- fs19.mkdirSync(root, { recursive: true });
5341
+ fs21.mkdirSync(root, { recursive: true });
5081
5342
  const lynxExt = {
5082
5343
  platforms: {
5083
5344
  android: {
@@ -5092,7 +5353,7 @@ async function create3(opts) {
5092
5353
  web: {}
5093
5354
  }
5094
5355
  };
5095
- fs19.writeFileSync(path20.join(root, "lynx.ext.json"), JSON.stringify(lynxExt, null, 2));
5356
+ fs21.writeFileSync(path22.join(root, "lynx.ext.json"), JSON.stringify(lynxExt, null, 2));
5096
5357
  const pkg = {
5097
5358
  name: extName,
5098
5359
  version: "0.0.1",
@@ -5105,20 +5366,20 @@ async function create3(opts) {
5105
5366
  engines: { node: ">=18" }
5106
5367
  };
5107
5368
  if (includeModule) pkg.types = "src/index.d.ts";
5108
- fs19.writeFileSync(path20.join(root, "package.json"), JSON.stringify(pkg, null, 2));
5369
+ fs21.writeFileSync(path22.join(root, "package.json"), JSON.stringify(pkg, null, 2));
5109
5370
  const pkgPath = packageName.replace(/\./g, "/");
5110
5371
  const hasSrc = includeModule || includeElement || includeService;
5111
5372
  if (hasSrc) {
5112
- fs19.mkdirSync(path20.join(root, "src"), { recursive: true });
5373
+ fs21.mkdirSync(path22.join(root, "src"), { recursive: true });
5113
5374
  }
5114
5375
  if (includeModule) {
5115
- fs19.writeFileSync(path20.join(root, "src", "index.d.ts"), `/** @lynxmodule */
5376
+ fs21.writeFileSync(path22.join(root, "src", "index.d.ts"), `/** @lynxmodule */
5116
5377
  export declare class ${simpleModuleName} {
5117
5378
  // Add your module methods here
5118
5379
  }
5119
5380
  `);
5120
- fs19.mkdirSync(path20.join(root, "android", "src", "main", "kotlin", pkgPath), { recursive: true });
5121
- fs19.writeFileSync(path20.join(root, "android", "build.gradle.kts"), `plugins {
5381
+ fs21.mkdirSync(path22.join(root, "android", "src", "main", "kotlin", pkgPath), { recursive: true });
5382
+ fs21.writeFileSync(path22.join(root, "android", "build.gradle.kts"), `plugins {
5122
5383
  id("com.android.library")
5123
5384
  id("org.jetbrains.kotlin.android")
5124
5385
  }
@@ -5139,7 +5400,7 @@ dependencies {
5139
5400
  implementation(libs.lynx.jssdk)
5140
5401
  }
5141
5402
  `);
5142
- fs19.writeFileSync(path20.join(root, "android", "src", "main", "AndroidManifest.xml"), `<?xml version="1.0" encoding="utf-8"?>
5403
+ fs21.writeFileSync(path22.join(root, "android", "src", "main", "AndroidManifest.xml"), `<?xml version="1.0" encoding="utf-8"?>
5143
5404
  <manifest />
5144
5405
  `);
5145
5406
  const ktContent = `package ${packageName}
@@ -5156,8 +5417,8 @@ class ${simpleModuleName}(context: Context) : LynxModule(context) {
5156
5417
  }
5157
5418
  }
5158
5419
  `;
5159
- fs19.writeFileSync(path20.join(root, "android", "src", "main", "kotlin", pkgPath, `${simpleModuleName}.kt`), ktContent);
5160
- fs19.mkdirSync(path20.join(root, "ios", extName, extName, "Classes"), { recursive: true });
5420
+ fs21.writeFileSync(path22.join(root, "android", "src", "main", "kotlin", pkgPath, `${simpleModuleName}.kt`), ktContent);
5421
+ fs21.mkdirSync(path22.join(root, "ios", extName, extName, "Classes"), { recursive: true });
5161
5422
  const podspec = `Pod::Spec.new do |s|
5162
5423
  s.name = '${extName}'
5163
5424
  s.version = '0.0.1'
@@ -5171,7 +5432,7 @@ class ${simpleModuleName}(context: Context) : LynxModule(context) {
5171
5432
  s.dependency 'Lynx'
5172
5433
  end
5173
5434
  `;
5174
- fs19.writeFileSync(path20.join(root, "ios", extName, `${extName}.podspec`), podspec);
5435
+ fs21.writeFileSync(path22.join(root, "ios", extName, `${extName}.podspec`), podspec);
5175
5436
  const swiftContent = `import Foundation
5176
5437
 
5177
5438
  @objc public class ${simpleModuleName}: NSObject {
@@ -5180,18 +5441,18 @@ end
5180
5441
  }
5181
5442
  }
5182
5443
  `;
5183
- fs19.writeFileSync(path20.join(root, "ios", extName, extName, "Classes", `${simpleModuleName}.swift`), swiftContent);
5444
+ fs21.writeFileSync(path22.join(root, "ios", extName, extName, "Classes", `${simpleModuleName}.swift`), swiftContent);
5184
5445
  }
5185
5446
  if (includeElement && !includeModule) {
5186
5447
  const elementName = extName.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
5187
- fs19.writeFileSync(path20.join(root, "src", "index.tsx"), `import type { FC } from '@lynx-js/react';
5448
+ fs21.writeFileSync(path22.join(root, "src", "index.tsx"), `import type { FC } from '@lynx-js/react';
5188
5449
 
5189
5450
  export const ${elementName}: FC = () => {
5190
5451
  return null;
5191
5452
  };
5192
5453
  `);
5193
5454
  }
5194
- fs19.writeFileSync(path20.join(root, "index.js"), `'use strict';
5455
+ fs21.writeFileSync(path22.join(root, "index.js"), `'use strict';
5195
5456
  module.exports = {};
5196
5457
  `);
5197
5458
  const tsconfigCompiler = {
@@ -5204,11 +5465,11 @@ module.exports = {};
5204
5465
  tsconfigCompiler.jsx = "preserve";
5205
5466
  tsconfigCompiler.jsxImportSource = "@lynx-js/react";
5206
5467
  }
5207
- fs19.writeFileSync(path20.join(root, "tsconfig.json"), JSON.stringify({
5468
+ fs21.writeFileSync(path22.join(root, "tsconfig.json"), JSON.stringify({
5208
5469
  compilerOptions: tsconfigCompiler,
5209
5470
  include: includeElement ? ["src", "src/**/*.tsx"] : ["src"]
5210
5471
  }, null, 2));
5211
- fs19.writeFileSync(path20.join(root, "README.md"), `# ${extName}
5472
+ fs21.writeFileSync(path22.join(root, "README.md"), `# ${extName}
5212
5473
 
5213
5474
  Lynx extension for ${extName}.
5214
5475
 
@@ -5233,8 +5494,8 @@ This package uses \`lynx.ext.json\` (RFC-compliant) for autolinking.
5233
5494
  var create_default3 = create3;
5234
5495
 
5235
5496
  // src/common/codegen.ts
5236
- import fs20 from "fs";
5237
- import path21 from "path";
5497
+ import fs22 from "fs";
5498
+ import path23 from "path";
5238
5499
  function codegen() {
5239
5500
  const cwd = process.cwd();
5240
5501
  const config = loadExtensionConfig(cwd);
@@ -5242,9 +5503,9 @@ function codegen() {
5242
5503
  console.error("\u274C No lynx.ext.json or tamer.json found. Run from an extension package root.");
5243
5504
  process.exit(1);
5244
5505
  }
5245
- const srcDir = path21.join(cwd, "src");
5246
- const generatedDir = path21.join(cwd, "generated");
5247
- fs20.mkdirSync(generatedDir, { recursive: true });
5506
+ const srcDir = path23.join(cwd, "src");
5507
+ const generatedDir = path23.join(cwd, "generated");
5508
+ fs22.mkdirSync(generatedDir, { recursive: true });
5248
5509
  const dtsFiles = findDtsFiles(srcDir);
5249
5510
  const modules = extractLynxModules(dtsFiles);
5250
5511
  if (modules.length === 0) {
@@ -5254,28 +5515,28 @@ function codegen() {
5254
5515
  for (const mod of modules) {
5255
5516
  const tsContent = `export type { ${mod} } from '../src/index.js';
5256
5517
  `;
5257
- const outPath = path21.join(generatedDir, `${mod}.ts`);
5258
- fs20.writeFileSync(outPath, tsContent);
5518
+ const outPath = path23.join(generatedDir, `${mod}.ts`);
5519
+ fs22.writeFileSync(outPath, tsContent);
5259
5520
  console.log(`\u2705 Generated ${outPath}`);
5260
5521
  }
5261
5522
  if (config.android) {
5262
- const androidGenerated = path21.join(cwd, "android", "src", "main", "kotlin", config.android.moduleClassName.replace(/\./g, "/").replace(/[^/]+$/, ""), "generated");
5263
- fs20.mkdirSync(androidGenerated, { recursive: true });
5523
+ const androidGenerated = path23.join(cwd, "android", "src", "main", "kotlin", config.android.moduleClassName.replace(/\./g, "/").replace(/[^/]+$/, ""), "generated");
5524
+ fs22.mkdirSync(androidGenerated, { recursive: true });
5264
5525
  console.log(`\u2139\uFE0F Android generated dir: ${androidGenerated} (spec generation coming soon)`);
5265
5526
  }
5266
5527
  if (config.ios) {
5267
- const iosGenerated = path21.join(cwd, "ios", "generated");
5268
- fs20.mkdirSync(iosGenerated, { recursive: true });
5528
+ const iosGenerated = path23.join(cwd, "ios", "generated");
5529
+ fs22.mkdirSync(iosGenerated, { recursive: true });
5269
5530
  console.log(`\u2139\uFE0F iOS generated dir: ${iosGenerated} (spec generation coming soon)`);
5270
5531
  }
5271
5532
  console.log("\u2728 Codegen complete.");
5272
5533
  }
5273
5534
  function findDtsFiles(dir) {
5274
5535
  const result = [];
5275
- if (!fs20.existsSync(dir)) return result;
5276
- const entries = fs20.readdirSync(dir, { withFileTypes: true });
5536
+ if (!fs22.existsSync(dir)) return result;
5537
+ const entries = fs22.readdirSync(dir, { withFileTypes: true });
5277
5538
  for (const e of entries) {
5278
- const full = path21.join(dir, e.name);
5539
+ const full = path23.join(dir, e.name);
5279
5540
  if (e.isDirectory()) result.push(...findDtsFiles(full));
5280
5541
  else if (e.name.endsWith(".d.ts")) result.push(full);
5281
5542
  }
@@ -5285,7 +5546,7 @@ function extractLynxModules(files) {
5285
5546
  const modules = [];
5286
5547
  const seen = /* @__PURE__ */ new Set();
5287
5548
  for (const file of files) {
5288
- const content = fs20.readFileSync(file, "utf8");
5549
+ const content = fs22.readFileSync(file, "utf8");
5289
5550
  const regex = /\/\*\*\s*@lynxmodule\s*\*\/\s*export\s+declare\s+class\s+(\w+)/g;
5290
5551
  let m;
5291
5552
  while ((m = regex.exec(content)) !== null) {
@@ -5302,14 +5563,16 @@ var codegen_default = codegen;
5302
5563
  // src/common/devServer.tsx
5303
5564
  import { useState as useState5, useEffect as useEffect3, useRef, useCallback as useCallback4 } from "react";
5304
5565
  import { spawn } from "child_process";
5305
- import fs21 from "fs";
5566
+ import fs23 from "fs";
5306
5567
  import http from "http";
5307
5568
  import os4 from "os";
5308
- import path22 from "path";
5569
+ import path24 from "path";
5309
5570
  import { render as render2, useInput, useApp } from "ink";
5310
- import { WebSocketServer } from "ws";
5571
+ import { WebSocket, WebSocketServer } from "ws";
5311
5572
  import { jsx as jsx10 } from "react/jsx-runtime";
5312
5573
  var DEFAULT_PORT = 3e3;
5574
+ var TAMER_CLI_VERSION = getCliVersion();
5575
+ var MAX_LOG_LINES = 800;
5313
5576
  var STATIC_MIME = {
5314
5577
  ".png": "image/png",
5315
5578
  ".jpg": "image/jpeg",
@@ -5321,13 +5584,13 @@ var STATIC_MIME = {
5321
5584
  ".pdf": "application/pdf"
5322
5585
  };
5323
5586
  function sendFileFromDisk(res, absPath) {
5324
- fs21.readFile(absPath, (err, data) => {
5587
+ fs23.readFile(absPath, (err, data) => {
5325
5588
  if (err) {
5326
5589
  res.writeHead(404);
5327
5590
  res.end("Not found");
5328
5591
  return;
5329
5592
  }
5330
- const ext = path22.extname(absPath).toLowerCase();
5593
+ const ext = path24.extname(absPath).toLowerCase();
5331
5594
  res.setHeader("Content-Type", STATIC_MIME[ext] ?? "application/octet-stream");
5332
5595
  res.setHeader("Access-Control-Allow-Origin", "*");
5333
5596
  res.end(data);
@@ -5364,9 +5627,9 @@ function getLanIp() {
5364
5627
  return "localhost";
5365
5628
  }
5366
5629
  function detectPackageManager(cwd) {
5367
- const dir = path22.resolve(cwd);
5368
- if (fs21.existsSync(path22.join(dir, "pnpm-lock.yaml"))) return { cmd: "pnpm", args: ["run", "build"] };
5369
- if (fs21.existsSync(path22.join(dir, "bun.lockb")) || fs21.existsSync(path22.join(dir, "bun.lock")))
5630
+ const dir = path24.resolve(cwd);
5631
+ if (fs23.existsSync(path24.join(dir, "pnpm-lock.yaml"))) return { cmd: "pnpm", args: ["run", "build"] };
5632
+ if (fs23.existsSync(path24.join(dir, "bun.lockb")) || fs23.existsSync(path24.join(dir, "bun.lock")))
5370
5633
  return { cmd: "bun", args: ["run", "build"] };
5371
5634
  return { cmd: "npm", args: ["run", "build"] };
5372
5635
  }
@@ -5383,7 +5646,6 @@ var initialUi = () => ({
5383
5646
  buildPhase: "idle",
5384
5647
  wsConnections: 0,
5385
5648
  logLines: [],
5386
- showLogs: false,
5387
5649
  qrLines: []
5388
5650
  });
5389
5651
  function DevServerApp({ verbose }) {
@@ -5396,13 +5658,20 @@ function DevServerApp({ verbose }) {
5396
5658
  const cleanupRef = useRef(null);
5397
5659
  const rebuildRef = useRef(() => Promise.resolve());
5398
5660
  const quitOnceRef = useRef(false);
5399
- const appendLog = useCallback4((chunk) => {
5400
- const lines = chunk.split(/\r?\n/).filter(Boolean);
5661
+ const appendLogLine = useCallback4((line) => {
5401
5662
  setUi((prev) => ({
5402
5663
  ...prev,
5403
- logLines: [...prev.logLines, ...lines].slice(-400)
5664
+ logLines: [...prev.logLines, line].slice(-MAX_LOG_LINES)
5404
5665
  }));
5405
5666
  }, []);
5667
+ const appendLog = useCallback4(
5668
+ (chunk) => {
5669
+ for (const line of chunk.split(/\r?\n/)) {
5670
+ appendLogLine(line);
5671
+ }
5672
+ },
5673
+ [appendLogLine]
5674
+ );
5406
5675
  const handleQuit = useCallback4(() => {
5407
5676
  if (quitOnceRef.current) return;
5408
5677
  quitOnceRef.current = true;
@@ -5410,7 +5679,7 @@ function DevServerApp({ verbose }) {
5410
5679
  exit();
5411
5680
  }, [exit]);
5412
5681
  useInput((input, key) => {
5413
- if (key.ctrl && key.name === "c") {
5682
+ if (key.ctrl && input === "c") {
5414
5683
  handleQuit();
5415
5684
  return;
5416
5685
  }
@@ -5422,8 +5691,9 @@ function DevServerApp({ verbose }) {
5422
5691
  void rebuildRef.current();
5423
5692
  return;
5424
5693
  }
5425
- if (input === "l") {
5426
- setUi((s) => ({ ...s, showLogs: !s.showLogs }));
5694
+ if (input === "c") {
5695
+ setUi((s) => ({ ...s, logLines: [] }));
5696
+ return;
5427
5697
  }
5428
5698
  });
5429
5699
  useEffect3(() => {
@@ -5446,8 +5716,8 @@ function DevServerApp({ verbose }) {
5446
5716
  try {
5447
5717
  const resolved = resolveHostPaths();
5448
5718
  const { projectRoot, lynxProjectDir, lynxBundlePath, lynxBundleFile, config } = resolved;
5449
- const distDir = path22.dirname(lynxBundlePath);
5450
- const projectName = path22.basename(lynxProjectDir);
5719
+ const distDir = path24.dirname(lynxBundlePath);
5720
+ const projectName = path24.basename(lynxProjectDir);
5451
5721
  const basePath = `/${projectName}`;
5452
5722
  setUi((s) => ({ ...s, projectName, lynxBundleFile }));
5453
5723
  const preferredPort = config.devServer?.port ?? config.devServer?.httpPort ?? DEFAULT_PORT;
@@ -5457,18 +5727,18 @@ function DevServerApp({ verbose }) {
5457
5727
  }
5458
5728
  const iconPaths = resolveIconPaths(projectRoot, config);
5459
5729
  let iconFilePath = null;
5460
- if (iconPaths?.source && fs21.statSync(iconPaths.source).isFile()) {
5730
+ if (iconPaths?.source && fs23.statSync(iconPaths.source).isFile()) {
5461
5731
  iconFilePath = iconPaths.source;
5462
- } else if (iconPaths?.androidAdaptiveForeground && fs21.statSync(iconPaths.androidAdaptiveForeground).isFile()) {
5732
+ } else if (iconPaths?.androidAdaptiveForeground && fs23.statSync(iconPaths.androidAdaptiveForeground).isFile()) {
5463
5733
  iconFilePath = iconPaths.androidAdaptiveForeground;
5464
5734
  } else if (iconPaths?.android) {
5465
- const androidIcon = path22.join(iconPaths.android, "mipmap-xxxhdpi", "ic_launcher.png");
5466
- if (fs21.existsSync(androidIcon)) iconFilePath = androidIcon;
5735
+ const androidIcon = path24.join(iconPaths.android, "mipmap-xxxhdpi", "ic_launcher.png");
5736
+ if (fs23.existsSync(androidIcon)) iconFilePath = androidIcon;
5467
5737
  } else if (iconPaths?.ios) {
5468
- const iosIcon = path22.join(iconPaths.ios, "Icon-1024.png");
5469
- if (fs21.existsSync(iosIcon)) iconFilePath = iosIcon;
5738
+ const iosIcon = path24.join(iconPaths.ios, "Icon-1024.png");
5739
+ if (fs23.existsSync(iosIcon)) iconFilePath = iosIcon;
5470
5740
  }
5471
- const iconExt = iconFilePath ? path22.extname(iconFilePath) || ".png" : "";
5741
+ const iconExt = iconFilePath ? path24.extname(iconFilePath) || ".png" : "";
5472
5742
  const runBuild = () => {
5473
5743
  return new Promise((resolve, reject) => {
5474
5744
  const { cmd, args } = detectPackageManager(lynxProjectDir);
@@ -5477,19 +5747,15 @@ function DevServerApp({ verbose }) {
5477
5747
  stdio: "pipe",
5478
5748
  shell: process.platform === "win32"
5479
5749
  });
5480
- let stderr = "";
5481
- buildProcess.stdout?.on("data", (d) => {
5482
- appendLog(d.toString());
5483
- });
5750
+ let stderrRaw = "";
5751
+ buildProcess.stdout?.resume();
5484
5752
  buildProcess.stderr?.on("data", (d) => {
5485
- const t = d.toString();
5486
- stderr += t;
5487
- appendLog(t);
5753
+ stderrRaw += d.toString();
5488
5754
  });
5489
5755
  buildProcess.on("close", (code) => {
5490
5756
  buildProcess = null;
5491
5757
  if (code === 0) resolve();
5492
- else reject(new Error(stderr || `Build exited ${code}`));
5758
+ else reject(new Error(stderrRaw.trim() || `Build exited ${code}`));
5493
5759
  });
5494
5760
  });
5495
5761
  };
@@ -5554,7 +5820,7 @@ function DevServerApp({ verbose }) {
5554
5820
  return;
5555
5821
  }
5556
5822
  if (iconFilePath && (reqPath === `${basePath}/icon` || reqPath === `${basePath}/icon${iconExt}`)) {
5557
- fs21.readFile(iconFilePath, (err, data) => {
5823
+ fs23.readFile(iconFilePath, (err, data) => {
5558
5824
  if (err) {
5559
5825
  res.writeHead(404);
5560
5826
  res.end();
@@ -5580,20 +5846,20 @@ function DevServerApp({ verbose }) {
5580
5846
  res.end();
5581
5847
  return;
5582
5848
  }
5583
- const safe = path22.normalize(rel).replace(/^(\.\.(\/|\\|$))+/, "");
5584
- if (path22.isAbsolute(safe) || safe.startsWith("..")) {
5849
+ const safe = path24.normalize(rel).replace(/^(\.\.(\/|\\|$))+/, "");
5850
+ if (path24.isAbsolute(safe) || safe.startsWith("..")) {
5585
5851
  res.writeHead(403);
5586
5852
  res.end();
5587
5853
  return;
5588
5854
  }
5589
- const allowedRoot = path22.resolve(lynxProjectDir, rootSub);
5590
- const abs = path22.resolve(allowedRoot, safe);
5591
- if (!abs.startsWith(allowedRoot + path22.sep) && abs !== allowedRoot) {
5855
+ const allowedRoot = path24.resolve(lynxProjectDir, rootSub);
5856
+ const abs = path24.resolve(allowedRoot, safe);
5857
+ if (!abs.startsWith(allowedRoot + path24.sep) && abs !== allowedRoot) {
5592
5858
  res.writeHead(403);
5593
5859
  res.end();
5594
5860
  return;
5595
5861
  }
5596
- if (!fs21.existsSync(abs) || !fs21.statSync(abs).isFile()) {
5862
+ if (!fs23.existsSync(abs) || !fs23.statSync(abs).isFile()) {
5597
5863
  res.writeHead(404);
5598
5864
  res.end("Not found");
5599
5865
  return;
@@ -5607,14 +5873,14 @@ function DevServerApp({ verbose }) {
5607
5873
  reqPath = basePath + (reqPath.startsWith("/") ? reqPath : "/" + reqPath);
5608
5874
  }
5609
5875
  const relPath = reqPath.replace(basePath, "").replace(/^\//, "") || lynxBundleFile;
5610
- const filePath = path22.resolve(distDir, relPath);
5611
- const distResolved = path22.resolve(distDir);
5612
- if (!filePath.startsWith(distResolved + path22.sep) && filePath !== distResolved) {
5876
+ const filePath = path24.resolve(distDir, relPath);
5877
+ const distResolved = path24.resolve(distDir);
5878
+ if (!filePath.startsWith(distResolved + path24.sep) && filePath !== distResolved) {
5613
5879
  res.writeHead(403);
5614
5880
  res.end();
5615
5881
  return;
5616
5882
  }
5617
- fs21.readFile(filePath, (err, data) => {
5883
+ fs23.readFile(filePath, (err, data) => {
5618
5884
  if (err) {
5619
5885
  res.writeHead(404);
5620
5886
  res.end("Not found");
@@ -5626,6 +5892,14 @@ function DevServerApp({ verbose }) {
5626
5892
  });
5627
5893
  });
5628
5894
  const wssInst = new WebSocketServer({ noServer: true });
5895
+ const syncWsClientCount = () => {
5896
+ if (!alive) return;
5897
+ let n = 0;
5898
+ wssInst.clients.forEach((c) => {
5899
+ if (c.readyState === WebSocket.OPEN) n++;
5900
+ });
5901
+ setUi((s) => ({ ...s, wsConnections: n }));
5902
+ };
5629
5903
  rebuildRef.current = async () => {
5630
5904
  try {
5631
5905
  await doBuild();
@@ -5647,12 +5921,15 @@ function DevServerApp({ verbose }) {
5647
5921
  });
5648
5922
  wssInst.on("connection", (ws, req) => {
5649
5923
  const clientIp = req.socket.remoteAddress ?? "unknown";
5650
- setUi((s) => ({ ...s, wsConnections: s.wsConnections + 1 }));
5651
5924
  appendLog(`[WS] connected: ${clientIp}`);
5652
5925
  ws.send(JSON.stringify({ type: "connected" }));
5926
+ syncWsClientCount();
5653
5927
  ws.on("close", () => {
5654
- setUi((s) => ({ ...s, wsConnections: Math.max(0, s.wsConnections - 1) }));
5655
5928
  appendLog(`[WS] disconnected: ${clientIp}`);
5929
+ queueMicrotask(() => syncWsClientCount());
5930
+ });
5931
+ ws.on("error", () => {
5932
+ queueMicrotask(() => syncWsClientCount());
5656
5933
  });
5657
5934
  ws.on("message", (data) => {
5658
5935
  try {
@@ -5675,10 +5952,10 @@ function DevServerApp({ verbose }) {
5675
5952
  }
5676
5953
  if (chokidar) {
5677
5954
  const watchPaths = [
5678
- path22.join(lynxProjectDir, "src"),
5679
- path22.join(lynxProjectDir, "lynx.config.ts"),
5680
- path22.join(lynxProjectDir, "lynx.config.js")
5681
- ].filter((p) => fs21.existsSync(p));
5955
+ path24.join(lynxProjectDir, "src"),
5956
+ path24.join(lynxProjectDir, "lynx.config.ts"),
5957
+ path24.join(lynxProjectDir, "lynx.config.js")
5958
+ ].filter((p) => fs23.existsSync(p));
5682
5959
  if (watchPaths.length > 0) {
5683
5960
  const w = chokidar.watch(watchPaths, { ignoreInitial: true });
5684
5961
  w.on("change", async () => {
@@ -5758,10 +6035,11 @@ function DevServerApp({ verbose }) {
5758
6035
  alive = false;
5759
6036
  void cleanupRef.current?.();
5760
6037
  };
5761
- }, [appendLog, verbose]);
6038
+ }, [appendLog, appendLogLine, verbose]);
5762
6039
  return /* @__PURE__ */ jsx10(
5763
6040
  ServerDashboard,
5764
6041
  {
6042
+ cliVersion: TAMER_CLI_VERSION,
5765
6043
  projectName: ui.projectName,
5766
6044
  port: ui.port,
5767
6045
  lanIp: ui.lanIp,
@@ -5774,7 +6052,6 @@ function DevServerApp({ verbose }) {
5774
6052
  buildError: ui.buildError,
5775
6053
  wsConnections: ui.wsConnections,
5776
6054
  logLines: ui.logLines,
5777
- showLogs: ui.showLogs,
5778
6055
  qrLines: ui.qrLines,
5779
6056
  phase: ui.phase,
5780
6057
  startError: ui.startError
@@ -5798,10 +6075,10 @@ async function start(opts) {
5798
6075
  var start_default = start;
5799
6076
 
5800
6077
  // src/common/injectHost.ts
5801
- import fs22 from "fs";
5802
- import path23 from "path";
6078
+ import fs24 from "fs";
6079
+ import path25 from "path";
5803
6080
  function readAndSubstitute(templatePath, vars) {
5804
- const raw = fs22.readFileSync(templatePath, "utf-8");
6081
+ const raw = fs24.readFileSync(templatePath, "utf-8");
5805
6082
  return Object.entries(vars).reduce(
5806
6083
  (s, [k, v]) => s.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), v),
5807
6084
  raw
@@ -5822,32 +6099,32 @@ async function injectHostAndroid(opts) {
5822
6099
  process.exit(1);
5823
6100
  }
5824
6101
  const androidDir = config.paths?.androidDir ?? "android";
5825
- const rootDir = path23.join(projectRoot, androidDir);
6102
+ const rootDir = path25.join(projectRoot, androidDir);
5826
6103
  const packagePath = packageName.replace(/\./g, "/");
5827
- const javaDir = path23.join(rootDir, "app", "src", "main", "java", packagePath);
5828
- const kotlinDir = path23.join(rootDir, "app", "src", "main", "kotlin", packagePath);
5829
- if (!fs22.existsSync(javaDir) || !fs22.existsSync(kotlinDir)) {
6104
+ const javaDir = path25.join(rootDir, "app", "src", "main", "java", packagePath);
6105
+ const kotlinDir = path25.join(rootDir, "app", "src", "main", "kotlin", packagePath);
6106
+ if (!fs24.existsSync(javaDir) || !fs24.existsSync(kotlinDir)) {
5830
6107
  console.error("\u274C Android project not found. Run `t4l android create` first or ensure android/ exists.");
5831
6108
  process.exit(1);
5832
6109
  }
5833
- const templateDir = path23.join(hostPkg, "android", "templates");
6110
+ const templateDir = path25.join(hostPkg, "android", "templates");
5834
6111
  const vars = { PACKAGE_NAME: packageName, APP_NAME: appName };
5835
6112
  const files = [
5836
- { src: "App.java", dst: path23.join(javaDir, "App.java") },
5837
- { src: "TemplateProvider.java", dst: path23.join(javaDir, "TemplateProvider.java") },
5838
- { src: "MainActivity.kt", dst: path23.join(kotlinDir, "MainActivity.kt") }
6113
+ { src: "App.java", dst: path25.join(javaDir, "App.java") },
6114
+ { src: "TemplateProvider.java", dst: path25.join(javaDir, "TemplateProvider.java") },
6115
+ { src: "MainActivity.kt", dst: path25.join(kotlinDir, "MainActivity.kt") }
5839
6116
  ];
5840
6117
  for (const { src, dst } of files) {
5841
- const srcPath = path23.join(templateDir, src);
5842
- if (!fs22.existsSync(srcPath)) continue;
5843
- if (fs22.existsSync(dst) && !opts?.force) {
5844
- console.log(`\u23ED\uFE0F Skipping ${path23.basename(dst)} (use --force to overwrite)`);
6118
+ const srcPath = path25.join(templateDir, src);
6119
+ if (!fs24.existsSync(srcPath)) continue;
6120
+ if (fs24.existsSync(dst) && !opts?.force) {
6121
+ console.log(`\u23ED\uFE0F Skipping ${path25.basename(dst)} (use --force to overwrite)`);
5845
6122
  continue;
5846
6123
  }
5847
6124
  const content = readAndSubstitute(srcPath, vars);
5848
- fs22.mkdirSync(path23.dirname(dst), { recursive: true });
5849
- fs22.writeFileSync(dst, content);
5850
- console.log(`\u2705 Injected ${path23.basename(dst)}`);
6125
+ fs24.mkdirSync(path25.dirname(dst), { recursive: true });
6126
+ fs24.writeFileSync(dst, content);
6127
+ console.log(`\u2705 Injected ${path25.basename(dst)}`);
5851
6128
  }
5852
6129
  }
5853
6130
  async function injectHostIos(opts) {
@@ -5865,13 +6142,13 @@ async function injectHostIos(opts) {
5865
6142
  process.exit(1);
5866
6143
  }
5867
6144
  const iosDir = config.paths?.iosDir ?? "ios";
5868
- const rootDir = path23.join(projectRoot, iosDir);
5869
- const projectDir = path23.join(rootDir, appName);
5870
- if (!fs22.existsSync(projectDir)) {
6145
+ const rootDir = path25.join(projectRoot, iosDir);
6146
+ const projectDir = path25.join(rootDir, appName);
6147
+ if (!fs24.existsSync(projectDir)) {
5871
6148
  console.error("\u274C iOS project not found. Run `t4l ios create` first or ensure ios/ exists.");
5872
6149
  process.exit(1);
5873
6150
  }
5874
- const templateDir = path23.join(hostPkg, "ios", "templates");
6151
+ const templateDir = path25.join(hostPkg, "ios", "templates");
5875
6152
  const vars = { PACKAGE_NAME: bundleId, APP_NAME: appName, BUNDLE_ID: bundleId };
5876
6153
  const files = [
5877
6154
  "AppDelegate.swift",
@@ -5881,22 +6158,22 @@ async function injectHostIos(opts) {
5881
6158
  "LynxInitProcessor.swift"
5882
6159
  ];
5883
6160
  for (const f of files) {
5884
- const srcPath = path23.join(templateDir, f);
5885
- const dstPath = path23.join(projectDir, f);
5886
- if (!fs22.existsSync(srcPath)) continue;
5887
- if (fs22.existsSync(dstPath) && !opts?.force) {
6161
+ const srcPath = path25.join(templateDir, f);
6162
+ const dstPath = path25.join(projectDir, f);
6163
+ if (!fs24.existsSync(srcPath)) continue;
6164
+ if (fs24.existsSync(dstPath) && !opts?.force) {
5888
6165
  console.log(`\u23ED\uFE0F Skipping ${f} (use --force to overwrite)`);
5889
6166
  continue;
5890
6167
  }
5891
6168
  const content = readAndSubstitute(srcPath, vars);
5892
- fs22.writeFileSync(dstPath, content);
6169
+ fs24.writeFileSync(dstPath, content);
5893
6170
  console.log(`\u2705 Injected ${f}`);
5894
6171
  }
5895
6172
  }
5896
6173
 
5897
6174
  // src/common/buildEmbeddable.ts
5898
- import fs23 from "fs";
5899
- import path24 from "path";
6175
+ import fs25 from "fs";
6176
+ import path26 from "path";
5900
6177
  import { execSync as execSync9 } from "child_process";
5901
6178
  var EMBEDDABLE_DIR = "embeddable";
5902
6179
  var LIB_PACKAGE = "com.tamer.embeddable";
@@ -5973,14 +6250,14 @@ object LynxEmbeddable {
5973
6250
  }
5974
6251
  `;
5975
6252
  function generateAndroidLibrary(outDir, androidDir, projectRoot, lynxBundlePath, lynxBundleFile, modules, abiFilters) {
5976
- const libDir = path24.join(androidDir, "lib");
5977
- const libSrcMain = path24.join(libDir, "src", "main");
5978
- const assetsDir = path24.join(libSrcMain, "assets");
5979
- const kotlinDir = path24.join(libSrcMain, "kotlin", LIB_PACKAGE.replace(/\./g, "/"));
5980
- const generatedDir = path24.join(kotlinDir, "generated");
5981
- fs23.mkdirSync(path24.join(androidDir, "gradle"), { recursive: true });
5982
- fs23.mkdirSync(generatedDir, { recursive: true });
5983
- fs23.mkdirSync(assetsDir, { recursive: true });
6253
+ const libDir = path26.join(androidDir, "lib");
6254
+ const libSrcMain = path26.join(libDir, "src", "main");
6255
+ const assetsDir = path26.join(libSrcMain, "assets");
6256
+ const kotlinDir = path26.join(libSrcMain, "kotlin", LIB_PACKAGE.replace(/\./g, "/"));
6257
+ const generatedDir = path26.join(kotlinDir, "generated");
6258
+ fs25.mkdirSync(path26.join(androidDir, "gradle"), { recursive: true });
6259
+ fs25.mkdirSync(generatedDir, { recursive: true });
6260
+ fs25.mkdirSync(assetsDir, { recursive: true });
5984
6261
  const androidModules = modules.filter((m) => m.config.android);
5985
6262
  const abiList = abiFilters.map((a) => `"${a}"`).join(", ");
5986
6263
  const settingsContent = `pluginManagement {
@@ -6000,7 +6277,7 @@ include(":lib")
6000
6277
  ${androidModules.map((p) => {
6001
6278
  const gradleName = p.name.replace(/^@/, "").replace(/\//g, "_");
6002
6279
  const sourceDir = p.config.android?.sourceDir || "android";
6003
- const absPath = path24.join(p.packagePath, sourceDir).replace(/\\/g, "/");
6280
+ const absPath = path26.join(p.packagePath, sourceDir).replace(/\\/g, "/");
6004
6281
  return `include(":${gradleName}")
6005
6282
  project(":${gradleName}").projectDir = file("${absPath}")`;
6006
6283
  }).join("\n")}
@@ -6049,10 +6326,10 @@ dependencies {
6049
6326
  ${libDeps}
6050
6327
  }
6051
6328
  `;
6052
- fs23.writeFileSync(path24.join(androidDir, "gradle", "libs.versions.toml"), LIBS_VERSIONS_TOML);
6053
- fs23.writeFileSync(path24.join(androidDir, "settings.gradle.kts"), settingsContent);
6054
- fs23.writeFileSync(
6055
- path24.join(androidDir, "build.gradle.kts"),
6329
+ fs25.writeFileSync(path26.join(androidDir, "gradle", "libs.versions.toml"), LIBS_VERSIONS_TOML);
6330
+ fs25.writeFileSync(path26.join(androidDir, "settings.gradle.kts"), settingsContent);
6331
+ fs25.writeFileSync(
6332
+ path26.join(androidDir, "build.gradle.kts"),
6056
6333
  `plugins {
6057
6334
  alias(libs.plugins.android.library) apply false
6058
6335
  alias(libs.plugins.kotlin.android) apply false
@@ -6060,26 +6337,26 @@ ${libDeps}
6060
6337
  }
6061
6338
  `
6062
6339
  );
6063
- fs23.writeFileSync(
6064
- path24.join(androidDir, "gradle.properties"),
6340
+ fs25.writeFileSync(
6341
+ path26.join(androidDir, "gradle.properties"),
6065
6342
  `org.gradle.jvmargs=-Xmx2048m
6066
6343
  android.useAndroidX=true
6067
6344
  kotlin.code.style=official
6068
6345
  `
6069
6346
  );
6070
- fs23.writeFileSync(path24.join(libDir, "build.gradle.kts"), libBuildContent);
6071
- fs23.writeFileSync(
6072
- path24.join(libSrcMain, "AndroidManifest.xml"),
6347
+ fs25.writeFileSync(path26.join(libDir, "build.gradle.kts"), libBuildContent);
6348
+ fs25.writeFileSync(
6349
+ path26.join(libSrcMain, "AndroidManifest.xml"),
6073
6350
  '<?xml version="1.0" encoding="utf-8"?>\n<manifest />'
6074
6351
  );
6075
- fs23.copyFileSync(lynxBundlePath, path24.join(assetsDir, lynxBundleFile));
6076
- fs23.writeFileSync(path24.join(kotlinDir, "LynxEmbeddable.kt"), LYNX_EMBEDDABLE_KT);
6077
- fs23.writeFileSync(
6078
- path24.join(generatedDir, "GeneratedLynxExtensions.kt"),
6352
+ fs25.copyFileSync(lynxBundlePath, path26.join(assetsDir, lynxBundleFile));
6353
+ fs25.writeFileSync(path26.join(kotlinDir, "LynxEmbeddable.kt"), LYNX_EMBEDDABLE_KT);
6354
+ fs25.writeFileSync(
6355
+ path26.join(generatedDir, "GeneratedLynxExtensions.kt"),
6079
6356
  generateLynxExtensionsKotlin(modules, LIB_PACKAGE)
6080
6357
  );
6081
- fs23.writeFileSync(
6082
- path24.join(generatedDir, "GeneratedActivityLifecycle.kt"),
6358
+ fs25.writeFileSync(
6359
+ path26.join(generatedDir, "GeneratedActivityLifecycle.kt"),
6083
6360
  generateActivityLifecycleKotlin(modules, LIB_PACKAGE)
6084
6361
  );
6085
6362
  }
@@ -6088,20 +6365,20 @@ async function buildEmbeddable(opts = {}) {
6088
6365
  const { lynxProjectDir, lynxBundlePath, lynxBundleFile, projectRoot, config } = resolved;
6089
6366
  console.log("\u{1F4E6} Building Lynx project (release)...");
6090
6367
  execSync9("npm run build", { stdio: "inherit", cwd: lynxProjectDir });
6091
- if (!fs23.existsSync(lynxBundlePath)) {
6368
+ if (!fs25.existsSync(lynxBundlePath)) {
6092
6369
  console.error(`\u274C Bundle not found at ${lynxBundlePath}`);
6093
6370
  process.exit(1);
6094
6371
  }
6095
- const outDir = path24.join(projectRoot, EMBEDDABLE_DIR);
6096
- fs23.mkdirSync(outDir, { recursive: true });
6097
- const distDir = path24.dirname(lynxBundlePath);
6372
+ const outDir = path26.join(projectRoot, EMBEDDABLE_DIR);
6373
+ fs25.mkdirSync(outDir, { recursive: true });
6374
+ const distDir = path26.dirname(lynxBundlePath);
6098
6375
  copyDistAssets(distDir, outDir, lynxBundleFile);
6099
6376
  const modules = discoverModules(projectRoot);
6100
6377
  const androidModules = modules.filter((m) => m.config.android);
6101
6378
  const abiFilters = resolveAbiFilters(config);
6102
- const androidDir = path24.join(outDir, "android");
6103
- if (fs23.existsSync(androidDir)) fs23.rmSync(androidDir, { recursive: true });
6104
- fs23.mkdirSync(androidDir, { recursive: true });
6379
+ const androidDir = path26.join(outDir, "android");
6380
+ if (fs25.existsSync(androidDir)) fs25.rmSync(androidDir, { recursive: true });
6381
+ fs25.mkdirSync(androidDir, { recursive: true });
6105
6382
  generateAndroidLibrary(
6106
6383
  outDir,
6107
6384
  androidDir,
@@ -6111,23 +6388,23 @@ async function buildEmbeddable(opts = {}) {
6111
6388
  modules,
6112
6389
  abiFilters
6113
6390
  );
6114
- const gradlewPath = path24.join(androidDir, "gradlew");
6391
+ const gradlewPath = path26.join(androidDir, "gradlew");
6115
6392
  const devAppDir = findDevAppPackage(projectRoot);
6116
6393
  const existingGradleDirs = [
6117
- path24.join(projectRoot, "android"),
6118
- devAppDir ? path24.join(devAppDir, "android") : null
6394
+ path26.join(projectRoot, "android"),
6395
+ devAppDir ? path26.join(devAppDir, "android") : null
6119
6396
  ].filter(Boolean);
6120
6397
  let hasWrapper = false;
6121
6398
  for (const d of existingGradleDirs) {
6122
- if (fs23.existsSync(path24.join(d, "gradlew"))) {
6399
+ if (fs25.existsSync(path26.join(d, "gradlew"))) {
6123
6400
  for (const name of ["gradlew", "gradlew.bat", "gradle"]) {
6124
- const src = path24.join(d, name);
6125
- if (fs23.existsSync(src)) {
6126
- const dest = path24.join(androidDir, name);
6127
- if (fs23.statSync(src).isDirectory()) {
6128
- fs23.cpSync(src, dest, { recursive: true });
6401
+ const src = path26.join(d, name);
6402
+ if (fs25.existsSync(src)) {
6403
+ const dest = path26.join(androidDir, name);
6404
+ if (fs25.statSync(src).isDirectory()) {
6405
+ fs25.cpSync(src, dest, { recursive: true });
6129
6406
  } else {
6130
- fs23.copyFileSync(src, dest);
6407
+ fs25.copyFileSync(src, dest);
6131
6408
  }
6132
6409
  }
6133
6410
  }
@@ -6146,10 +6423,10 @@ async function buildEmbeddable(opts = {}) {
6146
6423
  console.error("\u274C Android AAR build failed. Run manually: cd embeddable/android && ./gradlew :lib:assembleRelease");
6147
6424
  throw e;
6148
6425
  }
6149
- const aarSrc = path24.join(androidDir, "lib", "build", "outputs", "aar", "lib-release.aar");
6150
- const aarDest = path24.join(outDir, "tamer-embeddable.aar");
6151
- if (fs23.existsSync(aarSrc)) {
6152
- fs23.copyFileSync(aarSrc, aarDest);
6426
+ const aarSrc = path26.join(androidDir, "lib", "build", "outputs", "aar", "lib-release.aar");
6427
+ const aarDest = path26.join(outDir, "tamer-embeddable.aar");
6428
+ if (fs25.existsSync(aarSrc)) {
6429
+ fs25.copyFileSync(aarSrc, aarDest);
6153
6430
  console.log(` - tamer-embeddable.aar`);
6154
6431
  }
6155
6432
  const snippetAndroid = `// Add to your app's build.gradle:
@@ -6160,7 +6437,7 @@ async function buildEmbeddable(opts = {}) {
6160
6437
  // LynxEmbeddable.init(applicationContext)
6161
6438
  // val lynxView = LynxEmbeddable.buildLynxView(containerViewGroup)
6162
6439
  `;
6163
- fs23.writeFileSync(path24.join(outDir, "snippet-android.kt"), snippetAndroid);
6440
+ fs25.writeFileSync(path26.join(outDir, "snippet-android.kt"), snippetAndroid);
6164
6441
  generateIosPod(outDir, projectRoot, lynxBundlePath, lynxBundleFile, modules);
6165
6442
  const readme = `# Embeddable Lynx Bundle
6166
6443
 
@@ -6191,7 +6468,7 @@ Add the \`Podfile.snippet\` entries to your Podfile (inside your app target), th
6191
6468
 
6192
6469
  - [Embedding LynxView](https://lynxjs.org/guide/embed-lynx-to-native)
6193
6470
  `;
6194
- fs23.writeFileSync(path24.join(outDir, "README.md"), readme);
6471
+ fs25.writeFileSync(path26.join(outDir, "README.md"), readme);
6195
6472
  console.log(`
6196
6473
  \u2705 Embeddable output at ${outDir}/`);
6197
6474
  console.log(" - main.lynx.bundle");
@@ -6203,20 +6480,20 @@ Add the \`Podfile.snippet\` entries to your Podfile (inside your app target), th
6203
6480
  console.log(" - README.md");
6204
6481
  }
6205
6482
  function generateIosPod(outDir, projectRoot, lynxBundlePath, lynxBundleFile, modules) {
6206
- const iosDir = path24.join(outDir, "ios");
6207
- const podDir = path24.join(iosDir, "TamerEmbeddable");
6208
- const resourcesDir = path24.join(podDir, "Resources");
6209
- fs23.mkdirSync(resourcesDir, { recursive: true });
6210
- fs23.copyFileSync(lynxBundlePath, path24.join(resourcesDir, lynxBundleFile));
6483
+ const iosDir = path26.join(outDir, "ios");
6484
+ const podDir = path26.join(iosDir, "TamerEmbeddable");
6485
+ const resourcesDir = path26.join(podDir, "Resources");
6486
+ fs25.mkdirSync(resourcesDir, { recursive: true });
6487
+ fs25.copyFileSync(lynxBundlePath, path26.join(resourcesDir, lynxBundleFile));
6211
6488
  const iosModules = modules.filter((m) => m.config.ios);
6212
6489
  const podDeps = iosModules.map((p) => {
6213
6490
  const podspecPath = p.config.ios?.podspecPath || ".";
6214
- const podspecDir = path24.join(p.packagePath, podspecPath);
6215
- if (!fs23.existsSync(podspecDir)) return null;
6216
- const files = fs23.readdirSync(podspecDir);
6491
+ const podspecDir = path26.join(p.packagePath, podspecPath);
6492
+ if (!fs25.existsSync(podspecDir)) return null;
6493
+ const files = fs25.readdirSync(podspecDir);
6217
6494
  const podspecFile = files.find((f) => f.endsWith(".podspec"));
6218
6495
  const podName = podspecFile ? podspecFile.replace(".podspec", "") : p.name.split("/").pop().replace(/-/g, "");
6219
- const absPath = path24.resolve(podspecDir);
6496
+ const absPath = path26.resolve(podspecDir);
6220
6497
  return { podName, absPath };
6221
6498
  }).filter(Boolean);
6222
6499
  const podDepLines = podDeps.map((d) => ` s.dependency '${d.podName}'`).join("\n");
@@ -6256,9 +6533,9 @@ end
6256
6533
  });
6257
6534
  const swiftImports = iosModules.map((p) => {
6258
6535
  const podspecPath = p.config.ios?.podspecPath || ".";
6259
- const podspecDir = path24.join(p.packagePath, podspecPath);
6260
- if (!fs23.existsSync(podspecDir)) return null;
6261
- const files = fs23.readdirSync(podspecDir);
6536
+ const podspecDir = path26.join(p.packagePath, podspecPath);
6537
+ if (!fs25.existsSync(podspecDir)) return null;
6538
+ const files = fs25.readdirSync(podspecDir);
6262
6539
  const podspecFile = files.find((f) => f.endsWith(".podspec"));
6263
6540
  return podspecFile ? podspecFile.replace(".podspec", "") : null;
6264
6541
  }).filter(Boolean);
@@ -6277,17 +6554,17 @@ ${regBlock}
6277
6554
  }
6278
6555
  }
6279
6556
  `;
6280
- fs23.writeFileSync(path24.join(iosDir, "TamerEmbeddable.podspec"), podspecContent);
6281
- fs23.writeFileSync(path24.join(podDir, "LynxEmbeddable.swift"), lynxEmbeddableSwift);
6282
- const absIosDir = path24.resolve(iosDir);
6557
+ fs25.writeFileSync(path26.join(iosDir, "TamerEmbeddable.podspec"), podspecContent);
6558
+ fs25.writeFileSync(path26.join(podDir, "LynxEmbeddable.swift"), lynxEmbeddableSwift);
6559
+ const absIosDir = path26.resolve(iosDir);
6283
6560
  const podfileSnippet = `# Paste into your app target in Podfile:
6284
6561
 
6285
6562
  pod 'TamerEmbeddable', :path => '${absIosDir}'
6286
6563
  ${podDeps.map((d) => `pod '${d.podName}', :path => '${d.absPath}'`).join("\n")}
6287
6564
  `;
6288
- fs23.writeFileSync(path24.join(iosDir, "Podfile.snippet"), podfileSnippet);
6289
- fs23.writeFileSync(
6290
- path24.join(outDir, "snippet-ios.swift"),
6565
+ fs25.writeFileSync(path26.join(iosDir, "Podfile.snippet"), podfileSnippet);
6566
+ fs25.writeFileSync(
6567
+ path26.join(outDir, "snippet-ios.swift"),
6291
6568
  `// Add LynxEmbeddable.initEnvironment() in your AppDelegate/SceneDelegate before presenting LynxView.
6292
6569
  // Then create LynxView with your bundle URL (main.lynx.bundle is in the pod resources).
6293
6570
  `
@@ -6295,8 +6572,8 @@ ${podDeps.map((d) => `pod '${d.podName}', :path => '${d.absPath}'`).join("\n")}
6295
6572
  }
6296
6573
 
6297
6574
  // src/common/add.ts
6298
- import fs24 from "fs";
6299
- import path25 from "path";
6575
+ import fs26 from "fs";
6576
+ import path27 from "path";
6300
6577
  import { execFile, execSync as execSync10 } from "child_process";
6301
6578
  import { promisify } from "util";
6302
6579
  import semver from "semver";
@@ -6311,21 +6588,15 @@ var CORE_PACKAGES = [
6311
6588
  "@tamer4lynx/tamer-icons"
6312
6589
  ];
6313
6590
  var DEV_STACK_PACKAGES = [
6314
- "@tamer4lynx/jiggle",
6315
- "@tamer4lynx/tamer-app-shell",
6316
- "@tamer4lynx/tamer-biometric",
6317
6591
  "@tamer4lynx/tamer-dev-app",
6318
6592
  "@tamer4lynx/tamer-dev-client",
6319
- "@tamer4lynx/tamer-display-browser",
6593
+ "@tamer4lynx/tamer-app-shell",
6320
6594
  "@tamer4lynx/tamer-icons",
6321
6595
  "@tamer4lynx/tamer-insets",
6322
- "@tamer4lynx/tamer-linking",
6323
6596
  "@tamer4lynx/tamer-plugin",
6324
6597
  "@tamer4lynx/tamer-router",
6325
6598
  "@tamer4lynx/tamer-screen",
6326
- "@tamer4lynx/tamer-secure-store",
6327
- "@tamer4lynx/tamer-system-ui",
6328
- "@tamer4lynx/tamer-transports"
6599
+ "@tamer4lynx/tamer-system-ui"
6329
6600
  ];
6330
6601
  var PACKAGE_ALIASES = {};
6331
6602
  async function getHighestPublishedVersion(fullName) {
@@ -6353,9 +6624,9 @@ async function normalizeTamerInstallSpec(pkg) {
6353
6624
  return `${pkg}@prerelease`;
6354
6625
  }
6355
6626
  function detectPackageManager2(cwd) {
6356
- const dir = path25.resolve(cwd);
6357
- if (fs24.existsSync(path25.join(dir, "pnpm-lock.yaml"))) return "pnpm";
6358
- if (fs24.existsSync(path25.join(dir, "bun.lockb"))) return "bun";
6627
+ const dir = path27.resolve(cwd);
6628
+ if (fs26.existsSync(path27.join(dir, "pnpm-lock.yaml"))) return "pnpm";
6629
+ if (fs26.existsSync(path27.join(dir, "bun.lockb"))) return "bun";
6359
6630
  return "npm";
6360
6631
  }
6361
6632
  function runInstall(cwd, packages, pm) {
@@ -6407,13 +6678,13 @@ async function add(packages = []) {
6407
6678
  // src/common/signing.tsx
6408
6679
  import { useState as useState6, useEffect as useEffect4, useRef as useRef2 } from "react";
6409
6680
  import { render as render3, Text as Text10, Box as Box9 } from "ink";
6410
- import fs27 from "fs";
6411
- import path28 from "path";
6681
+ import fs29 from "fs";
6682
+ import path30 from "path";
6412
6683
 
6413
6684
  // src/common/androidKeystore.ts
6414
6685
  import { execFileSync } from "child_process";
6415
- import fs25 from "fs";
6416
- import path26 from "path";
6686
+ import fs27 from "fs";
6687
+ import path28 from "path";
6417
6688
  function normalizeJavaHome(raw) {
6418
6689
  if (!raw) return void 0;
6419
6690
  const t = raw.trim().replace(/^["']|["']$/g, "");
@@ -6426,7 +6697,7 @@ function discoverJavaHomeMacOs() {
6426
6697
  encoding: "utf8",
6427
6698
  stdio: ["pipe", "pipe", "pipe"]
6428
6699
  }).trim().split("\n")[0]?.trim();
6429
- if (out && fs25.existsSync(path26.join(out, "bin", "keytool"))) return out;
6700
+ if (out && fs27.existsSync(path28.join(out, "bin", "keytool"))) return out;
6430
6701
  } catch {
6431
6702
  }
6432
6703
  return void 0;
@@ -6436,13 +6707,13 @@ function resolveKeytoolPath() {
6436
6707
  const win = process.platform === "win32";
6437
6708
  const keytoolName = win ? "keytool.exe" : "keytool";
6438
6709
  if (jh) {
6439
- const p = path26.join(jh, "bin", keytoolName);
6440
- if (fs25.existsSync(p)) return p;
6710
+ const p = path28.join(jh, "bin", keytoolName);
6711
+ if (fs27.existsSync(p)) return p;
6441
6712
  }
6442
6713
  const mac = discoverJavaHomeMacOs();
6443
6714
  if (mac) {
6444
- const p = path26.join(mac, "bin", keytoolName);
6445
- if (fs25.existsSync(p)) return p;
6715
+ const p = path28.join(mac, "bin", keytoolName);
6716
+ if (fs27.existsSync(p)) return p;
6446
6717
  }
6447
6718
  return "keytool";
6448
6719
  }
@@ -6457,16 +6728,16 @@ function keytoolAvailable() {
6457
6728
  };
6458
6729
  if (tryRun("keytool")) return true;
6459
6730
  const fromJavaHome = resolveKeytoolPath();
6460
- if (fromJavaHome !== "keytool" && fs25.existsSync(fromJavaHome)) {
6731
+ if (fromJavaHome !== "keytool" && fs27.existsSync(fromJavaHome)) {
6461
6732
  return tryRun(fromJavaHome);
6462
6733
  }
6463
6734
  return false;
6464
6735
  }
6465
6736
  function generateReleaseKeystore(opts) {
6466
6737
  const keytool = resolveKeytoolPath();
6467
- const dir = path26.dirname(opts.keystoreAbsPath);
6468
- fs25.mkdirSync(dir, { recursive: true });
6469
- if (fs25.existsSync(opts.keystoreAbsPath)) {
6738
+ const dir = path28.dirname(opts.keystoreAbsPath);
6739
+ fs27.mkdirSync(dir, { recursive: true });
6740
+ if (fs27.existsSync(opts.keystoreAbsPath)) {
6470
6741
  throw new Error(`Keystore already exists: ${opts.keystoreAbsPath}`);
6471
6742
  }
6472
6743
  if (!opts.storePassword || !opts.keyPassword) {
@@ -6504,13 +6775,13 @@ function generateReleaseKeystore(opts) {
6504
6775
  }
6505
6776
 
6506
6777
  // src/common/appendEnvFile.ts
6507
- import fs26 from "fs";
6508
- import path27 from "path";
6778
+ import fs28 from "fs";
6779
+ import path29 from "path";
6509
6780
  import { parse } from "dotenv";
6510
6781
  function keysDefinedInFile(filePath) {
6511
- if (!fs26.existsSync(filePath)) return /* @__PURE__ */ new Set();
6782
+ if (!fs28.existsSync(filePath)) return /* @__PURE__ */ new Set();
6512
6783
  try {
6513
- return new Set(Object.keys(parse(fs26.readFileSync(filePath, "utf8"))));
6784
+ return new Set(Object.keys(parse(fs28.readFileSync(filePath, "utf8"))));
6514
6785
  } catch {
6515
6786
  return /* @__PURE__ */ new Set();
6516
6787
  }
@@ -6525,11 +6796,11 @@ function formatEnvLine(key, value) {
6525
6796
  function appendEnvVarsIfMissing(projectRoot, vars) {
6526
6797
  const entries = Object.entries(vars).filter(([, v]) => v !== void 0 && v !== "");
6527
6798
  if (entries.length === 0) return null;
6528
- const envLocal = path27.join(projectRoot, ".env.local");
6529
- const envDefault = path27.join(projectRoot, ".env");
6799
+ const envLocal = path29.join(projectRoot, ".env.local");
6800
+ const envDefault = path29.join(projectRoot, ".env");
6530
6801
  let target;
6531
- if (fs26.existsSync(envLocal)) target = envLocal;
6532
- else if (fs26.existsSync(envDefault)) target = envDefault;
6802
+ if (fs28.existsSync(envLocal)) target = envLocal;
6803
+ else if (fs28.existsSync(envDefault)) target = envDefault;
6533
6804
  else target = envLocal;
6534
6805
  const existing = keysDefinedInFile(target);
6535
6806
  const lines = [];
@@ -6541,20 +6812,20 @@ function appendEnvVarsIfMissing(projectRoot, vars) {
6541
6812
  }
6542
6813
  if (lines.length === 0) {
6543
6814
  return {
6544
- file: path27.basename(target),
6815
+ file: path29.basename(target),
6545
6816
  keys: [],
6546
6817
  skippedAll: entries.length > 0
6547
6818
  };
6548
6819
  }
6549
6820
  let prefix = "";
6550
- if (fs26.existsSync(target)) {
6551
- const cur = fs26.readFileSync(target, "utf8");
6821
+ if (fs28.existsSync(target)) {
6822
+ const cur = fs28.readFileSync(target, "utf8");
6552
6823
  prefix = cur.length === 0 ? "" : cur.endsWith("\n") ? cur : `${cur}
6553
6824
  `;
6554
6825
  }
6555
6826
  const block = lines.join("\n") + "\n";
6556
- fs26.writeFileSync(target, prefix + block, "utf8");
6557
- return { file: path27.basename(target), keys: appendedKeys };
6827
+ fs28.writeFileSync(target, prefix + block, "utf8");
6828
+ return { file: path29.basename(target), keys: appendedKeys };
6558
6829
  }
6559
6830
 
6560
6831
  // src/common/signing.tsx
@@ -6660,7 +6931,7 @@ function SigningWizard({ platform: initialPlatform }) {
6660
6931
  try {
6661
6932
  const resolved = resolveHostPaths();
6662
6933
  const rel = state.android.genKeystorePath.trim() || "android/release.keystore";
6663
- abs = path28.isAbsolute(rel) ? rel : path28.join(resolved.projectRoot, rel);
6934
+ abs = path30.isAbsolute(rel) ? rel : path30.join(resolved.projectRoot, rel);
6664
6935
  const alias = state.android.keyAlias.trim() || "release";
6665
6936
  const pw = state.android.genPassword;
6666
6937
  const pkg = resolved.config.android?.packageName ?? "com.example.app";
@@ -6687,7 +6958,7 @@ function SigningWizard({ platform: initialPlatform }) {
6687
6958
  }));
6688
6959
  } catch (e) {
6689
6960
  const msg = e.message;
6690
- if (abs && fs27.existsSync(abs) && (msg.includes("already exists") || msg.includes("Keystore already exists"))) {
6961
+ if (abs && fs29.existsSync(abs) && (msg.includes("already exists") || msg.includes("Keystore already exists"))) {
6691
6962
  if (cancelled || runId !== generateRunId.current) return;
6692
6963
  const rel = state.android.genKeystorePath.trim() || "android/release.keystore";
6693
6964
  const alias = state.android.keyAlias.trim() || "release";
@@ -6727,11 +6998,11 @@ function SigningWizard({ platform: initialPlatform }) {
6727
6998
  const saveConfig = async () => {
6728
6999
  try {
6729
7000
  const resolved = resolveHostPaths();
6730
- const configPath = path28.join(resolved.projectRoot, "tamer.config.json");
7001
+ const configPath = path30.join(resolved.projectRoot, "tamer.config.json");
6731
7002
  let config = {};
6732
7003
  let androidEnvAppend = null;
6733
- if (fs27.existsSync(configPath)) {
6734
- config = JSON.parse(fs27.readFileSync(configPath, "utf8"));
7004
+ if (fs29.existsSync(configPath)) {
7005
+ config = JSON.parse(fs29.readFileSync(configPath, "utf8"));
6735
7006
  }
6736
7007
  if (state.platform === "android" || state.platform === "both") {
6737
7008
  config.android = config.android || {};
@@ -6758,10 +7029,10 @@ function SigningWizard({ platform: initialPlatform }) {
6758
7029
  ...state.ios.provisioningProfileSpecifier && { provisioningProfileSpecifier: state.ios.provisioningProfileSpecifier }
6759
7030
  };
6760
7031
  }
6761
- fs27.writeFileSync(configPath, JSON.stringify(config, null, 2));
6762
- const gitignorePath = path28.join(resolved.projectRoot, ".gitignore");
6763
- if (fs27.existsSync(gitignorePath)) {
6764
- let gitignore = fs27.readFileSync(gitignorePath, "utf8");
7032
+ fs29.writeFileSync(configPath, JSON.stringify(config, null, 2));
7033
+ const gitignorePath = path30.join(resolved.projectRoot, ".gitignore");
7034
+ if (fs29.existsSync(gitignorePath)) {
7035
+ let gitignore = fs29.readFileSync(gitignorePath, "utf8");
6765
7036
  const additions = [
6766
7037
  ".env.local",
6767
7038
  "*.jks",
@@ -6774,7 +7045,7 @@ ${addition}
6774
7045
  `;
6775
7046
  }
6776
7047
  }
6777
- fs27.writeFileSync(gitignorePath, gitignore);
7048
+ fs29.writeFileSync(gitignorePath, gitignore);
6778
7049
  }
6779
7050
  setState((s) => ({
6780
7051
  ...s,
@@ -7032,13 +7303,13 @@ async function signing(platform) {
7032
7303
  }
7033
7304
 
7034
7305
  // src/common/productionSigning.ts
7035
- import fs28 from "fs";
7036
- import path29 from "path";
7306
+ import fs30 from "fs";
7307
+ import path31 from "path";
7037
7308
  function isAndroidSigningConfigured(resolved) {
7038
7309
  const signing2 = resolved.config.android?.signing;
7039
7310
  const hasConfig = Boolean(signing2?.keystoreFile?.trim() && signing2?.keyAlias?.trim());
7040
- const signingProps = path29.join(resolved.androidDir, "signing.properties");
7041
- const hasProps = fs28.existsSync(signingProps);
7311
+ const signingProps = path31.join(resolved.androidDir, "signing.properties");
7312
+ const hasProps = fs30.existsSync(signingProps);
7042
7313
  return hasConfig || hasProps;
7043
7314
  }
7044
7315
  function isIosSigningConfigured(resolved) {
@@ -7068,14 +7339,7 @@ function assertProductionSigningReady(filter) {
7068
7339
  }
7069
7340
 
7070
7341
  // index.ts
7071
- function readCliVersion() {
7072
- const root = path30.dirname(fileURLToPath(import.meta.url));
7073
- const here = path30.join(root, "package.json");
7074
- const parent = path30.join(root, "..", "package.json");
7075
- const pkgPath = fs29.existsSync(here) ? here : parent;
7076
- return JSON.parse(fs29.readFileSync(pkgPath, "utf8")).version;
7077
- }
7078
- var version = readCliVersion();
7342
+ var version = getCliVersion();
7079
7343
  function validateBuildMode(debug, release, production) {
7080
7344
  const modes = [debug, release, production].filter(Boolean).length;
7081
7345
  if (modes > 1) {
@@ -7114,7 +7378,10 @@ program.command("create <target>").description("Create a project or extension. T
7114
7378
  console.error(`Invalid create target: ${target}. Use ios | android | module | element | service | combo`);
7115
7379
  process.exit(1);
7116
7380
  });
7117
- 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 (unsigned)").option("-p, --production", "Production build for app store (signed)").option("-i, --install", "Install after building").action(async (platform, opts) => {
7381
+ 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 (unsigned)").option("-p, --production", "Production build for app store (signed)").option(
7382
+ "-i, --install",
7383
+ "Install after build (iOS: simulator with -d; connected device with -p)"
7384
+ ).option("-C, --clean", "Run Gradle clean before Android build (fixes stubborn caches)").action(async (platform, opts) => {
7118
7385
  validateBuildMode(opts.debug, opts.release, opts.production);
7119
7386
  const release = opts.release === true || opts.production === true;
7120
7387
  const production = opts.production === true;
@@ -7127,7 +7394,7 @@ program.command("build [platform]").description("Build app. Platform: ios | andr
7127
7394
  assertProductionSigningReady(p);
7128
7395
  }
7129
7396
  if (p === "android" || p === "all") {
7130
- await build_default({ install: opts.install, release, production });
7397
+ await build_default({ install: opts.install, release, production, clean: opts.clean });
7131
7398
  }
7132
7399
  if (p === "ios" || p === "all") {
7133
7400
  await build_default2({ install: opts.install, release, production });
@@ -7216,7 +7483,7 @@ program.command("signing [platform]").description("Configure Android and iOS sig
7216
7483
  program.command("codegen").description("Generate code from @lynxmodule declarations").action(() => {
7217
7484
  codegen_default();
7218
7485
  });
7219
- 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("-p, --production", "Bundle/build: production for app store (signed)").option("-i, --install", "Install after build").option("-e, --embeddable", "Build embeddable").option("-f, --force", "Force (inject)").action(async (subcommand, opts) => {
7486
+ 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("-p, --production", "Bundle/build: production for app store (signed)").option("-i, --install", "Install after build").option("-C, --clean", "Run Gradle clean before Android build").option("-e, --embeddable", "Build embeddable").option("-f, --force", "Force (inject)").action(async (subcommand, opts) => {
7220
7487
  const sub = subcommand?.toLowerCase();
7221
7488
  if (sub === "create") {
7222
7489
  if (opts.debug && opts.release) {
@@ -7242,7 +7509,12 @@ program.command("android <subcommand>").description("(Legacy) Use: t4l <command>
7242
7509
  if (opts.embeddable) await buildEmbeddable({ release: true });
7243
7510
  else {
7244
7511
  if (opts.production === true) assertProductionSigningReady("android");
7245
- await build_default({ install: opts.install, release, production: opts.production === true });
7512
+ await build_default({
7513
+ install: opts.install,
7514
+ release,
7515
+ production: opts.production === true,
7516
+ clean: opts.clean
7517
+ });
7246
7518
  }
7247
7519
  return;
7248
7520
  }
@@ -7291,10 +7563,10 @@ program.command("ios <subcommand>").description("(Legacy) Use: t4l <command> ios
7291
7563
  process.exit(1);
7292
7564
  });
7293
7565
  program.command("autolink-toggle").alias("autolink").description("Toggle autolink on/off in tamer.config.json (controls postinstall linking)").action(async () => {
7294
- const configPath = path30.join(process.cwd(), "tamer.config.json");
7566
+ const configPath = path32.join(process.cwd(), "tamer.config.json");
7295
7567
  let config = {};
7296
- if (fs29.existsSync(configPath)) {
7297
- config = JSON.parse(fs29.readFileSync(configPath, "utf8"));
7568
+ if (fs31.existsSync(configPath)) {
7569
+ config = JSON.parse(fs31.readFileSync(configPath, "utf8"));
7298
7570
  }
7299
7571
  if (config.autolink) {
7300
7572
  delete config.autolink;
@@ -7303,7 +7575,7 @@ program.command("autolink-toggle").alias("autolink").description("Toggle autolin
7303
7575
  config.autolink = true;
7304
7576
  console.log("Autolink enabled in tamer.config.json");
7305
7577
  }
7306
- fs29.writeFileSync(configPath, JSON.stringify(config, null, 2));
7578
+ fs31.writeFileSync(configPath, JSON.stringify(config, null, 2));
7307
7579
  console.log(`Updated ${configPath}`);
7308
7580
  });
7309
7581
  if (process.argv.length <= 2 || process.argv.length === 3 && process.argv[2] === "init") {