@tamer4lynx/cli 0.0.13 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +18 -0
  2. package/dist/index.js +2110 -764
  3. package/package.json +10 -1
package/dist/index.js CHANGED
@@ -7,8 +7,8 @@ process.on("warning", (w) => {
7
7
  });
8
8
 
9
9
  // index.ts
10
- import fs24 from "fs";
11
- import path25 from "path";
10
+ import fs29 from "fs";
11
+ import path30 from "path";
12
12
  import { fileURLToPath } from "url";
13
13
  import { program } from "commander";
14
14
 
@@ -2229,8 +2229,8 @@ $1$2`
2229
2229
  var autolink_default = autolink;
2230
2230
 
2231
2231
  // src/android/bundle.ts
2232
- import fs10 from "fs";
2233
- import path10 from "path";
2232
+ import fs11 from "fs";
2233
+ import path11 from "path";
2234
2234
  import { execSync as execSync3 } from "child_process";
2235
2235
 
2236
2236
  // src/common/copyDistAssets.ts
@@ -2256,19 +2256,117 @@ function copyDistAssets(distDir, destDir, bundleFile) {
2256
2256
  }
2257
2257
  }
2258
2258
 
2259
- // src/android/syncDevClient.ts
2259
+ // src/common/tsconfigUtils.ts
2260
2260
  import fs9 from "fs";
2261
2261
  import path9 from "path";
2262
+ function stripJsonCommentsAndTrailingCommas(str) {
2263
+ return str.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "").replace(/,\s*([\]}])/g, "$1");
2264
+ }
2265
+ function parseTsconfigJson(raw) {
2266
+ try {
2267
+ return JSON.parse(raw);
2268
+ } catch {
2269
+ return JSON.parse(stripJsonCommentsAndTrailingCommas(raw));
2270
+ }
2271
+ }
2272
+ function readTsconfig(filePath) {
2273
+ if (!fs9.existsSync(filePath)) return null;
2274
+ try {
2275
+ return parseTsconfigJson(fs9.readFileSync(filePath, "utf-8"));
2276
+ } catch {
2277
+ return null;
2278
+ }
2279
+ }
2280
+ function resolveExtends(tsconfig, dir) {
2281
+ if (!tsconfig.extends) return tsconfig;
2282
+ const basePath = path9.resolve(dir, tsconfig.extends);
2283
+ const base = readTsconfig(basePath);
2284
+ if (!base) return tsconfig;
2285
+ const baseDir = path9.dirname(basePath);
2286
+ const resolved = resolveExtends(base, baseDir);
2287
+ return {
2288
+ ...resolved,
2289
+ ...tsconfig,
2290
+ compilerOptions: { ...resolved.compilerOptions, ...tsconfig.compilerOptions }
2291
+ };
2292
+ }
2293
+ function fixTsconfigReferencesForBuild(tsconfigPath) {
2294
+ const dir = path9.dirname(tsconfigPath);
2295
+ const tsconfig = readTsconfig(tsconfigPath);
2296
+ if (!tsconfig?.references?.length) return false;
2297
+ const refsWithNoEmit = [];
2298
+ 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");
2301
+ const refConfig = readTsconfig(refConfigPath);
2302
+ if (refConfig?.compilerOptions?.noEmit === true) {
2303
+ refsWithNoEmit.push(refConfigPath);
2304
+ }
2305
+ }
2306
+ if (refsWithNoEmit.length === 0) return false;
2307
+ const merged = {
2308
+ ...tsconfig,
2309
+ references: void 0,
2310
+ files: void 0
2311
+ };
2312
+ const includes = [];
2313
+ const compilerOpts = { ...tsconfig.compilerOptions };
2314
+ 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");
2317
+ const refConfig = readTsconfig(refConfigPath);
2318
+ if (!refConfig) continue;
2319
+ const refDir = path9.dirname(refConfigPath);
2320
+ const resolved = resolveExtends(refConfig, refDir);
2321
+ const inc = resolved.include;
2322
+ if (inc) {
2323
+ const arr = Array.isArray(inc) ? inc : [inc];
2324
+ const baseDir = path9.relative(dir, refDir);
2325
+ for (const p of arr) {
2326
+ const clean = typeof p === "string" && p.startsWith("./") ? p.slice(2) : p;
2327
+ includes.push(!baseDir || baseDir === "." ? clean : `${baseDir}/${clean}`);
2328
+ }
2329
+ }
2330
+ const opts = resolved.compilerOptions;
2331
+ if (opts) {
2332
+ for (const [k, v] of Object.entries(opts)) {
2333
+ if (k !== "composite" && k !== "noEmit" && compilerOpts[k] === void 0) {
2334
+ compilerOpts[k] = v;
2335
+ }
2336
+ }
2337
+ }
2338
+ }
2339
+ if (includes.length > 0) merged.include = [...new Set(includes)];
2340
+ compilerOpts.noEmit = true;
2341
+ merged.compilerOptions = compilerOpts;
2342
+ fs9.writeFileSync(tsconfigPath, JSON.stringify(merged, null, 2));
2343
+ return true;
2344
+ }
2345
+ function addTamerTypesInclude(tsconfigPath, tamerTypesInclude) {
2346
+ const tsconfig = readTsconfig(tsconfigPath);
2347
+ if (!tsconfig) return false;
2348
+ const include = tsconfig.include ?? [];
2349
+ const arr = Array.isArray(include) ? include : [include];
2350
+ if (arr.some((p) => (typeof p === "string" ? p : "").includes("tamer-"))) return false;
2351
+ arr.push(tamerTypesInclude);
2352
+ tsconfig.include = arr;
2353
+ fs9.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
2354
+ return true;
2355
+ }
2356
+
2357
+ // src/android/syncDevClient.ts
2358
+ import fs10 from "fs";
2359
+ import path10 from "path";
2262
2360
  function readAndSubstituteTemplate2(templatePath, vars) {
2263
- const raw = fs9.readFileSync(templatePath, "utf-8");
2361
+ const raw = fs10.readFileSync(templatePath, "utf-8");
2264
2362
  return Object.entries(vars).reduce(
2265
2363
  (s, [k, v]) => s.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), v),
2266
2364
  raw
2267
2365
  );
2268
2366
  }
2269
2367
  function patchAppLogService(appPath) {
2270
- if (!fs9.existsSync(appPath)) return;
2271
- const raw = fs9.readFileSync(appPath, "utf-8");
2368
+ if (!fs10.existsSync(appPath)) return;
2369
+ const raw = fs10.readFileSync(appPath, "utf-8");
2272
2370
  const patched = raw.replace(
2273
2371
  /private void initLynxService\(\)\s*\{[\s\S]*?\n\s*}\s*\n\s*private void initFresco\(\)/,
2274
2372
  `private void initLynxService() {
@@ -2287,7 +2385,7 @@ function patchAppLogService(appPath) {
2287
2385
  private void initFresco()`
2288
2386
  );
2289
2387
  if (patched !== raw) {
2290
- fs9.writeFileSync(appPath, patched);
2388
+ fs10.writeFileSync(appPath, patched);
2291
2389
  }
2292
2390
  }
2293
2391
  async function syncDevClient(opts) {
@@ -2302,9 +2400,9 @@ async function syncDevClient(opts) {
2302
2400
  const packageName = config.android?.packageName;
2303
2401
  const appName = config.android?.appName;
2304
2402
  const packagePath = packageName.replace(/\./g, "/");
2305
- const javaDir = path9.join(rootDir, "app", "src", "main", "java", packagePath);
2306
- const kotlinDir = path9.join(rootDir, "app", "src", "main", "kotlin", packagePath);
2307
- if (!fs9.existsSync(javaDir) || !fs9.existsSync(kotlinDir)) {
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)) {
2308
2406
  console.error("\u274C Android project not found. Run `tamer android create` first.");
2309
2407
  process.exit(1);
2310
2408
  }
@@ -2320,14 +2418,14 @@ async function syncDevClient(opts) {
2320
2418
  const [templateProviderSource] = await Promise.all([
2321
2419
  fetchAndPatchTemplateProvider(vars)
2322
2420
  ]);
2323
- fs9.writeFileSync(path9.join(javaDir, "TemplateProvider.java"), templateProviderSource);
2324
- fs9.writeFileSync(path9.join(kotlinDir, "MainActivity.kt"), getStandaloneMainActivity(vars));
2325
- patchAppLogService(path9.join(javaDir, "App.java"));
2326
- const appDir = path9.join(rootDir, "app");
2327
- const mainDir = path9.join(appDir, "src", "main");
2328
- const manifestPath = path9.join(mainDir, "AndroidManifest.xml");
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");
2329
2427
  if (hasDevClient) {
2330
- const templateDir = path9.join(devClientPkg, "android", "templates");
2428
+ const templateDir = path10.join(devClientPkg, "android", "templates");
2331
2429
  const templateVars = { PACKAGE_NAME: packageName, APP_NAME: appName };
2332
2430
  const devClientFiles = [
2333
2431
  "DevClientManager.kt",
@@ -2336,13 +2434,13 @@ async function syncDevClient(opts) {
2336
2434
  "PortraitCaptureActivity.kt"
2337
2435
  ];
2338
2436
  for (const f of devClientFiles) {
2339
- const src = path9.join(templateDir, f);
2340
- if (fs9.existsSync(src)) {
2437
+ const src = path10.join(templateDir, f);
2438
+ if (fs10.existsSync(src)) {
2341
2439
  const content = readAndSubstituteTemplate2(src, templateVars);
2342
- fs9.writeFileSync(path9.join(kotlinDir, f), content);
2440
+ fs10.writeFileSync(path10.join(kotlinDir, f), content);
2343
2441
  }
2344
2442
  }
2345
- let manifest = fs9.readFileSync(manifestPath, "utf-8");
2443
+ let manifest = fs10.readFileSync(manifestPath, "utf-8");
2346
2444
  const projectActivityEntry = ' <activity android:name=".ProjectActivity" android:exported="false" android:taskAffinity="" android:launchMode="singleTask" android:documentLaunchMode="always" android:windowSoftInputMode="adjustResize" />';
2347
2445
  const portraitCaptureEntry = ' <activity android:name=".PortraitCaptureActivity" android:screenOrientation="portrait" android:stateNotNeeded="true" android:theme="@style/zxing_CaptureTheme" android:windowSoftInputMode="stateAlwaysHidden" />';
2348
2446
  if (!manifest.includes("ProjectActivity")) {
@@ -2364,16 +2462,16 @@ $1$2`);
2364
2462
  '$1 android:windowSoftInputMode="adjustResize"$2'
2365
2463
  );
2366
2464
  }
2367
- fs9.writeFileSync(manifestPath, manifest);
2465
+ fs10.writeFileSync(manifestPath, manifest);
2368
2466
  console.log("\u2705 Synced dev client (TemplateProvider, MainActivity, ProjectActivity, DevClientManager)");
2369
2467
  } else {
2370
2468
  for (const f of ["DevClientManager.kt", "DevServerPrefs.kt", "ProjectActivity.kt", "PortraitCaptureActivity.kt", "DevLauncherActivity.kt"]) {
2371
2469
  try {
2372
- fs9.rmSync(path9.join(kotlinDir, f));
2470
+ fs10.rmSync(path10.join(kotlinDir, f));
2373
2471
  } catch {
2374
2472
  }
2375
2473
  }
2376
- let manifest = fs9.readFileSync(manifestPath, "utf-8");
2474
+ let manifest = fs10.readFileSync(manifestPath, "utf-8");
2377
2475
  manifest = manifest.replace(/\s*<activity android:name="\.ProjectActivity"[^\/]*\/>\n?/g, "");
2378
2476
  manifest = manifest.replace(/\s*<activity android:name="\.PortraitCaptureActivity"[^\/]*\/>\n?/g, "");
2379
2477
  const mainActivityTag = manifest.match(/<activity[^>]*android:name="\.MainActivity"[^>]*>/);
@@ -2383,7 +2481,7 @@ $1$2`);
2383
2481
  '$1 android:windowSoftInputMode="adjustResize"$2'
2384
2482
  );
2385
2483
  }
2386
- fs9.writeFileSync(manifestPath, manifest);
2484
+ fs10.writeFileSync(manifestPath, manifest);
2387
2485
  console.log("\u2705 Synced (dev client disabled - use -d for debug build with dev client)");
2388
2486
  }
2389
2487
  }
@@ -2391,7 +2489,7 @@ var syncDevClient_default = syncDevClient;
2391
2489
 
2392
2490
  // src/android/bundle.ts
2393
2491
  async function bundleAndDeploy(opts = {}) {
2394
- const release = opts.release === true;
2492
+ const release = opts.release === true || opts.production === true;
2395
2493
  let resolved;
2396
2494
  try {
2397
2495
  resolved = resolveHostPaths();
@@ -2407,13 +2505,17 @@ async function bundleAndDeploy(opts = {}) {
2407
2505
  await syncDevClient_default({ includeDevClient });
2408
2506
  const iconPaths = resolveIconPaths(projectRoot, resolved.config);
2409
2507
  if (iconPaths) {
2410
- const resDir = path10.join(resolved.androidAppDir, "src", "main", "res");
2508
+ const resDir = path11.join(resolved.androidAppDir, "src", "main", "res");
2411
2509
  if (applyAndroidLauncherIcons(resDir, iconPaths)) {
2412
2510
  console.log("\u2705 Synced Android launcher icon(s) from tamer.config.json");
2413
- ensureAndroidManifestLauncherIcon(path10.join(resolved.androidAppDir, "src", "main", "AndroidManifest.xml"));
2511
+ ensureAndroidManifestLauncherIcon(path11.join(resolved.androidAppDir, "src", "main", "AndroidManifest.xml"));
2414
2512
  }
2415
2513
  }
2416
2514
  try {
2515
+ const lynxTsconfig = path11.join(lynxProjectDir, "tsconfig.json");
2516
+ if (fs11.existsSync(lynxTsconfig)) {
2517
+ fixTsconfigReferencesForBuild(lynxTsconfig);
2518
+ }
2417
2519
  console.log("\u{1F4E6} Building Lynx bundle...");
2418
2520
  execSync3("npm run build", { stdio: "inherit", cwd: lynxProjectDir });
2419
2521
  console.log("\u2705 Build completed successfully.");
@@ -2421,8 +2523,8 @@ async function bundleAndDeploy(opts = {}) {
2421
2523
  console.error("\u274C Build process failed.");
2422
2524
  process.exit(1);
2423
2525
  }
2424
- if (includeDevClient && devClientBundlePath && !fs10.existsSync(devClientBundlePath)) {
2425
- const devClientDir = path10.dirname(path10.dirname(devClientBundlePath));
2526
+ if (includeDevClient && devClientBundlePath && !fs11.existsSync(devClientBundlePath)) {
2527
+ const devClientDir = path11.dirname(path11.dirname(devClientBundlePath));
2426
2528
  try {
2427
2529
  console.log("\u{1F4E6} Building dev launcher (tamer-dev-client)...");
2428
2530
  execSync3("npm run build", { stdio: "inherit", cwd: devClientDir });
@@ -2433,22 +2535,22 @@ async function bundleAndDeploy(opts = {}) {
2433
2535
  }
2434
2536
  }
2435
2537
  try {
2436
- fs10.mkdirSync(destinationDir, { recursive: true });
2538
+ fs11.mkdirSync(destinationDir, { recursive: true });
2437
2539
  if (release) {
2438
- const devClientAsset = path10.join(destinationDir, "dev-client.lynx.bundle");
2439
- if (fs10.existsSync(devClientAsset)) {
2440
- fs10.rmSync(devClientAsset);
2540
+ const devClientAsset = path11.join(destinationDir, "dev-client.lynx.bundle");
2541
+ if (fs11.existsSync(devClientAsset)) {
2542
+ fs11.rmSync(devClientAsset);
2441
2543
  console.log(`\u2728 Removed dev-client.lynx.bundle from assets (production build)`);
2442
2544
  }
2443
- } else if (includeDevClient && devClientBundlePath && fs10.existsSync(devClientBundlePath)) {
2444
- fs10.copyFileSync(devClientBundlePath, path10.join(destinationDir, "dev-client.lynx.bundle"));
2545
+ } else if (includeDevClient && devClientBundlePath && fs11.existsSync(devClientBundlePath)) {
2546
+ fs11.copyFileSync(devClientBundlePath, path11.join(destinationDir, "dev-client.lynx.bundle"));
2445
2547
  console.log(`\u2728 Copied dev-client.lynx.bundle to assets`);
2446
2548
  }
2447
- if (!fs10.existsSync(lynxBundlePath)) {
2549
+ if (!fs11.existsSync(lynxBundlePath)) {
2448
2550
  console.error(`\u274C Build output not found at: ${lynxBundlePath}`);
2449
2551
  process.exit(1);
2450
2552
  }
2451
- const distDir = path10.dirname(lynxBundlePath);
2553
+ const distDir = path11.dirname(lynxBundlePath);
2452
2554
  copyDistAssets(distDir, destinationDir, resolved.lynxBundleFile);
2453
2555
  console.log(`\u2728 Copied ${resolved.lynxBundleFile} to assets`);
2454
2556
  } catch (error) {
@@ -2459,7 +2561,7 @@ async function bundleAndDeploy(opts = {}) {
2459
2561
  var bundle_default = bundleAndDeploy;
2460
2562
 
2461
2563
  // src/android/build.ts
2462
- import path11 from "path";
2564
+ import path12 from "path";
2463
2565
  import { execSync as execSync4 } from "child_process";
2464
2566
  async function buildApk(opts = {}) {
2465
2567
  let resolved;
@@ -2468,10 +2570,11 @@ async function buildApk(opts = {}) {
2468
2570
  } catch (error) {
2469
2571
  throw error;
2470
2572
  }
2471
- await bundle_default({ release: opts.release });
2573
+ const release = opts.release === true || opts.production === true;
2574
+ await bundle_default({ release, production: opts.production });
2472
2575
  const androidDir = resolved.androidDir;
2473
- const gradlew = path11.join(androidDir, process.platform === "win32" ? "gradlew.bat" : "gradlew");
2474
- const variant = opts.release ? "Release" : "Debug";
2576
+ const gradlew = path12.join(androidDir, process.platform === "win32" ? "gradlew.bat" : "gradlew");
2577
+ const variant = release ? "Release" : "Debug";
2475
2578
  const task = opts.install ? `install${variant}` : `assemble${variant}`;
2476
2579
  console.log(`
2477
2580
  \u{1F528} Building ${variant.toLowerCase()} APK${opts.install ? " and installing" : ""}...`);
@@ -2495,13 +2598,13 @@ async function buildApk(opts = {}) {
2495
2598
  var build_default = buildApk;
2496
2599
 
2497
2600
  // src/ios/create.ts
2498
- import fs12 from "fs";
2499
- import path13 from "path";
2601
+ import fs13 from "fs";
2602
+ import path14 from "path";
2500
2603
 
2501
2604
  // src/ios/getPod.ts
2502
2605
  import { execSync as execSync5 } from "child_process";
2503
- import fs11 from "fs";
2504
- import path12 from "path";
2606
+ import fs12 from "fs";
2607
+ import path13 from "path";
2505
2608
  function isCocoaPodsInstalled() {
2506
2609
  try {
2507
2610
  execSync5("command -v pod >/dev/null 2>&1");
@@ -2523,8 +2626,8 @@ async function setupCocoaPods(rootDir) {
2523
2626
  }
2524
2627
  try {
2525
2628
  console.log("\u{1F4E6} CocoaPods is installed. Proceeding with dependency installation...");
2526
- const podfilePath = path12.join(rootDir, "Podfile");
2527
- if (!fs11.existsSync(podfilePath)) {
2629
+ const podfilePath = path13.join(rootDir, "Podfile");
2630
+ if (!fs12.existsSync(podfilePath)) {
2528
2631
  throw new Error(`Podfile not found at ${podfilePath}`);
2529
2632
  }
2530
2633
  console.log(`\u{1F680} Executing pod install in: ${rootDir}`);
@@ -2550,7 +2653,7 @@ async function setupCocoaPods(rootDir) {
2550
2653
  // src/ios/create.ts
2551
2654
  import { randomBytes } from "crypto";
2552
2655
  function readAndSubstituteTemplate3(templatePath, vars) {
2553
- const raw = fs12.readFileSync(templatePath, "utf-8");
2656
+ const raw = fs13.readFileSync(templatePath, "utf-8");
2554
2657
  return Object.entries(vars).reduce(
2555
2658
  (s, [k, v]) => s.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), v),
2556
2659
  raw
@@ -2573,17 +2676,17 @@ var create2 = () => {
2573
2676
  process.exit(1);
2574
2677
  }
2575
2678
  const iosDir = config.paths?.iosDir ?? "ios";
2576
- const rootDir = path13.join(process.cwd(), iosDir);
2577
- const projectDir = path13.join(rootDir, appName);
2578
- const xcodeprojDir = path13.join(rootDir, `${appName}.xcodeproj`);
2679
+ const rootDir = path14.join(process.cwd(), iosDir);
2680
+ const projectDir = path14.join(rootDir, appName);
2681
+ const xcodeprojDir = path14.join(rootDir, `${appName}.xcodeproj`);
2579
2682
  const bridgingHeader = `${appName}-Bridging-Header.h`;
2580
2683
  function writeFile2(filePath, content) {
2581
- fs12.mkdirSync(path13.dirname(filePath), { recursive: true });
2582
- fs12.writeFileSync(filePath, content.trimStart(), "utf8");
2684
+ fs13.mkdirSync(path14.dirname(filePath), { recursive: true });
2685
+ fs13.writeFileSync(filePath, content.trimStart(), "utf8");
2583
2686
  }
2584
- if (fs12.existsSync(rootDir)) {
2687
+ if (fs13.existsSync(rootDir)) {
2585
2688
  console.log(`\u{1F9F9} Removing existing directory: ${rootDir}`);
2586
- fs12.rmSync(rootDir, { recursive: true, force: true });
2689
+ fs13.rmSync(rootDir, { recursive: true, force: true });
2587
2690
  }
2588
2691
  console.log(`\u{1F680} Creating a new Tamer4Lynx project in: ${rootDir}`);
2589
2692
  const ids = {
@@ -2619,7 +2722,7 @@ var create2 = () => {
2619
2722
  targetDebugConfig: generateId(),
2620
2723
  targetReleaseConfig: generateId()
2621
2724
  };
2622
- writeFile2(path13.join(rootDir, "Podfile"), `
2725
+ writeFile2(path14.join(rootDir, "Podfile"), `
2623
2726
  source 'https://cdn.cocoapods.org/'
2624
2727
 
2625
2728
  platform :ios, '13.0'
@@ -2704,15 +2807,15 @@ end
2704
2807
  const hostPkg = findTamerHostPackage(process.cwd());
2705
2808
  const templateVars = { PACKAGE_NAME: bundleId, APP_NAME: appName, BUNDLE_ID: bundleId };
2706
2809
  if (hostPkg) {
2707
- const templateDir = path13.join(hostPkg, "ios", "templates");
2810
+ const templateDir = path14.join(hostPkg, "ios", "templates");
2708
2811
  for (const f of ["AppDelegate.swift", "SceneDelegate.swift", "ViewController.swift", "LynxProvider.swift", "LynxInitProcessor.swift"]) {
2709
- const srcPath = path13.join(templateDir, f);
2710
- if (fs12.existsSync(srcPath)) {
2711
- writeFile2(path13.join(projectDir, f), readAndSubstituteTemplate3(srcPath, templateVars));
2812
+ const srcPath = path14.join(templateDir, f);
2813
+ if (fs13.existsSync(srcPath)) {
2814
+ writeFile2(path14.join(projectDir, f), readAndSubstituteTemplate3(srcPath, templateVars));
2712
2815
  }
2713
2816
  }
2714
2817
  } else {
2715
- writeFile2(path13.join(projectDir, "AppDelegate.swift"), `
2818
+ writeFile2(path14.join(projectDir, "AppDelegate.swift"), `
2716
2819
  import UIKit
2717
2820
 
2718
2821
  @UIApplicationMain
@@ -2727,7 +2830,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
2727
2830
  }
2728
2831
  }
2729
2832
  `);
2730
- writeFile2(path13.join(projectDir, "SceneDelegate.swift"), `
2833
+ writeFile2(path14.join(projectDir, "SceneDelegate.swift"), `
2731
2834
  import UIKit
2732
2835
 
2733
2836
  class SceneDelegate: UIResponder, UIWindowSceneDelegate {
@@ -2741,7 +2844,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
2741
2844
  }
2742
2845
  }
2743
2846
  `);
2744
- writeFile2(path13.join(projectDir, "ViewController.swift"), `
2847
+ writeFile2(path14.join(projectDir, "ViewController.swift"), `
2745
2848
  import UIKit
2746
2849
  import Lynx
2747
2850
  import tamerinsets
@@ -2757,7 +2860,9 @@ class ViewController: UIViewController {
2757
2860
  additionalSafeAreaInsets = .zero
2758
2861
  view.insetsLayoutMarginsFromSafeArea = false
2759
2862
  view.preservesSuperviewLayoutMargins = false
2760
- viewRespectsSystemMinimumLayoutMargins = false
2863
+ if #available(iOS 15.0, *) {
2864
+ viewRespectsSystemMinimumLayoutMargins = false
2865
+ }
2761
2866
  }
2762
2867
 
2763
2868
  override func viewDidLayoutSubviews() {
@@ -2812,7 +2917,7 @@ class ViewController: UIViewController {
2812
2917
  }
2813
2918
  }
2814
2919
  `);
2815
- writeFile2(path13.join(projectDir, "LynxProvider.swift"), `
2920
+ writeFile2(path14.join(projectDir, "LynxProvider.swift"), `
2816
2921
  import Foundation
2817
2922
 
2818
2923
  class LynxProvider: NSObject, LynxTemplateProvider {
@@ -2831,7 +2936,7 @@ class LynxProvider: NSObject, LynxTemplateProvider {
2831
2936
  }
2832
2937
  }
2833
2938
  `);
2834
- writeFile2(path13.join(projectDir, "LynxInitProcessor.swift"), `
2939
+ writeFile2(path14.join(projectDir, "LynxInitProcessor.swift"), `
2835
2940
  // Copyright 2024 The Lynx Authors. All rights reserved.
2836
2941
  // Licensed under the Apache License Version 2.0 that can be found in the
2837
2942
  // LICENSE file in the root directory of this source tree.
@@ -2871,7 +2976,7 @@ final class LynxInitProcessor {
2871
2976
  }
2872
2977
  `);
2873
2978
  }
2874
- writeFile2(path13.join(projectDir, bridgingHeader), `
2979
+ writeFile2(path14.join(projectDir, bridgingHeader), `
2875
2980
  #import <Lynx/LynxConfig.h>
2876
2981
  #import <Lynx/LynxEnv.h>
2877
2982
  #import <Lynx/LynxTemplateProvider.h>
@@ -2880,7 +2985,7 @@ final class LynxInitProcessor {
2880
2985
  #import <SDWebImage/SDWebImage.h>
2881
2986
  #import <SDWebImageWebPCoder/SDWebImageWebPCoder.h>
2882
2987
  `);
2883
- writeFile2(path13.join(projectDir, "Info.plist"), `
2988
+ writeFile2(path14.join(projectDir, "Info.plist"), `
2884
2989
  <?xml version="1.0" encoding="UTF-8"?>
2885
2990
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2886
2991
  <plist version="1.0">
@@ -2938,21 +3043,21 @@ final class LynxInitProcessor {
2938
3043
  </dict>
2939
3044
  </plist>
2940
3045
  `);
2941
- const appIconDir = path13.join(projectDir, "Assets.xcassets", "AppIcon.appiconset");
2942
- fs12.mkdirSync(appIconDir, { recursive: true });
3046
+ const appIconDir = path14.join(projectDir, "Assets.xcassets", "AppIcon.appiconset");
3047
+ fs13.mkdirSync(appIconDir, { recursive: true });
2943
3048
  const iconPaths = resolveIconPaths(process.cwd(), config);
2944
3049
  if (applyIosAppIconAssets(appIconDir, iconPaths)) {
2945
3050
  console.log(iconPaths?.ios ? "\u2705 Copied iOS icon from tamer.config.json icon.ios" : "\u2705 Copied app icon from tamer.config.json icon.source");
2946
3051
  } else {
2947
- writeFile2(path13.join(appIconDir, "Contents.json"), `
3052
+ writeFile2(path14.join(appIconDir, "Contents.json"), `
2948
3053
  {
2949
3054
  "images" : [ { "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ],
2950
3055
  "info" : { "author" : "xcode", "version" : 1 }
2951
3056
  }
2952
3057
  `);
2953
3058
  }
2954
- fs12.mkdirSync(xcodeprojDir, { recursive: true });
2955
- writeFile2(path13.join(xcodeprojDir, "project.pbxproj"), `
3059
+ fs13.mkdirSync(xcodeprojDir, { recursive: true });
3060
+ writeFile2(path14.join(xcodeprojDir, "project.pbxproj"), `
2956
3061
  // !$*UTF8*$!
2957
3062
  {
2958
3063
  archiveVersion = 1;
@@ -3238,8 +3343,8 @@ final class LynxInitProcessor {
3238
3343
  var create_default2 = create2;
3239
3344
 
3240
3345
  // src/ios/autolink.ts
3241
- import fs14 from "fs";
3242
- import path15 from "path";
3346
+ import fs15 from "fs";
3347
+ import path16 from "path";
3243
3348
  import { execSync as execSync6 } from "child_process";
3244
3349
 
3245
3350
  // src/common/hostNativeModulesManifest.ts
@@ -3250,8 +3355,8 @@ function buildHostNativeModulesManifestJson(moduleClassNames) {
3250
3355
  }
3251
3356
 
3252
3357
  // src/ios/syncHost.ts
3253
- import fs13 from "fs";
3254
- import path14 from "path";
3358
+ import fs14 from "fs";
3359
+ import path15 from "path";
3255
3360
  import crypto from "crypto";
3256
3361
  function deterministicUUID(seed) {
3257
3362
  return crypto.createHash("sha256").update(seed).digest("hex").substring(0, 24).toUpperCase();
@@ -3299,7 +3404,7 @@ function getLaunchScreenStoryboard() {
3299
3404
  `;
3300
3405
  }
3301
3406
  function addLaunchScreenToXcodeProject(pbxprojPath, appName) {
3302
- let content = fs13.readFileSync(pbxprojPath, "utf8");
3407
+ let content = fs14.readFileSync(pbxprojPath, "utf8");
3303
3408
  if (content.includes("LaunchScreen.storyboard")) return;
3304
3409
  const baseFileRefUUID = deterministicUUID(`launchScreenBase:${appName}`);
3305
3410
  const variantGroupUUID = deterministicUUID(`launchScreenGroup:${appName}`);
@@ -3336,11 +3441,11 @@ function addLaunchScreenToXcodeProject(pbxprojPath, appName) {
3336
3441
  );
3337
3442
  content = content.replace(groupPattern, `$1
3338
3443
  ${variantGroupUUID} /* LaunchScreen.storyboard */,`);
3339
- fs13.writeFileSync(pbxprojPath, content, "utf8");
3444
+ fs14.writeFileSync(pbxprojPath, content, "utf8");
3340
3445
  console.log("\u2705 Registered LaunchScreen.storyboard in Xcode project");
3341
3446
  }
3342
3447
  function addSwiftSourceToXcodeProject(pbxprojPath, appName, filename) {
3343
- let content = fs13.readFileSync(pbxprojPath, "utf8");
3448
+ let content = fs14.readFileSync(pbxprojPath, "utf8");
3344
3449
  const escaped = filename.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3345
3450
  if (new RegExp(`path = "?${escaped}"?;`).test(content)) return;
3346
3451
  const fileRefUUID = deterministicUUID(`fileRef:${appName}:${filename}`);
@@ -3365,11 +3470,11 @@ function addSwiftSourceToXcodeProject(pbxprojPath, appName, filename) {
3365
3470
  );
3366
3471
  content = content.replace(groupPattern, `$1
3367
3472
  ${fileRefUUID} /* ${filename} */,`);
3368
- fs13.writeFileSync(pbxprojPath, content, "utf8");
3473
+ fs14.writeFileSync(pbxprojPath, content, "utf8");
3369
3474
  console.log(`\u2705 Registered ${filename} in Xcode project sources`);
3370
3475
  }
3371
3476
  function addResourceToXcodeProject(pbxprojPath, appName, filename) {
3372
- let content = fs13.readFileSync(pbxprojPath, "utf8");
3477
+ let content = fs14.readFileSync(pbxprojPath, "utf8");
3373
3478
  const escaped = filename.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3374
3479
  if (new RegExp(`path = "?${escaped}"?;`).test(content)) return;
3375
3480
  const fileRefUUID = deterministicUUID(`fileRef:${appName}:${filename}`);
@@ -3394,12 +3499,12 @@ function addResourceToXcodeProject(pbxprojPath, appName, filename) {
3394
3499
  );
3395
3500
  content = content.replace(groupPattern, `$1
3396
3501
  ${fileRefUUID} /* ${filename} */,`);
3397
- fs13.writeFileSync(pbxprojPath, content, "utf8");
3502
+ fs14.writeFileSync(pbxprojPath, content, "utf8");
3398
3503
  console.log(`\u2705 Registered ${filename} in Xcode project resources`);
3399
3504
  }
3400
3505
  function writeFile(filePath, content) {
3401
- fs13.mkdirSync(path14.dirname(filePath), { recursive: true });
3402
- fs13.writeFileSync(filePath, content, "utf8");
3506
+ fs14.mkdirSync(path15.dirname(filePath), { recursive: true });
3507
+ fs14.writeFileSync(filePath, content, "utf8");
3403
3508
  }
3404
3509
  function getAppDelegateSwift() {
3405
3510
  return `import UIKit
@@ -3459,7 +3564,9 @@ class ViewController: UIViewController {
3459
3564
  additionalSafeAreaInsets = .zero
3460
3565
  view.insetsLayoutMarginsFromSafeArea = false
3461
3566
  view.preservesSuperviewLayoutMargins = false
3462
- viewRespectsSystemMinimumLayoutMargins = false
3567
+ if #available(iOS 15.0, *) {
3568
+ viewRespectsSystemMinimumLayoutMargins = false
3569
+ }
3463
3570
  }
3464
3571
 
3465
3572
  override func viewDidLayoutSubviews() {
@@ -3520,6 +3627,7 @@ function getDevViewControllerSwift() {
3520
3627
  import Lynx
3521
3628
  import tamerdevclient
3522
3629
  import tamerinsets
3630
+ import tamersystemui
3523
3631
 
3524
3632
  class ViewController: UIViewController {
3525
3633
  private var lynxView: LynxView?
@@ -3532,7 +3640,9 @@ class ViewController: UIViewController {
3532
3640
  additionalSafeAreaInsets = .zero
3533
3641
  view.insetsLayoutMarginsFromSafeArea = false
3534
3642
  view.preservesSuperviewLayoutMargins = false
3535
- viewRespectsSystemMinimumLayoutMargins = false
3643
+ if #available(iOS 15.0, *) {
3644
+ viewRespectsSystemMinimumLayoutMargins = false
3645
+ }
3536
3646
  setupLynxView()
3537
3647
  setupDevClientModule()
3538
3648
  }
@@ -3549,7 +3659,7 @@ class ViewController: UIViewController {
3549
3659
  TamerInsetsModule.reRequestInsets()
3550
3660
  }
3551
3661
 
3552
- override var preferredStatusBarStyle: UIStatusBarStyle { TamerPreferredStatusBar.style }
3662
+ override var preferredStatusBarStyle: UIStatusBarStyle { SystemUIModule.statusBarStyleForHost }
3553
3663
 
3554
3664
  private func setupLynxView() {
3555
3665
  let size = fullscreenBounds().size
@@ -3611,8 +3721,8 @@ class ViewController: UIViewController {
3611
3721
  `;
3612
3722
  }
3613
3723
  function patchInfoPlist(infoPlistPath) {
3614
- if (!fs13.existsSync(infoPlistPath)) return;
3615
- let content = fs13.readFileSync(infoPlistPath, "utf8");
3724
+ if (!fs14.existsSync(infoPlistPath)) return;
3725
+ let content = fs14.readFileSync(infoPlistPath, "utf8");
3616
3726
  content = content.replace(/\s*<key>UIMainStoryboardFile<\/key>\s*<string>[^<]*<\/string>/g, "");
3617
3727
  if (!content.includes("UILaunchStoryboardName")) {
3618
3728
  content = content.replace("</dict>\n</plist>", ` <key>UILaunchStoryboardName</key>
@@ -3644,7 +3754,7 @@ function patchInfoPlist(infoPlistPath) {
3644
3754
  </plist>`);
3645
3755
  console.log("\u2705 Added UIApplicationSceneManifest to Info.plist");
3646
3756
  }
3647
- fs13.writeFileSync(infoPlistPath, content, "utf8");
3757
+ fs14.writeFileSync(infoPlistPath, content, "utf8");
3648
3758
  }
3649
3759
  function getSimpleLynxProviderSwift() {
3650
3760
  return `import Foundation
@@ -3669,9 +3779,9 @@ class LynxProvider: NSObject, LynxTemplateProvider {
3669
3779
  }
3670
3780
  function readTemplateOrFallback(devClientPkg, templateName, fallback, vars = {}) {
3671
3781
  if (devClientPkg) {
3672
- const tplPath = path14.join(devClientPkg, "ios", "templates", templateName);
3673
- if (fs13.existsSync(tplPath)) {
3674
- let content = fs13.readFileSync(tplPath, "utf8");
3782
+ const tplPath = path15.join(devClientPkg, "ios", "templates", templateName);
3783
+ if (fs14.existsSync(tplPath)) {
3784
+ let content = fs14.readFileSync(tplPath, "utf8");
3675
3785
  for (const [k, v] of Object.entries(vars)) {
3676
3786
  content = content.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), v);
3677
3787
  }
@@ -3689,19 +3799,19 @@ function syncHostIos(opts) {
3689
3799
  if (!appName) {
3690
3800
  throw new Error('"ios.appName" must be defined in tamer.config.json');
3691
3801
  }
3692
- const projectDir = path14.join(resolved.iosDir, appName);
3693
- const infoPlistPath = path14.join(projectDir, "Info.plist");
3694
- if (!fs13.existsSync(projectDir)) {
3802
+ const projectDir = path15.join(resolved.iosDir, appName);
3803
+ const infoPlistPath = path15.join(projectDir, "Info.plist");
3804
+ if (!fs14.existsSync(projectDir)) {
3695
3805
  throw new Error(`iOS project not found at ${projectDir}. Run \`tamer ios create\` first.`);
3696
3806
  }
3697
- const pbxprojPath = path14.join(resolved.iosDir, `${appName}.xcodeproj`, "project.pbxproj");
3698
- const baseLprojDir = path14.join(projectDir, "Base.lproj");
3699
- const launchScreenPath = path14.join(baseLprojDir, "LaunchScreen.storyboard");
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");
3700
3810
  patchInfoPlist(infoPlistPath);
3701
- writeFile(path14.join(projectDir, "AppDelegate.swift"), getAppDelegateSwift());
3702
- writeFile(path14.join(projectDir, "SceneDelegate.swift"), getSceneDelegateSwift());
3703
- if (!fs13.existsSync(launchScreenPath)) {
3704
- fs13.mkdirSync(baseLprojDir, { recursive: true });
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 });
3705
3815
  writeFile(launchScreenPath, getLaunchScreenStoryboard());
3706
3816
  addLaunchScreenToXcodeProject(pbxprojPath, appName);
3707
3817
  }
@@ -3710,33 +3820,33 @@ function syncHostIos(opts) {
3710
3820
  const devClientPkg2 = findDevClientPackage(resolved.projectRoot);
3711
3821
  const segment = resolved.lynxProjectDir.split("/").filter(Boolean).pop() ?? "";
3712
3822
  const tplVars = { PROJECT_BUNDLE_SEGMENT: segment };
3713
- writeFile(path14.join(projectDir, "ViewController.swift"), getDevViewControllerSwift());
3714
- writeFile(path14.join(projectDir, "LynxProvider.swift"), getSimpleLynxProviderSwift());
3823
+ writeFile(path15.join(projectDir, "ViewController.swift"), getDevViewControllerSwift());
3824
+ writeFile(path15.join(projectDir, "LynxProvider.swift"), getSimpleLynxProviderSwift());
3715
3825
  addSwiftSourceToXcodeProject(pbxprojPath, appName, "LynxProvider.swift");
3716
3826
  const devTPContent = readTemplateOrFallback(devClientPkg2, "DevTemplateProvider.swift", "", tplVars);
3717
3827
  if (devTPContent) {
3718
- writeFile(path14.join(projectDir, "DevTemplateProvider.swift"), devTPContent);
3828
+ writeFile(path15.join(projectDir, "DevTemplateProvider.swift"), devTPContent);
3719
3829
  addSwiftSourceToXcodeProject(pbxprojPath, appName, "DevTemplateProvider.swift");
3720
3830
  }
3721
3831
  const projectVCContent = readTemplateOrFallback(devClientPkg2, "ProjectViewController.swift", "", tplVars);
3722
3832
  if (projectVCContent) {
3723
- writeFile(path14.join(projectDir, "ProjectViewController.swift"), projectVCContent);
3833
+ writeFile(path15.join(projectDir, "ProjectViewController.swift"), projectVCContent);
3724
3834
  addSwiftSourceToXcodeProject(pbxprojPath, appName, "ProjectViewController.swift");
3725
3835
  }
3726
3836
  const devCMContent = readTemplateOrFallback(devClientPkg2, "DevClientManager.swift", "", tplVars);
3727
3837
  if (devCMContent) {
3728
- writeFile(path14.join(projectDir, "DevClientManager.swift"), devCMContent);
3838
+ writeFile(path15.join(projectDir, "DevClientManager.swift"), devCMContent);
3729
3839
  addSwiftSourceToXcodeProject(pbxprojPath, appName, "DevClientManager.swift");
3730
3840
  }
3731
3841
  const qrContent = readTemplateOrFallback(devClientPkg2, "QRScannerViewController.swift", "", tplVars);
3732
3842
  if (qrContent) {
3733
- writeFile(path14.join(projectDir, "QRScannerViewController.swift"), qrContent);
3843
+ writeFile(path15.join(projectDir, "QRScannerViewController.swift"), qrContent);
3734
3844
  addSwiftSourceToXcodeProject(pbxprojPath, appName, "QRScannerViewController.swift");
3735
3845
  }
3736
3846
  console.log("\u2705 Synced iOS host app (embedded dev mode) \u2014 ViewController, DevTemplateProvider, ProjectViewController, DevClientManager, QRScannerViewController");
3737
3847
  } else {
3738
- writeFile(path14.join(projectDir, "ViewController.swift"), getViewControllerSwift());
3739
- writeFile(path14.join(projectDir, "LynxProvider.swift"), getSimpleLynxProviderSwift());
3848
+ writeFile(path15.join(projectDir, "ViewController.swift"), getViewControllerSwift());
3849
+ writeFile(path15.join(projectDir, "LynxProvider.swift"), getSimpleLynxProviderSwift());
3740
3850
  addSwiftSourceToXcodeProject(pbxprojPath, appName, "LynxProvider.swift");
3741
3851
  console.log("\u2705 Synced iOS host app controller files");
3742
3852
  }
@@ -3755,11 +3865,11 @@ var autolink2 = () => {
3755
3865
  const projectRoot = resolved.projectRoot;
3756
3866
  const iosProjectPath = resolved.iosDir;
3757
3867
  function updateGeneratedSection(filePath, newContent, startMarker, endMarker) {
3758
- if (!fs14.existsSync(filePath)) {
3868
+ if (!fs15.existsSync(filePath)) {
3759
3869
  console.warn(`\u26A0\uFE0F File not found, skipping update: ${filePath}`);
3760
3870
  return;
3761
3871
  }
3762
- let fileContent = fs14.readFileSync(filePath, "utf8");
3872
+ let fileContent = fs15.readFileSync(filePath, "utf8");
3763
3873
  const escapedStartMarker = startMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3764
3874
  const escapedEndMarker = endMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3765
3875
  const regex = new RegExp(`${escapedStartMarker}[\\s\\S]*?${escapedEndMarker}`, "g");
@@ -3779,33 +3889,33 @@ ${replacementBlock}
3779
3889
  `;
3780
3890
  }
3781
3891
  } else {
3782
- console.warn(`\u26A0\uFE0F Could not find autolink markers in ${path15.basename(filePath)}. Appending to the end of the file.`);
3892
+ console.warn(`\u26A0\uFE0F Could not find autolink markers in ${path16.basename(filePath)}. Appending to the end of the file.`);
3783
3893
  fileContent += `
3784
3894
  ${replacementBlock}
3785
3895
  `;
3786
3896
  }
3787
- fs14.writeFileSync(filePath, fileContent, "utf8");
3788
- console.log(`\u2705 Updated autolinked section in ${path15.basename(filePath)}`);
3897
+ fs15.writeFileSync(filePath, fileContent, "utf8");
3898
+ console.log(`\u2705 Updated autolinked section in ${path16.basename(filePath)}`);
3789
3899
  }
3790
3900
  function resolvePodDirectory(pkg) {
3791
- const configuredDir = path15.join(pkg.packagePath, pkg.config.ios?.podspecPath || ".");
3792
- if (fs14.existsSync(configuredDir)) {
3901
+ const configuredDir = path16.join(pkg.packagePath, pkg.config.ios?.podspecPath || ".");
3902
+ if (fs15.existsSync(configuredDir)) {
3793
3903
  return configuredDir;
3794
3904
  }
3795
- const iosDir = path15.join(pkg.packagePath, "ios");
3796
- if (fs14.existsSync(iosDir)) {
3905
+ const iosDir = path16.join(pkg.packagePath, "ios");
3906
+ if (fs15.existsSync(iosDir)) {
3797
3907
  const stack = [iosDir];
3798
3908
  while (stack.length > 0) {
3799
3909
  const current = stack.pop();
3800
3910
  try {
3801
- const entries = fs14.readdirSync(current, { withFileTypes: true });
3911
+ const entries = fs15.readdirSync(current, { withFileTypes: true });
3802
3912
  const podspec = entries.find((entry) => entry.isFile() && entry.name.endsWith(".podspec"));
3803
3913
  if (podspec) {
3804
3914
  return current;
3805
3915
  }
3806
3916
  for (const entry of entries) {
3807
3917
  if (entry.isDirectory()) {
3808
- stack.push(path15.join(current, entry.name));
3918
+ stack.push(path16.join(current, entry.name));
3809
3919
  }
3810
3920
  }
3811
3921
  } catch {
@@ -3816,9 +3926,9 @@ ${replacementBlock}
3816
3926
  }
3817
3927
  function resolvePodName(pkg) {
3818
3928
  const fullPodspecDir = resolvePodDirectory(pkg);
3819
- if (fs14.existsSync(fullPodspecDir)) {
3929
+ if (fs15.existsSync(fullPodspecDir)) {
3820
3930
  try {
3821
- const files = fs14.readdirSync(fullPodspecDir);
3931
+ const files = fs15.readdirSync(fullPodspecDir);
3822
3932
  const podspecFile = files.find((f) => f.endsWith(".podspec"));
3823
3933
  if (podspecFile) return podspecFile.replace(".podspec", "");
3824
3934
  } catch {
@@ -3827,13 +3937,13 @@ ${replacementBlock}
3827
3937
  return pkg.name.split("/").pop().replace(/-/g, "");
3828
3938
  }
3829
3939
  function updatePodfile(packages) {
3830
- const podfilePath = path15.join(iosProjectPath, "Podfile");
3940
+ const podfilePath = path16.join(iosProjectPath, "Podfile");
3831
3941
  let scriptContent = ` # This section is automatically generated by Tamer4Lynx.
3832
3942
  # Manual edits will be overwritten.`;
3833
3943
  const iosPackages = packages.filter((p) => p.config.ios);
3834
3944
  if (iosPackages.length > 0) {
3835
3945
  iosPackages.forEach((pkg) => {
3836
- const relativePath = path15.relative(iosProjectPath, resolvePodDirectory(pkg));
3946
+ const relativePath = path16.relative(iosProjectPath, resolvePodDirectory(pkg));
3837
3947
  const podName = resolvePodName(pkg);
3838
3948
  scriptContent += `
3839
3949
  pod '${podName}', :path => '${relativePath}'`;
@@ -3845,9 +3955,9 @@ ${replacementBlock}
3845
3955
  updateGeneratedSection(podfilePath, scriptContent.trim(), "# GENERATED AUTOLINK DEPENDENCIES START", "# GENERATED AUTOLINK DEPENDENCIES END");
3846
3956
  }
3847
3957
  function ensureXElementPod() {
3848
- const podfilePath = path15.join(iosProjectPath, "Podfile");
3849
- if (!fs14.existsSync(podfilePath)) return;
3850
- let content = fs14.readFileSync(podfilePath, "utf8");
3958
+ const podfilePath = path16.join(iosProjectPath, "Podfile");
3959
+ if (!fs15.existsSync(podfilePath)) return;
3960
+ let content = fs15.readFileSync(podfilePath, "utf8");
3851
3961
  if (content.includes("pod 'XElement'")) return;
3852
3962
  const lynxVersionMatch = content.match(/pod\s+'Lynx',\s*'([^']+)'/);
3853
3963
  const lynxVersion = lynxVersionMatch?.[1] ?? "3.6.0";
@@ -3872,13 +3982,13 @@ ${replacementBlock}
3872
3982
  `;
3873
3983
  }
3874
3984
  }
3875
- fs14.writeFileSync(podfilePath, content, "utf8");
3985
+ fs15.writeFileSync(podfilePath, content, "utf8");
3876
3986
  console.log(`\u2705 Added XElement pod (v${lynxVersion}) to Podfile`);
3877
3987
  }
3878
3988
  function ensureLynxPatchInPodfile() {
3879
- const podfilePath = path15.join(iosProjectPath, "Podfile");
3880
- if (!fs14.existsSync(podfilePath)) return;
3881
- let content = fs14.readFileSync(podfilePath, "utf8");
3989
+ const podfilePath = path16.join(iosProjectPath, "Podfile");
3990
+ if (!fs15.existsSync(podfilePath)) return;
3991
+ let content = fs15.readFileSync(podfilePath, "utf8");
3882
3992
  if (content.includes("content.gsub(/\\btypeof\\(/, '__typeof__(')")) return;
3883
3993
  const patch = `
3884
3994
  Dir.glob(File.join(installer.sandbox.root, 'Lynx/platform/darwin/**/*.{m,mm}')).each do |lynx_source|
@@ -3890,13 +4000,13 @@ ${replacementBlock}
3890
4000
  end`;
3891
4001
  content = content.replace(/(\n end\s*\n)(end\s*)$/, `$1${patch}
3892
4002
  $2`);
3893
- fs14.writeFileSync(podfilePath, content, "utf8");
4003
+ fs15.writeFileSync(podfilePath, content, "utf8");
3894
4004
  console.log("\u2705 Added Lynx typeof patch to Podfile post_install.");
3895
4005
  }
3896
4006
  function ensurePodBuildSettings() {
3897
- const podfilePath = path15.join(iosProjectPath, "Podfile");
3898
- if (!fs14.existsSync(podfilePath)) return;
3899
- let content = fs14.readFileSync(podfilePath, "utf8");
4007
+ const podfilePath = path16.join(iosProjectPath, "Podfile");
4008
+ if (!fs15.existsSync(podfilePath)) return;
4009
+ let content = fs15.readFileSync(podfilePath, "utf8");
3900
4010
  let changed = false;
3901
4011
  if (!content.includes("CLANG_ENABLE_EXPLICIT_MODULES")) {
3902
4012
  content = content.replace(
@@ -3939,7 +4049,7 @@ $2`);
3939
4049
  changed = true;
3940
4050
  }
3941
4051
  if (changed) {
3942
- fs14.writeFileSync(podfilePath, content, "utf8");
4052
+ fs15.writeFileSync(podfilePath, content, "utf8");
3943
4053
  console.log("\u2705 Added Xcode compatibility build settings to Podfile post_install.");
3944
4054
  }
3945
4055
  }
@@ -3947,10 +4057,10 @@ $2`);
3947
4057
  const appNameFromConfig = resolved.config.ios?.appName;
3948
4058
  const candidatePaths = [];
3949
4059
  if (appNameFromConfig) {
3950
- candidatePaths.push(path15.join(iosProjectPath, appNameFromConfig, "LynxInitProcessor.swift"));
4060
+ candidatePaths.push(path16.join(iosProjectPath, appNameFromConfig, "LynxInitProcessor.swift"));
3951
4061
  }
3952
- candidatePaths.push(path15.join(iosProjectPath, "LynxInitProcessor.swift"));
3953
- const found = candidatePaths.find((p) => fs14.existsSync(p));
4062
+ candidatePaths.push(path16.join(iosProjectPath, "LynxInitProcessor.swift"));
4063
+ const found = candidatePaths.find((p) => fs15.existsSync(p));
3954
4064
  const lynxInitPath = found ?? candidatePaths[0];
3955
4065
  const iosPackages = packages.filter((p) => getIosModuleClassNames(p.config.ios).length > 0 || Object.keys(getIosElements(p.config.ios)).length > 0);
3956
4066
  const seenModules = /* @__PURE__ */ new Set();
@@ -3989,7 +4099,7 @@ $2`);
3989
4099
  const podName = resolvePodName(pkg);
3990
4100
  return `import ${podName}`;
3991
4101
  }).join("\n");
3992
- const fileContent = fs14.readFileSync(filePath, "utf8");
4102
+ const fileContent = fs15.readFileSync(filePath, "utf8");
3993
4103
  if (fileContent.indexOf(startMarker) !== -1) {
3994
4104
  updateGeneratedSection(filePath, imports, startMarker, endMarker);
3995
4105
  return;
@@ -4026,8 +4136,8 @@ ${after}`;
4026
4136
  ${fileContent}`;
4027
4137
  }
4028
4138
  }
4029
- fs14.writeFileSync(filePath, newContent, "utf8");
4030
- console.log(`\u2705 Updated imports in ${path15.basename(filePath)}`);
4139
+ fs15.writeFileSync(filePath, newContent, "utf8");
4140
+ console.log(`\u2705 Updated imports in ${path16.basename(filePath)}`);
4031
4141
  }
4032
4142
  updateImportsSection(lynxInitPath, importPackages);
4033
4143
  if (importPackages.length === 0) {
@@ -4071,7 +4181,7 @@ ${androidNames.map((n) => ` "${n.replace(/\\/g, "\\\\").replace(/"/g,
4071
4181
  } else {
4072
4182
  devClientSupportedBody = " // @tamer4lynx/tamer-dev-client not linked";
4073
4183
  }
4074
- if (fs14.readFileSync(lynxInitPath, "utf8").includes("GENERATED DEV_CLIENT_SUPPORTED START")) {
4184
+ if (fs15.readFileSync(lynxInitPath, "utf8").includes("GENERATED DEV_CLIENT_SUPPORTED START")) {
4075
4185
  updateGeneratedSection(lynxInitPath, devClientSupportedBody, "// GENERATED DEV_CLIENT_SUPPORTED START", "// GENERATED DEV_CLIENT_SUPPORTED END");
4076
4186
  }
4077
4187
  }
@@ -4079,13 +4189,13 @@ ${androidNames.map((n) => ` "${n.replace(/\\/g, "\\\\").replace(/"/g,
4079
4189
  const appNameFromConfig = resolved.config.ios?.appName;
4080
4190
  const candidates = [];
4081
4191
  if (appNameFromConfig) {
4082
- candidates.push(path15.join(iosProjectPath, appNameFromConfig, "Info.plist"));
4192
+ candidates.push(path16.join(iosProjectPath, appNameFromConfig, "Info.plist"));
4083
4193
  }
4084
- candidates.push(path15.join(iosProjectPath, "Info.plist"));
4085
- return candidates.find((p) => fs14.existsSync(p)) ?? null;
4194
+ candidates.push(path16.join(iosProjectPath, "Info.plist"));
4195
+ return candidates.find((p) => fs15.existsSync(p)) ?? null;
4086
4196
  }
4087
4197
  function readPlistXml(plistPath) {
4088
- return fs14.readFileSync(plistPath, "utf8");
4198
+ return fs15.readFileSync(plistPath, "utf8");
4089
4199
  }
4090
4200
  function syncInfoPlistPermissions(packages) {
4091
4201
  const plistPath = findInfoPlist();
@@ -4116,7 +4226,7 @@ ${androidNames.map((n) => ` "${n.replace(/\\/g, "\\\\").replace(/"/g,
4116
4226
  added++;
4117
4227
  }
4118
4228
  if (added > 0) {
4119
- fs14.writeFileSync(plistPath, plist, "utf8");
4229
+ fs15.writeFileSync(plistPath, plist, "utf8");
4120
4230
  console.log(`\u2705 Synced ${added} Info.plist permission description(s)`);
4121
4231
  }
4122
4232
  }
@@ -4163,16 +4273,16 @@ ${schemesXml}
4163
4273
  $1`
4164
4274
  );
4165
4275
  }
4166
- fs14.writeFileSync(plistPath, plist, "utf8");
4276
+ fs15.writeFileSync(plistPath, plist, "utf8");
4167
4277
  console.log(`\u2705 Synced ${urlSchemes.length} iOS URL scheme(s) into Info.plist`);
4168
4278
  }
4169
4279
  function runPodInstall(forcePath) {
4170
- const podfilePath = forcePath ?? path15.join(iosProjectPath, "Podfile");
4171
- if (!fs14.existsSync(podfilePath)) {
4280
+ const podfilePath = forcePath ?? path16.join(iosProjectPath, "Podfile");
4281
+ if (!fs15.existsSync(podfilePath)) {
4172
4282
  console.log("\u2139\uFE0F No Podfile found in ios directory; skipping `pod install`.");
4173
4283
  return;
4174
4284
  }
4175
- const cwd = path15.dirname(podfilePath);
4285
+ const cwd = path16.dirname(podfilePath);
4176
4286
  try {
4177
4287
  console.log(`\u2139\uFE0F Running \`pod install\` in ${cwd}...`);
4178
4288
  try {
@@ -4206,8 +4316,8 @@ $1`
4206
4316
  syncInfoPlistUrlSchemes();
4207
4317
  const appNameFromConfig = resolved.config.ios?.appName;
4208
4318
  if (appNameFromConfig) {
4209
- const appPodfile = path15.join(iosProjectPath, appNameFromConfig, "Podfile");
4210
- if (fs14.existsSync(appPodfile)) {
4319
+ const appPodfile = path16.join(iosProjectPath, appNameFromConfig, "Podfile");
4320
+ if (fs15.existsSync(appPodfile)) {
4211
4321
  runPodInstall(appPodfile);
4212
4322
  console.log("\u2728 Autolinking complete for iOS.");
4213
4323
  return;
@@ -4222,13 +4332,13 @@ $1`
4222
4332
  const appFolder = resolved.config.ios?.appName;
4223
4333
  if (!hasDevClient || !appFolder) return;
4224
4334
  const androidNames = getDedupedAndroidModuleClassNames(allPkgs);
4225
- const appDir = path15.join(iosProjectPath, appFolder);
4226
- fs14.mkdirSync(appDir, { recursive: true });
4227
- const manifestPath = path15.join(appDir, TAMER_HOST_NATIVE_MODULES_FILENAME);
4228
- fs14.writeFileSync(manifestPath, buildHostNativeModulesManifestJson(androidNames), "utf8");
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");
4229
4339
  console.log(`\u2705 Wrote ${TAMER_HOST_NATIVE_MODULES_FILENAME} (native module ids for dev-client checks)`);
4230
- const pbxprojPath = path15.join(iosProjectPath, `${appFolder}.xcodeproj`, "project.pbxproj");
4231
- if (fs14.existsSync(pbxprojPath)) {
4340
+ const pbxprojPath = path16.join(iosProjectPath, `${appFolder}.xcodeproj`, "project.pbxproj");
4341
+ if (fs15.existsSync(pbxprojPath)) {
4232
4342
  addResourceToXcodeProject(pbxprojPath, appFolder, TAMER_HOST_NATIVE_MODULES_FILENAME);
4233
4343
  }
4234
4344
  }
@@ -4237,11 +4347,11 @@ $1`
4237
4347
  var autolink_default2 = autolink2;
4238
4348
 
4239
4349
  // src/ios/bundle.ts
4240
- import fs15 from "fs";
4241
- import path16 from "path";
4350
+ import fs16 from "fs";
4351
+ import path17 from "path";
4242
4352
  import { execSync as execSync7 } from "child_process";
4243
4353
  function bundleAndDeploy2(opts = {}) {
4244
- const release = opts.release === true;
4354
+ const release = opts.release === true || opts.production === true;
4245
4355
  let resolved;
4246
4356
  try {
4247
4357
  resolved = resolveHostPaths();
@@ -4256,18 +4366,22 @@ function bundleAndDeploy2(opts = {}) {
4256
4366
  const includeDevClient = !release && !!devClientPkg;
4257
4367
  const appName = resolved.config.ios.appName;
4258
4368
  const sourceBundlePath = resolved.lynxBundlePath;
4259
- const destinationDir = path16.join(resolved.iosDir, appName);
4260
- const destinationBundlePath = path16.join(destinationDir, resolved.lynxBundleFile);
4369
+ const destinationDir = path17.join(resolved.iosDir, appName);
4370
+ const destinationBundlePath = path17.join(destinationDir, resolved.lynxBundleFile);
4261
4371
  syncHost_default({ release, includeDevClient });
4262
4372
  autolink_default2();
4263
4373
  const iconPaths = resolveIconPaths(resolved.projectRoot, resolved.config);
4264
4374
  if (iconPaths) {
4265
- const appIconDir = path16.join(destinationDir, "Assets.xcassets", "AppIcon.appiconset");
4375
+ const appIconDir = path17.join(destinationDir, "Assets.xcassets", "AppIcon.appiconset");
4266
4376
  if (applyIosAppIconAssets(appIconDir, iconPaths)) {
4267
4377
  console.log("\u2705 Synced iOS AppIcon from tamer.config.json");
4268
4378
  }
4269
4379
  }
4270
4380
  try {
4381
+ const lynxTsconfig = path17.join(resolved.lynxProjectDir, "tsconfig.json");
4382
+ if (fs16.existsSync(lynxTsconfig)) {
4383
+ fixTsconfigReferencesForBuild(lynxTsconfig);
4384
+ }
4271
4385
  console.log("\u{1F4E6} Building Lynx bundle...");
4272
4386
  execSync7("npm run build", { stdio: "inherit", cwd: resolved.lynxProjectDir });
4273
4387
  console.log("\u2705 Build completed successfully.");
@@ -4276,40 +4390,40 @@ function bundleAndDeploy2(opts = {}) {
4276
4390
  process.exit(1);
4277
4391
  }
4278
4392
  try {
4279
- if (!fs15.existsSync(sourceBundlePath)) {
4393
+ if (!fs16.existsSync(sourceBundlePath)) {
4280
4394
  console.error(`\u274C Build output not found at: ${sourceBundlePath}`);
4281
4395
  process.exit(1);
4282
4396
  }
4283
- if (!fs15.existsSync(destinationDir)) {
4397
+ if (!fs16.existsSync(destinationDir)) {
4284
4398
  console.error(`Destination directory not found at: ${destinationDir}`);
4285
4399
  process.exit(1);
4286
4400
  }
4287
- const distDir = path16.dirname(sourceBundlePath);
4401
+ const distDir = path17.dirname(sourceBundlePath);
4288
4402
  console.log(`\u{1F69A} Copying bundle and assets to iOS project...`);
4289
4403
  copyDistAssets(distDir, destinationDir, resolved.lynxBundleFile);
4290
4404
  console.log(`\u2728 Successfully copied bundle to: ${destinationBundlePath}`);
4291
- const pbxprojPath = path16.join(resolved.iosDir, `${appName}.xcodeproj`, "project.pbxproj");
4292
- if (fs15.existsSync(pbxprojPath)) {
4405
+ const pbxprojPath = path17.join(resolved.iosDir, `${appName}.xcodeproj`, "project.pbxproj");
4406
+ if (fs16.existsSync(pbxprojPath)) {
4293
4407
  const skip = /* @__PURE__ */ new Set([".rspeedy", "stats.json"]);
4294
- for (const entry of fs15.readdirSync(distDir)) {
4295
- if (skip.has(entry) || fs15.statSync(path16.join(distDir, entry)).isDirectory()) continue;
4408
+ for (const entry of fs16.readdirSync(distDir)) {
4409
+ if (skip.has(entry) || fs16.statSync(path17.join(distDir, entry)).isDirectory()) continue;
4296
4410
  addResourceToXcodeProject(pbxprojPath, appName, entry);
4297
4411
  }
4298
4412
  }
4299
4413
  if (includeDevClient && devClientPkg) {
4300
- const devClientBundle = path16.join(destinationDir, "dev-client.lynx.bundle");
4414
+ const devClientBundle = path17.join(destinationDir, "dev-client.lynx.bundle");
4301
4415
  console.log("\u{1F4E6} Building dev-client bundle...");
4302
4416
  try {
4303
4417
  execSync7("npm run build", { stdio: "inherit", cwd: devClientPkg });
4304
4418
  } catch {
4305
4419
  console.warn("\u26A0\uFE0F dev-client build failed; skipping dev-client bundle");
4306
4420
  }
4307
- const builtBundle = path16.join(devClientPkg, "dist", "dev-client.lynx.bundle");
4308
- if (fs15.existsSync(builtBundle)) {
4309
- fs15.copyFileSync(builtBundle, devClientBundle);
4421
+ const builtBundle = path17.join(devClientPkg, "dist", "dev-client.lynx.bundle");
4422
+ if (fs16.existsSync(builtBundle)) {
4423
+ fs16.copyFileSync(builtBundle, devClientBundle);
4310
4424
  console.log("\u2728 Copied dev-client.lynx.bundle to iOS project");
4311
- const pbxprojPath2 = path16.join(resolved.iosDir, `${appName}.xcodeproj`, "project.pbxproj");
4312
- if (fs15.existsSync(pbxprojPath2)) {
4425
+ const pbxprojPath2 = path17.join(resolved.iosDir, `${appName}.xcodeproj`, "project.pbxproj");
4426
+ if (fs16.existsSync(pbxprojPath2)) {
4313
4427
  addResourceToXcodeProject(pbxprojPath2, appName, "dev-client.lynx.bundle");
4314
4428
  }
4315
4429
  }
@@ -4323,8 +4437,8 @@ function bundleAndDeploy2(opts = {}) {
4323
4437
  var bundle_default2 = bundleAndDeploy2;
4324
4438
 
4325
4439
  // src/ios/build.ts
4326
- import fs16 from "fs";
4327
- import path17 from "path";
4440
+ import fs17 from "fs";
4441
+ import path18 from "path";
4328
4442
  import os3 from "os";
4329
4443
  import { execSync as execSync8 } from "child_process";
4330
4444
  function hostArch() {
@@ -4351,14 +4465,15 @@ async function buildIpa(opts = {}) {
4351
4465
  const appName = resolved.config.ios.appName;
4352
4466
  const bundleId = resolved.config.ios.bundleId;
4353
4467
  const iosDir = resolved.iosDir;
4354
- const configuration = opts.release ? "Release" : "Debug";
4355
- bundle_default2({ release: opts.release });
4468
+ const release = opts.release === true || opts.production === true;
4469
+ const configuration = release ? "Release" : "Debug";
4470
+ bundle_default2({ release, production: opts.production });
4356
4471
  const scheme = appName;
4357
- const workspacePath = path17.join(iosDir, `${appName}.xcworkspace`);
4358
- const projectPath = path17.join(iosDir, `${appName}.xcodeproj`);
4359
- const xcproject = fs16.existsSync(workspacePath) ? workspacePath : projectPath;
4472
+ const workspacePath = path18.join(iosDir, `${appName}.xcworkspace`);
4473
+ const projectPath = path18.join(iosDir, `${appName}.xcodeproj`);
4474
+ const xcproject = fs17.existsSync(workspacePath) ? workspacePath : projectPath;
4360
4475
  const flag = xcproject.endsWith(".xcworkspace") ? "-workspace" : "-project";
4361
- const derivedDataPath = path17.join(iosDir, "build");
4476
+ const derivedDataPath = path18.join(iosDir, "build");
4362
4477
  const sdk = opts.install ? "iphonesimulator" : "iphoneos";
4363
4478
  const signingArgs = opts.install ? "" : " CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO";
4364
4479
  const archFlag = opts.install ? `-arch ${hostArch()} ` : "";
@@ -4374,14 +4489,14 @@ async function buildIpa(opts = {}) {
4374
4489
  );
4375
4490
  console.log(`\u2705 Build completed.`);
4376
4491
  if (opts.install) {
4377
- const appGlob = path17.join(
4492
+ const appGlob = path18.join(
4378
4493
  derivedDataPath,
4379
4494
  "Build",
4380
4495
  "Products",
4381
4496
  `${configuration}-iphonesimulator`,
4382
4497
  `${appName}.app`
4383
4498
  );
4384
- if (!fs16.existsSync(appGlob)) {
4499
+ if (!fs17.existsSync(appGlob)) {
4385
4500
  console.error(`\u274C Built app not found at: ${appGlob}`);
4386
4501
  process.exit(1);
4387
4502
  }
@@ -4403,101 +4518,502 @@ async function buildIpa(opts = {}) {
4403
4518
  }
4404
4519
  var build_default2 = buildIpa;
4405
4520
 
4406
- // src/common/init.ts
4407
- import fs17 from "fs";
4408
- import path18 from "path";
4409
- import readline from "readline";
4410
- var rl = readline.createInterface({
4411
- input: process.stdin,
4412
- output: process.stdout,
4413
- terminal: false
4414
- });
4415
- function ask(question) {
4416
- return new Promise((resolve) => {
4417
- rl.question(question, (answer) => resolve(answer.trim()));
4418
- });
4419
- }
4420
- async function init() {
4421
- console.log("Tamer4Lynx Init: Let's set up your tamer.config.json\n");
4422
- const androidAppName = await ask("Android app name: ");
4423
- const androidPackageName = await ask("Android package name (e.g. com.example.app): ");
4424
- let androidSdk = await ask("Android SDK path (e.g. ~/Library/Android/sdk or $ANDROID_HOME): ");
4521
+ // src/common/init.tsx
4522
+ import fs18 from "fs";
4523
+ import path19 from "path";
4524
+ import { useState as useState4, useEffect as useEffect2, useCallback as useCallback3 } from "react";
4525
+ import { render, Text as Text9, Box as Box8 } from "ink";
4526
+
4527
+ // src/common/tui/components/TextInput.tsx
4528
+ import { useState, useEffect } from "react";
4529
+ import { Box, Text } from "ink";
4530
+ import InkTextInput from "ink-text-input";
4531
+ import { jsx, jsxs } from "react/jsx-runtime";
4532
+ function TuiTextInput({
4533
+ label,
4534
+ value: valueProp,
4535
+ defaultValue = "",
4536
+ onChange: onChangeProp,
4537
+ onSubmitValue,
4538
+ onSubmit,
4539
+ hint,
4540
+ error
4541
+ }) {
4542
+ const controlled = valueProp !== void 0;
4543
+ const [internal, setInternal] = useState(defaultValue);
4544
+ useEffect(() => {
4545
+ if (!controlled) setInternal(defaultValue);
4546
+ }, [defaultValue, controlled]);
4547
+ const value = controlled ? valueProp : internal;
4548
+ const onChange = (v) => {
4549
+ if (!controlled) setInternal(v);
4550
+ onChangeProp?.(v);
4551
+ };
4552
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
4553
+ label ? /* @__PURE__ */ jsx(Text, { children: label }) : null,
4554
+ /* @__PURE__ */ jsx(
4555
+ InkTextInput,
4556
+ {
4557
+ value,
4558
+ onChange,
4559
+ onSubmit: () => {
4560
+ const r = onSubmitValue?.(value);
4561
+ if (r === false) return;
4562
+ onSubmit();
4563
+ }
4564
+ }
4565
+ ),
4566
+ error ? /* @__PURE__ */ jsx(Text, { color: "red", children: error }) : hint ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: hint }) : null
4567
+ ] });
4568
+ }
4569
+
4570
+ // src/common/tui/components/SelectInput.tsx
4571
+ import "react";
4572
+ import { Box as Box2, Text as Text2 } from "ink";
4573
+ import InkSelectInput from "ink-select-input";
4574
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
4575
+ function TuiSelectInput({
4576
+ label,
4577
+ items,
4578
+ onSelect,
4579
+ hint
4580
+ }) {
4581
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
4582
+ label ? /* @__PURE__ */ jsx2(Text2, { children: label }) : null,
4583
+ /* @__PURE__ */ jsx2(
4584
+ InkSelectInput,
4585
+ {
4586
+ items,
4587
+ onSelect: (item) => onSelect(item.value)
4588
+ }
4589
+ ),
4590
+ hint ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: hint }) : null
4591
+ ] });
4592
+ }
4593
+
4594
+ // src/common/tui/components/PasswordInput.tsx
4595
+ import "react";
4596
+ import { Box as Box3, Text as Text3 } from "ink";
4597
+ import InkTextInput2 from "ink-text-input";
4598
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
4599
+
4600
+ // src/common/tui/components/ConfirmInput.tsx
4601
+ import "react";
4602
+ import { Box as Box4, Text as Text4 } from "ink";
4603
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
4604
+ function TuiConfirmInput({
4605
+ label,
4606
+ onConfirm,
4607
+ defaultYes = false,
4608
+ hint
4609
+ }) {
4610
+ const items = defaultYes ? [
4611
+ { label: "Yes (default)", value: "yes" },
4612
+ { label: "No", value: "no" }
4613
+ ] : [
4614
+ { label: "No (default)", value: "no" },
4615
+ { label: "Yes", value: "yes" }
4616
+ ];
4617
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
4618
+ label ? /* @__PURE__ */ jsx4(Text4, { children: label }) : null,
4619
+ /* @__PURE__ */ jsx4(
4620
+ TuiSelectInput,
4621
+ {
4622
+ items,
4623
+ onSelect: (v) => onConfirm(v === "yes"),
4624
+ hint
4625
+ }
4626
+ )
4627
+ ] });
4628
+ }
4629
+
4630
+ // src/common/tui/components/Spinner.tsx
4631
+ import "react";
4632
+ import { Text as Text5 } from "ink";
4633
+ import InkSpinner from "ink-spinner";
4634
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
4635
+ function TuiSpinner({ label, type = "dots" }) {
4636
+ return /* @__PURE__ */ jsxs5(Text5, { color: "cyan", children: [
4637
+ /* @__PURE__ */ jsx5(InkSpinner, { type }),
4638
+ label ? ` ${label}` : ""
4639
+ ] });
4640
+ }
4641
+
4642
+ // src/common/tui/components/StatusBox.tsx
4643
+ import "react";
4644
+ import { Box as Box5, Text as Text6 } from "ink";
4645
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
4646
+ var colors = {
4647
+ success: "green",
4648
+ error: "red",
4649
+ warning: "yellow",
4650
+ info: "cyan"
4651
+ };
4652
+ function StatusBox({ variant, children, title }) {
4653
+ const c = colors[variant];
4654
+ return /* @__PURE__ */ jsxs6(Box5, { flexDirection: "column", borderStyle: "round", borderColor: c, paddingX: 1, children: [
4655
+ title ? /* @__PURE__ */ jsx6(Text6, { bold: true, color: c, children: title }) : null,
4656
+ /* @__PURE__ */ jsx6(Box5, { flexDirection: "column", children })
4657
+ ] });
4658
+ }
4659
+
4660
+ // src/common/tui/components/Wizard.tsx
4661
+ import "react";
4662
+ import { Box as Box6, Text as Text7 } from "ink";
4663
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
4664
+ function Wizard({ step, total, title, children }) {
4665
+ return /* @__PURE__ */ jsxs7(Box6, { flexDirection: "column", children: [
4666
+ /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
4667
+ "Step ",
4668
+ step,
4669
+ "/",
4670
+ total,
4671
+ title ? ` \u2014 ${title}` : ""
4672
+ ] }),
4673
+ /* @__PURE__ */ jsx7(Box6, { marginTop: 1, flexDirection: "column", children })
4674
+ ] });
4675
+ }
4676
+
4677
+ // src/common/tui/components/ServerDashboard.tsx
4678
+ import "react";
4679
+ import { Box as Box7, Text as Text8 } from "ink";
4680
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
4681
+ function ServerDashboard({
4682
+ projectName,
4683
+ port,
4684
+ lanIp,
4685
+ devUrl,
4686
+ wsUrl,
4687
+ lynxBundleFile,
4688
+ bonjour,
4689
+ verbose,
4690
+ buildPhase,
4691
+ buildError,
4692
+ wsConnections,
4693
+ logLines,
4694
+ showLogs,
4695
+ qrLines,
4696
+ phase,
4697
+ startError
4698
+ }) {
4699
+ if (phase === "failed") {
4700
+ return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", children: [
4701
+ /* @__PURE__ */ jsx8(Text8, { color: "red", bold: true, children: "Dev server failed to start" }),
4702
+ startError ? /* @__PURE__ */ jsx8(Text8, { color: "red", children: startError }) : null,
4703
+ /* @__PURE__ */ jsx8(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Press Ctrl+C or 'q' to quit" }) })
4704
+ ] });
4705
+ }
4706
+ const bundlePath = `${devUrl}/${lynxBundleFile}`;
4707
+ return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", children: [
4708
+ /* @__PURE__ */ jsxs8(Text8, { bold: true, color: "green", children: [
4709
+ "Tamer4Lynx dev server (",
4710
+ projectName,
4711
+ ")"
4712
+ ] }),
4713
+ verbose ? /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Logs: verbose (native + JS)" }) : null,
4714
+ /* @__PURE__ */ jsxs8(Box7, { marginTop: 1, flexDirection: "row", columnGap: 3, alignItems: "flex-start", children: [
4715
+ qrLines.length > 0 ? /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", flexShrink: 0, children: [
4716
+ /* @__PURE__ */ jsx8(Text8, { bold: true, children: "Scan" }),
4717
+ qrLines.map((line, i) => /* @__PURE__ */ jsx8(Text8, { children: line }, i)),
4718
+ /* @__PURE__ */ jsxs8(Box7, { marginTop: 1, flexDirection: "column", children: [
4719
+ /* @__PURE__ */ jsx8(Text8, { bold: true, children: "Open" }),
4720
+ /* @__PURE__ */ jsx8(Text8, { color: "cyan", wrap: "truncate-end", children: devUrl })
4721
+ ] })
4722
+ ] }) : /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", flexShrink: 0, children: [
4723
+ /* @__PURE__ */ jsx8(Text8, { bold: true, children: "Open" }),
4724
+ /* @__PURE__ */ jsx8(Text8, { color: "cyan", wrap: "truncate-end", children: devUrl })
4725
+ ] }),
4726
+ /* @__PURE__ */ jsxs8(
4727
+ Box7,
4728
+ {
4729
+ flexDirection: "column",
4730
+ flexGrow: 1,
4731
+ minWidth: 28,
4732
+ marginTop: qrLines.length > 0 ? 2 : 0,
4733
+ children: [
4734
+ /* @__PURE__ */ jsxs8(Text8, { children: [
4735
+ "Port: ",
4736
+ /* @__PURE__ */ jsx8(Text8, { color: "cyan", children: port }),
4737
+ " \xB7 LAN: ",
4738
+ /* @__PURE__ */ jsx8(Text8, { color: "cyan", children: lanIp })
4739
+ ] }),
4740
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, wrap: "truncate-end", children: bundlePath }),
4741
+ /* @__PURE__ */ jsxs8(Text8, { dimColor: true, wrap: "truncate-end", children: [
4742
+ devUrl,
4743
+ "/meta.json"
4744
+ ] }),
4745
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, wrap: "truncate-end", children: wsUrl }),
4746
+ bonjour ? /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "mDNS: _tamer._tcp" }) : null,
4747
+ /* @__PURE__ */ jsxs8(Box7, { marginTop: 1, flexDirection: "column", children: [
4748
+ /* @__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" })
4750
+ ] }),
4751
+ /* @__PURE__ */ jsxs8(Box7, { marginTop: 1, flexDirection: "column", children: [
4752
+ /* @__PURE__ */ jsx8(Text8, { bold: true, children: "Connections" }),
4753
+ /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
4754
+ "WebSocket clients: ",
4755
+ wsConnections
4756
+ ] })
4757
+ ] })
4758
+ ]
4759
+ }
4760
+ )
4761
+ ] }),
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" }) })
4771
+ ] });
4772
+ }
4773
+
4774
+ // src/common/tui/hooks/useInputState.ts
4775
+ import { useState as useState2, useCallback } from "react";
4776
+
4777
+ // src/common/tui/hooks/useValidation.ts
4778
+ function isValidAndroidPackage(name) {
4779
+ const s = name.trim();
4780
+ if (!s) return false;
4781
+ return /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/.test(s);
4782
+ }
4783
+ function isValidIosBundleId(id) {
4784
+ const s = id.trim();
4785
+ if (!s) return false;
4786
+ return /^[a-zA-Z][a-zA-Z0-9_-]*(\.[a-zA-Z0-9][a-zA-Z0-9_-]*)+$/.test(s);
4787
+ }
4788
+
4789
+ // src/common/tui/hooks/useServerStatus.ts
4790
+ import { useState as useState3, useCallback as useCallback2 } from "react";
4791
+
4792
+ // src/common/init.tsx
4793
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
4794
+ function resolveSdkInput(raw) {
4795
+ let androidSdk = raw.trim();
4425
4796
  if (androidSdk.startsWith("$") && /^[A-Z0-9_]+$/.test(androidSdk.slice(1))) {
4426
4797
  const envVar = androidSdk.slice(1);
4427
4798
  const envValue = process.env[envVar];
4428
4799
  if (envValue) {
4429
4800
  androidSdk = envValue;
4430
- console.log(`Resolved ${androidSdk} from $${envVar}`);
4431
- } else {
4432
- console.warn(`Environment variable $${envVar} not found. SDK path will be left as-is.`);
4801
+ return { resolved: androidSdk, message: `Using ${androidSdk} from $${envVar}` };
4433
4802
  }
4803
+ return {
4804
+ resolved: androidSdk,
4805
+ message: `Environment variable $${envVar} not found \u2014 path saved as typed.`
4806
+ };
4434
4807
  }
4435
- const useSame = await ask("Use same name and bundle ID for iOS as Android? (y/N): ");
4436
- let iosAppName;
4437
- let iosBundleId;
4438
- if (/^y(es)?$/i.test(useSame)) {
4439
- iosAppName = androidAppName;
4440
- iosBundleId = androidPackageName;
4441
- } else {
4442
- iosAppName = await ask("iOS app name: ");
4443
- iosBundleId = await ask("iOS bundle ID (e.g. com.example.app): ");
4444
- }
4445
- const lynxProject = await ask("Lynx project path (relative to project root, e.g. packages/example) [optional]: ");
4446
- const config = {
4447
- android: {
4448
- appName: androidAppName || void 0,
4449
- packageName: androidPackageName || void 0,
4450
- sdk: androidSdk || void 0
4451
- },
4452
- ios: {
4453
- appName: iosAppName || void 0,
4454
- bundleId: iosBundleId || void 0
4455
- },
4456
- paths: { androidDir: "android", iosDir: "ios" }
4457
- };
4458
- if (lynxProject) config.lynxProject = lynxProject;
4459
- const configPath = path18.join(process.cwd(), "tamer.config.json");
4460
- fs17.writeFileSync(configPath, JSON.stringify(config, null, 2));
4461
- console.log(`
4462
- \u2705 Generated tamer.config.json at ${configPath}`);
4463
- const tamerTypesInclude = "node_modules/@tamer4lynx/tamer-*/src/**/*.d.ts";
4464
- const tsconfigCandidates = lynxProject ? [path18.join(process.cwd(), lynxProject, "tsconfig.json"), path18.join(process.cwd(), "tsconfig.json")] : [path18.join(process.cwd(), "tsconfig.json")];
4465
- function parseTsconfigJson(raw) {
4466
- try {
4467
- return JSON.parse(raw);
4468
- } catch {
4469
- const noTrailingCommas = raw.replace(/,\s*([\]}])/g, "$1");
4470
- return JSON.parse(noTrailingCommas);
4808
+ return { resolved: androidSdk };
4809
+ }
4810
+ function InitWizard() {
4811
+ const [step, setStep] = useState4("welcome");
4812
+ const [androidAppName, setAndroidAppName] = useState4("");
4813
+ const [androidPackageName, setAndroidPackageName] = useState4("");
4814
+ const [androidSdk, setAndroidSdk] = useState4("");
4815
+ const [sdkHint, setSdkHint] = useState4();
4816
+ const [iosAppName, setIosAppName] = useState4("");
4817
+ const [iosBundleId, setIosBundleId] = useState4("");
4818
+ const [lynxProject, setLynxProject] = useState4("");
4819
+ const [pkgError, setPkgError] = useState4();
4820
+ const [bundleError, setBundleError] = useState4();
4821
+ const [doneMessage, setDoneMessage] = useState4([]);
4822
+ const writeConfigAndTsconfig = useCallback3(() => {
4823
+ const config = {
4824
+ android: {
4825
+ appName: androidAppName || void 0,
4826
+ packageName: androidPackageName || void 0,
4827
+ sdk: androidSdk || void 0
4828
+ },
4829
+ ios: {
4830
+ appName: iosAppName || void 0,
4831
+ bundleId: iosBundleId || void 0
4832
+ },
4833
+ paths: { androidDir: "android", iosDir: "ios" }
4834
+ };
4835
+ 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));
4838
+ const lines = [`Generated tamer.config.json at ${configPath}`];
4839
+ const tamerTypesInclude = "node_modules/@tamer4lynx/tamer-*/src/**/*.d.ts";
4840
+ 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")];
4844
+ for (const tsconfigPath of tsconfigCandidates) {
4845
+ if (!fs18.existsSync(tsconfigPath)) continue;
4846
+ try {
4847
+ if (fixTsconfigReferencesForBuild(tsconfigPath)) {
4848
+ lines.push(`Flattened ${path19.relative(process.cwd(), tsconfigPath)} (fixed TS6310)`);
4849
+ }
4850
+ if (addTamerTypesInclude(tsconfigPath, tamerTypesInclude)) {
4851
+ lines.push(`Updated ${path19.relative(process.cwd(), tsconfigPath)} for tamer types`);
4852
+ }
4853
+ break;
4854
+ } catch (e) {
4855
+ lines.push(`Could not update ${tsconfigPath}: ${e.message}`);
4856
+ }
4471
4857
  }
4858
+ setDoneMessage(lines);
4859
+ setStep("done");
4860
+ setTimeout(() => process.exit(0), 2e3);
4861
+ }, [androidAppName, androidPackageName, androidSdk, iosAppName, iosBundleId, lynxProject]);
4862
+ useEffect2(() => {
4863
+ if (step !== "saving") return;
4864
+ writeConfigAndTsconfig();
4865
+ }, [step, writeConfigAndTsconfig]);
4866
+ if (step === "welcome") {
4867
+ return /* @__PURE__ */ jsxs9(Box8, { flexDirection: "column", children: [
4868
+ /* @__PURE__ */ jsx9(Text9, { bold: true, children: "Tamer4Lynx init" }),
4869
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Set up tamer.config.json for your project." }),
4870
+ /* @__PURE__ */ jsx9(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx9(
4871
+ TuiSelectInput,
4872
+ {
4873
+ label: "Continue?",
4874
+ items: [{ label: "Start", value: "start" }],
4875
+ onSelect: () => setStep("android-app")
4876
+ }
4877
+ ) })
4878
+ ] });
4879
+ }
4880
+ if (step === "android-app") {
4881
+ return /* @__PURE__ */ jsx9(Wizard, { step: 1, total: 6, title: "Android app name", children: /* @__PURE__ */ jsx9(
4882
+ TuiTextInput,
4883
+ {
4884
+ label: "Android app name:",
4885
+ defaultValue: androidAppName,
4886
+ onSubmitValue: (v) => setAndroidAppName(v),
4887
+ onSubmit: () => setStep("android-pkg")
4888
+ },
4889
+ step
4890
+ ) });
4891
+ }
4892
+ if (step === "android-pkg") {
4893
+ return /* @__PURE__ */ jsx9(Wizard, { step: 2, total: 6, title: "Android package name", children: /* @__PURE__ */ jsx9(
4894
+ TuiTextInput,
4895
+ {
4896
+ label: "Android package name (e.g. com.example.app):",
4897
+ defaultValue: androidPackageName,
4898
+ error: pkgError,
4899
+ onChange: () => setPkgError(void 0),
4900
+ onSubmitValue: (v) => {
4901
+ const t = v.trim();
4902
+ if (t && !isValidAndroidPackage(t)) {
4903
+ setPkgError("Use reverse-DNS form: com.mycompany.app");
4904
+ return false;
4905
+ }
4906
+ setAndroidPackageName(t);
4907
+ setPkgError(void 0);
4908
+ },
4909
+ onSubmit: () => setStep("android-sdk")
4910
+ },
4911
+ step
4912
+ ) });
4913
+ }
4914
+ if (step === "android-sdk") {
4915
+ return /* @__PURE__ */ jsx9(Wizard, { step: 3, total: 6, title: "Android SDK", children: /* @__PURE__ */ jsx9(
4916
+ TuiTextInput,
4917
+ {
4918
+ label: "Android SDK path (e.g. ~/Library/Android/sdk or $ANDROID_HOME):",
4919
+ defaultValue: androidSdk,
4920
+ onSubmitValue: (v) => {
4921
+ const { resolved, message } = resolveSdkInput(v);
4922
+ setAndroidSdk(resolved);
4923
+ setSdkHint(message);
4924
+ },
4925
+ onSubmit: () => setStep("ios-reuse"),
4926
+ hint: sdkHint
4927
+ },
4928
+ step
4929
+ ) });
4930
+ }
4931
+ if (step === "ios-reuse") {
4932
+ return /* @__PURE__ */ jsx9(Wizard, { step: 4, total: 6, title: "iOS", children: /* @__PURE__ */ jsx9(
4933
+ TuiConfirmInput,
4934
+ {
4935
+ label: "Use the same app name and bundle ID for iOS as Android?",
4936
+ defaultYes: false,
4937
+ onConfirm: (yes) => {
4938
+ if (yes) {
4939
+ setIosAppName(androidAppName);
4940
+ setIosBundleId(androidPackageName);
4941
+ setStep("lynx-path");
4942
+ } else {
4943
+ setStep("ios-app");
4944
+ }
4945
+ },
4946
+ hint: "No = enter iOS-specific values next"
4947
+ }
4948
+ ) });
4949
+ }
4950
+ if (step === "ios-app") {
4951
+ return /* @__PURE__ */ jsx9(Wizard, { step: 4, total: 6, title: "iOS app name", children: /* @__PURE__ */ jsx9(
4952
+ TuiTextInput,
4953
+ {
4954
+ label: "iOS app name:",
4955
+ defaultValue: iosAppName,
4956
+ onSubmitValue: (v) => setIosAppName(v),
4957
+ onSubmit: () => setStep("ios-bundle")
4958
+ },
4959
+ step
4960
+ ) });
4961
+ }
4962
+ if (step === "ios-bundle") {
4963
+ return /* @__PURE__ */ jsx9(Wizard, { step: 5, total: 6, title: "iOS bundle ID", children: /* @__PURE__ */ jsx9(
4964
+ TuiTextInput,
4965
+ {
4966
+ label: "iOS bundle ID (e.g. com.example.app):",
4967
+ defaultValue: iosBundleId,
4968
+ error: bundleError,
4969
+ onChange: () => setBundleError(void 0),
4970
+ onSubmitValue: (v) => {
4971
+ const t = v.trim();
4972
+ if (t && !isValidIosBundleId(t)) {
4973
+ setBundleError("Use reverse-DNS form: com.mycompany.App");
4974
+ return false;
4975
+ }
4976
+ setIosBundleId(t);
4977
+ setBundleError(void 0);
4978
+ },
4979
+ onSubmit: () => setStep("lynx-path")
4980
+ },
4981
+ step
4982
+ ) });
4983
+ }
4984
+ if (step === "lynx-path") {
4985
+ return /* @__PURE__ */ jsx9(Wizard, { step: 6, total: 6, title: "Lynx project", children: /* @__PURE__ */ jsx9(
4986
+ TuiTextInput,
4987
+ {
4988
+ label: "Lynx project path relative to project root (optional, e.g. packages/example):",
4989
+ defaultValue: lynxProject,
4990
+ onSubmitValue: (v) => setLynxProject(v),
4991
+ onSubmit: () => setStep("saving"),
4992
+ hint: "Press Enter with empty to skip"
4993
+ },
4994
+ step
4995
+ ) });
4472
4996
  }
4473
- for (const tsconfigPath of tsconfigCandidates) {
4474
- if (!fs17.existsSync(tsconfigPath)) continue;
4475
- try {
4476
- const raw = fs17.readFileSync(tsconfigPath, "utf-8");
4477
- const tsconfig = parseTsconfigJson(raw);
4478
- const include = tsconfig.include ?? [];
4479
- const arr = Array.isArray(include) ? include : [include];
4480
- if (arr.some((p) => (typeof p === "string" ? p : "").includes("tamer-"))) continue;
4481
- arr.push(tamerTypesInclude);
4482
- tsconfig.include = arr;
4483
- fs17.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
4484
- console.log(`\u2705 Updated ${path18.relative(process.cwd(), tsconfigPath)} to include tamer type declarations`);
4485
- break;
4486
- } catch (e) {
4487
- console.warn(`\u26A0 Could not update ${tsconfigPath}:`, e.message);
4488
- }
4997
+ if (step === "saving") {
4998
+ return /* @__PURE__ */ jsx9(Box8, { children: /* @__PURE__ */ jsx9(TuiSpinner, { label: "Writing tamer.config.json and updating tsconfig\u2026" }) });
4489
4999
  }
4490
- rl.close();
5000
+ if (step === "done") {
5001
+ return /* @__PURE__ */ jsx9(Box8, { flexDirection: "column", children: /* @__PURE__ */ jsx9(StatusBox, { variant: "success", title: "Done", children: doneMessage.map((line, i) => /* @__PURE__ */ jsx9(Text9, { color: "green", children: line }, i)) }) });
5002
+ }
5003
+ return null;
5004
+ }
5005
+ async function init() {
5006
+ const { waitUntilExit } = render(/* @__PURE__ */ jsx9(InitWizard, {}));
5007
+ await waitUntilExit();
4491
5008
  }
4492
- var init_default = init;
4493
5009
 
4494
5010
  // src/common/create.ts
4495
- import fs18 from "fs";
4496
- import path19 from "path";
4497
- import readline2 from "readline";
4498
- var rl2 = readline2.createInterface({ input: process.stdin, output: process.stdout, terminal: false });
4499
- function ask2(question) {
4500
- return new Promise((resolve) => rl2.question(question, (answer) => resolve(answer.trim())));
5011
+ import fs19 from "fs";
5012
+ import path20 from "path";
5013
+ import readline from "readline";
5014
+ var rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false });
5015
+ function ask(question) {
5016
+ return new Promise((resolve) => rl.question(question, (answer) => resolve(answer.trim())));
4501
5017
  }
4502
5018
  async function create3(opts) {
4503
5019
  console.log("Tamer4Lynx: Create Lynx Extension\n");
@@ -4536,32 +5052,32 @@ async function create3(opts) {
4536
5052
  console.log(" [ ] Native Module");
4537
5053
  console.log(" [ ] Element");
4538
5054
  console.log(" [ ] Service\n");
4539
- includeModule = /^y(es)?$/i.test(await ask2("Include Native Module? (Y/n): ") || "y");
4540
- includeElement = /^y(es)?$/i.test(await ask2("Include Element? (y/N): ") || "n");
4541
- includeService = /^y(es)?$/i.test(await ask2("Include Service? (y/N): ") || "n");
5055
+ includeModule = /^y(es)?$/i.test(await ask("Include Native Module? (Y/n): ") || "y");
5056
+ includeElement = /^y(es)?$/i.test(await ask("Include Element? (y/N): ") || "n");
5057
+ includeService = /^y(es)?$/i.test(await ask("Include Service? (y/N): ") || "n");
4542
5058
  }
4543
5059
  if (!includeModule && !includeElement && !includeService) {
4544
5060
  console.error("\u274C At least one extension type is required.");
4545
- rl2.close();
5061
+ rl.close();
4546
5062
  process.exit(1);
4547
5063
  }
4548
- const extName = await ask2("Extension package name (e.g. my-lynx-module): ");
5064
+ const extName = await ask("Extension package name (e.g. my-lynx-module): ");
4549
5065
  if (!extName || !/^[a-z0-9-_]+$/.test(extName)) {
4550
5066
  console.error("\u274C Invalid package name. Use lowercase letters, numbers, hyphens, underscores.");
4551
- rl2.close();
5067
+ rl.close();
4552
5068
  process.exit(1);
4553
5069
  }
4554
- const packageName = await ask2("Android package name (e.g. com.example.mymodule): ") || `com.example.${extName.replace(/-/g, "")}`;
5070
+ const packageName = await ask("Android package name (e.g. com.example.mymodule): ") || `com.example.${extName.replace(/-/g, "")}`;
4555
5071
  const simpleModuleName = extName.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("") + "Module";
4556
5072
  const fullModuleClassName = `${packageName}.${simpleModuleName}`;
4557
5073
  const cwd = process.cwd();
4558
- const root = path19.join(cwd, extName);
4559
- if (fs18.existsSync(root)) {
5074
+ const root = path20.join(cwd, extName);
5075
+ if (fs19.existsSync(root)) {
4560
5076
  console.error(`\u274C Directory ${extName} already exists.`);
4561
- rl2.close();
5077
+ rl.close();
4562
5078
  process.exit(1);
4563
5079
  }
4564
- fs18.mkdirSync(root, { recursive: true });
5080
+ fs19.mkdirSync(root, { recursive: true });
4565
5081
  const lynxExt = {
4566
5082
  platforms: {
4567
5083
  android: {
@@ -4576,7 +5092,7 @@ async function create3(opts) {
4576
5092
  web: {}
4577
5093
  }
4578
5094
  };
4579
- fs18.writeFileSync(path19.join(root, "lynx.ext.json"), JSON.stringify(lynxExt, null, 2));
5095
+ fs19.writeFileSync(path20.join(root, "lynx.ext.json"), JSON.stringify(lynxExt, null, 2));
4580
5096
  const pkg = {
4581
5097
  name: extName,
4582
5098
  version: "0.0.1",
@@ -4589,20 +5105,20 @@ async function create3(opts) {
4589
5105
  engines: { node: ">=18" }
4590
5106
  };
4591
5107
  if (includeModule) pkg.types = "src/index.d.ts";
4592
- fs18.writeFileSync(path19.join(root, "package.json"), JSON.stringify(pkg, null, 2));
5108
+ fs19.writeFileSync(path20.join(root, "package.json"), JSON.stringify(pkg, null, 2));
4593
5109
  const pkgPath = packageName.replace(/\./g, "/");
4594
5110
  const hasSrc = includeModule || includeElement || includeService;
4595
5111
  if (hasSrc) {
4596
- fs18.mkdirSync(path19.join(root, "src"), { recursive: true });
5112
+ fs19.mkdirSync(path20.join(root, "src"), { recursive: true });
4597
5113
  }
4598
5114
  if (includeModule) {
4599
- fs18.writeFileSync(path19.join(root, "src", "index.d.ts"), `/** @lynxmodule */
5115
+ fs19.writeFileSync(path20.join(root, "src", "index.d.ts"), `/** @lynxmodule */
4600
5116
  export declare class ${simpleModuleName} {
4601
5117
  // Add your module methods here
4602
5118
  }
4603
5119
  `);
4604
- fs18.mkdirSync(path19.join(root, "android", "src", "main", "kotlin", pkgPath), { recursive: true });
4605
- fs18.writeFileSync(path19.join(root, "android", "build.gradle.kts"), `plugins {
5120
+ fs19.mkdirSync(path20.join(root, "android", "src", "main", "kotlin", pkgPath), { recursive: true });
5121
+ fs19.writeFileSync(path20.join(root, "android", "build.gradle.kts"), `plugins {
4606
5122
  id("com.android.library")
4607
5123
  id("org.jetbrains.kotlin.android")
4608
5124
  }
@@ -4623,7 +5139,7 @@ dependencies {
4623
5139
  implementation(libs.lynx.jssdk)
4624
5140
  }
4625
5141
  `);
4626
- fs18.writeFileSync(path19.join(root, "android", "src", "main", "AndroidManifest.xml"), `<?xml version="1.0" encoding="utf-8"?>
5142
+ fs19.writeFileSync(path20.join(root, "android", "src", "main", "AndroidManifest.xml"), `<?xml version="1.0" encoding="utf-8"?>
4627
5143
  <manifest />
4628
5144
  `);
4629
5145
  const ktContent = `package ${packageName}
@@ -4640,8 +5156,8 @@ class ${simpleModuleName}(context: Context) : LynxModule(context) {
4640
5156
  }
4641
5157
  }
4642
5158
  `;
4643
- fs18.writeFileSync(path19.join(root, "android", "src", "main", "kotlin", pkgPath, `${simpleModuleName}.kt`), ktContent);
4644
- fs18.mkdirSync(path19.join(root, "ios", extName, extName, "Classes"), { recursive: true });
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 });
4645
5161
  const podspec = `Pod::Spec.new do |s|
4646
5162
  s.name = '${extName}'
4647
5163
  s.version = '0.0.1'
@@ -4655,7 +5171,7 @@ class ${simpleModuleName}(context: Context) : LynxModule(context) {
4655
5171
  s.dependency 'Lynx'
4656
5172
  end
4657
5173
  `;
4658
- fs18.writeFileSync(path19.join(root, "ios", extName, `${extName}.podspec`), podspec);
5174
+ fs19.writeFileSync(path20.join(root, "ios", extName, `${extName}.podspec`), podspec);
4659
5175
  const swiftContent = `import Foundation
4660
5176
 
4661
5177
  @objc public class ${simpleModuleName}: NSObject {
@@ -4664,18 +5180,18 @@ end
4664
5180
  }
4665
5181
  }
4666
5182
  `;
4667
- fs18.writeFileSync(path19.join(root, "ios", extName, extName, "Classes", `${simpleModuleName}.swift`), swiftContent);
5183
+ fs19.writeFileSync(path20.join(root, "ios", extName, extName, "Classes", `${simpleModuleName}.swift`), swiftContent);
4668
5184
  }
4669
5185
  if (includeElement && !includeModule) {
4670
5186
  const elementName = extName.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
4671
- fs18.writeFileSync(path19.join(root, "src", "index.tsx"), `import type { FC } from '@lynx-js/react';
5187
+ fs19.writeFileSync(path20.join(root, "src", "index.tsx"), `import type { FC } from '@lynx-js/react';
4672
5188
 
4673
5189
  export const ${elementName}: FC = () => {
4674
5190
  return null;
4675
5191
  };
4676
5192
  `);
4677
5193
  }
4678
- fs18.writeFileSync(path19.join(root, "index.js"), `'use strict';
5194
+ fs19.writeFileSync(path20.join(root, "index.js"), `'use strict';
4679
5195
  module.exports = {};
4680
5196
  `);
4681
5197
  const tsconfigCompiler = {
@@ -4688,11 +5204,11 @@ module.exports = {};
4688
5204
  tsconfigCompiler.jsx = "preserve";
4689
5205
  tsconfigCompiler.jsxImportSource = "@lynx-js/react";
4690
5206
  }
4691
- fs18.writeFileSync(path19.join(root, "tsconfig.json"), JSON.stringify({
5207
+ fs19.writeFileSync(path20.join(root, "tsconfig.json"), JSON.stringify({
4692
5208
  compilerOptions: tsconfigCompiler,
4693
5209
  include: includeElement ? ["src", "src/**/*.tsx"] : ["src"]
4694
5210
  }, null, 2));
4695
- fs18.writeFileSync(path19.join(root, "README.md"), `# ${extName}
5211
+ fs19.writeFileSync(path20.join(root, "README.md"), `# ${extName}
4696
5212
 
4697
5213
  Lynx extension for ${extName}.
4698
5214
 
@@ -4712,13 +5228,13 @@ This package uses \`lynx.ext.json\` (RFC-compliant) for autolinking.
4712
5228
  console.log(` cd ${extName}`);
4713
5229
  console.log(" npm install");
4714
5230
  if (includeModule) console.log(" npm run codegen");
4715
- rl2.close();
5231
+ rl.close();
4716
5232
  }
4717
5233
  var create_default3 = create3;
4718
5234
 
4719
5235
  // src/common/codegen.ts
4720
- import fs19 from "fs";
4721
- import path20 from "path";
5236
+ import fs20 from "fs";
5237
+ import path21 from "path";
4722
5238
  function codegen() {
4723
5239
  const cwd = process.cwd();
4724
5240
  const config = loadExtensionConfig(cwd);
@@ -4726,9 +5242,9 @@ function codegen() {
4726
5242
  console.error("\u274C No lynx.ext.json or tamer.json found. Run from an extension package root.");
4727
5243
  process.exit(1);
4728
5244
  }
4729
- const srcDir = path20.join(cwd, "src");
4730
- const generatedDir = path20.join(cwd, "generated");
4731
- fs19.mkdirSync(generatedDir, { recursive: true });
5245
+ const srcDir = path21.join(cwd, "src");
5246
+ const generatedDir = path21.join(cwd, "generated");
5247
+ fs20.mkdirSync(generatedDir, { recursive: true });
4732
5248
  const dtsFiles = findDtsFiles(srcDir);
4733
5249
  const modules = extractLynxModules(dtsFiles);
4734
5250
  if (modules.length === 0) {
@@ -4738,28 +5254,28 @@ function codegen() {
4738
5254
  for (const mod of modules) {
4739
5255
  const tsContent = `export type { ${mod} } from '../src/index.js';
4740
5256
  `;
4741
- const outPath = path20.join(generatedDir, `${mod}.ts`);
4742
- fs19.writeFileSync(outPath, tsContent);
5257
+ const outPath = path21.join(generatedDir, `${mod}.ts`);
5258
+ fs20.writeFileSync(outPath, tsContent);
4743
5259
  console.log(`\u2705 Generated ${outPath}`);
4744
5260
  }
4745
5261
  if (config.android) {
4746
- const androidGenerated = path20.join(cwd, "android", "src", "main", "kotlin", config.android.moduleClassName.replace(/\./g, "/").replace(/[^/]+$/, ""), "generated");
4747
- fs19.mkdirSync(androidGenerated, { recursive: true });
5262
+ const androidGenerated = path21.join(cwd, "android", "src", "main", "kotlin", config.android.moduleClassName.replace(/\./g, "/").replace(/[^/]+$/, ""), "generated");
5263
+ fs20.mkdirSync(androidGenerated, { recursive: true });
4748
5264
  console.log(`\u2139\uFE0F Android generated dir: ${androidGenerated} (spec generation coming soon)`);
4749
5265
  }
4750
5266
  if (config.ios) {
4751
- const iosGenerated = path20.join(cwd, "ios", "generated");
4752
- fs19.mkdirSync(iosGenerated, { recursive: true });
5267
+ const iosGenerated = path21.join(cwd, "ios", "generated");
5268
+ fs20.mkdirSync(iosGenerated, { recursive: true });
4753
5269
  console.log(`\u2139\uFE0F iOS generated dir: ${iosGenerated} (spec generation coming soon)`);
4754
5270
  }
4755
5271
  console.log("\u2728 Codegen complete.");
4756
5272
  }
4757
5273
  function findDtsFiles(dir) {
4758
5274
  const result = [];
4759
- if (!fs19.existsSync(dir)) return result;
4760
- const entries = fs19.readdirSync(dir, { withFileTypes: true });
5275
+ if (!fs20.existsSync(dir)) return result;
5276
+ const entries = fs20.readdirSync(dir, { withFileTypes: true });
4761
5277
  for (const e of entries) {
4762
- const full = path20.join(dir, e.name);
5278
+ const full = path21.join(dir, e.name);
4763
5279
  if (e.isDirectory()) result.push(...findDtsFiles(full));
4764
5280
  else if (e.name.endsWith(".d.ts")) result.push(full);
4765
5281
  }
@@ -4769,7 +5285,7 @@ function extractLynxModules(files) {
4769
5285
  const modules = [];
4770
5286
  const seen = /* @__PURE__ */ new Set();
4771
5287
  for (const file of files) {
4772
- const content = fs19.readFileSync(file, "utf8");
5288
+ const content = fs20.readFileSync(file, "utf8");
4773
5289
  const regex = /\/\*\*\s*@lynxmodule\s*\*\/\s*export\s+declare\s+class\s+(\w+)/g;
4774
5290
  let m;
4775
5291
  while ((m = regex.exec(content)) !== null) {
@@ -4783,14 +5299,16 @@ function extractLynxModules(files) {
4783
5299
  }
4784
5300
  var codegen_default = codegen;
4785
5301
 
4786
- // src/common/devServer.ts
5302
+ // src/common/devServer.tsx
5303
+ import { useState as useState5, useEffect as useEffect3, useRef, useCallback as useCallback4 } from "react";
4787
5304
  import { spawn } from "child_process";
4788
- import fs20 from "fs";
5305
+ import fs21 from "fs";
4789
5306
  import http from "http";
4790
5307
  import os4 from "os";
4791
- import path21 from "path";
4792
- import readline3 from "readline";
5308
+ import path22 from "path";
5309
+ import { render as render2, useInput, useApp } from "ink";
4793
5310
  import { WebSocketServer } from "ws";
5311
+ import { jsx as jsx10 } from "react/jsx-runtime";
4794
5312
  var DEFAULT_PORT = 3e3;
4795
5313
  var STATIC_MIME = {
4796
5314
  ".png": "image/png",
@@ -4803,13 +5321,13 @@ var STATIC_MIME = {
4803
5321
  ".pdf": "application/pdf"
4804
5322
  };
4805
5323
  function sendFileFromDisk(res, absPath) {
4806
- fs20.readFile(absPath, (err, data) => {
5324
+ fs21.readFile(absPath, (err, data) => {
4807
5325
  if (err) {
4808
5326
  res.writeHead(404);
4809
5327
  res.end("Not found");
4810
5328
  return;
4811
5329
  }
4812
- const ext = path21.extname(absPath).toLowerCase();
5330
+ const ext = path22.extname(absPath).toLowerCase();
4813
5331
  res.setHeader("Content-Type", STATIC_MIME[ext] ?? "application/octet-stream");
4814
5332
  res.setHeader("Access-Control-Allow-Origin", "*");
4815
5333
  res.end(data);
@@ -4845,319 +5363,431 @@ function getLanIp() {
4845
5363
  }
4846
5364
  return "localhost";
4847
5365
  }
4848
- async function startDevServer(opts) {
4849
- const verbose = opts?.verbose ?? false;
4850
- const resolved = resolveHostPaths();
4851
- const { projectRoot, lynxProjectDir, lynxBundlePath, lynxBundleFile, config } = resolved;
4852
- const distDir = path21.dirname(lynxBundlePath);
4853
- let buildProcess = null;
4854
- function detectPackageManager2(cwd) {
4855
- const dir = path21.resolve(cwd);
4856
- if (fs20.existsSync(path21.join(dir, "pnpm-lock.yaml"))) return { cmd: "pnpm", args: ["run", "build"] };
4857
- if (fs20.existsSync(path21.join(dir, "bun.lockb")) || fs20.existsSync(path21.join(dir, "bun.lock"))) return { cmd: "bun", args: ["run", "build"] };
4858
- return { cmd: "npm", args: ["run", "build"] };
4859
- }
4860
- function runBuild() {
4861
- return new Promise((resolve, reject) => {
4862
- const { cmd, args } = detectPackageManager2(lynxProjectDir);
4863
- buildProcess = spawn(cmd, args, {
4864
- cwd: lynxProjectDir,
4865
- stdio: "pipe",
4866
- shell: process.platform === "win32"
4867
- });
4868
- let stderr = "";
4869
- buildProcess.stderr?.on("data", (d) => {
4870
- stderr += d.toString();
4871
- });
4872
- buildProcess.on("close", (code) => {
4873
- buildProcess = null;
4874
- if (code === 0) resolve();
4875
- else reject(new Error(stderr || `Build exited ${code}`));
4876
- });
4877
- });
4878
- }
4879
- const preferredPort = config.devServer?.port ?? config.devServer?.httpPort ?? DEFAULT_PORT;
4880
- const port = await findAvailablePort(preferredPort);
4881
- if (port !== preferredPort) {
4882
- console.log(`\x1B[33m\u26A0 Port ${preferredPort} in use, using ${port}\x1B[0m`);
4883
- }
4884
- const projectName = path21.basename(lynxProjectDir);
4885
- const basePath = `/${projectName}`;
4886
- const iconPaths = resolveIconPaths(projectRoot, config);
4887
- let iconFilePath = null;
4888
- if (iconPaths?.source && fs20.statSync(iconPaths.source).isFile()) {
4889
- iconFilePath = iconPaths.source;
4890
- } else if (iconPaths?.androidAdaptiveForeground && fs20.statSync(iconPaths.androidAdaptiveForeground).isFile()) {
4891
- iconFilePath = iconPaths.androidAdaptiveForeground;
4892
- } else if (iconPaths?.android) {
4893
- const androidIcon = path21.join(iconPaths.android, "mipmap-xxxhdpi", "ic_launcher.png");
4894
- if (fs20.existsSync(androidIcon)) iconFilePath = androidIcon;
4895
- } else if (iconPaths?.ios) {
4896
- const iosIcon = path21.join(iconPaths.ios, "Icon-1024.png");
4897
- if (fs20.existsSync(iosIcon)) iconFilePath = iosIcon;
4898
- }
4899
- const iconExt = iconFilePath ? path21.extname(iconFilePath) || ".png" : "";
4900
- const httpServer = http.createServer((req, res) => {
4901
- let reqPath = (req.url || "/").split("?")[0];
4902
- if (reqPath === `${basePath}/status`) {
4903
- res.setHeader("Content-Type", "text/plain");
4904
- res.setHeader("Access-Control-Allow-Origin", "*");
4905
- res.end("packager-status:running");
5366
+ 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")))
5370
+ return { cmd: "bun", args: ["run", "build"] };
5371
+ return { cmd: "npm", args: ["run", "build"] };
5372
+ }
5373
+ var initialUi = () => ({
5374
+ phase: "starting",
5375
+ projectName: "",
5376
+ port: 0,
5377
+ lanIp: "localhost",
5378
+ devUrl: "",
5379
+ wsUrl: "",
5380
+ lynxBundleFile: "main.lynx.bundle",
5381
+ bonjour: false,
5382
+ verbose: false,
5383
+ buildPhase: "idle",
5384
+ wsConnections: 0,
5385
+ logLines: [],
5386
+ showLogs: false,
5387
+ qrLines: []
5388
+ });
5389
+ function DevServerApp({ verbose }) {
5390
+ const { exit } = useApp();
5391
+ const [ui, setUi] = useState5(() => {
5392
+ const s = initialUi();
5393
+ s.verbose = verbose;
5394
+ return s;
5395
+ });
5396
+ const cleanupRef = useRef(null);
5397
+ const rebuildRef = useRef(() => Promise.resolve());
5398
+ const quitOnceRef = useRef(false);
5399
+ const appendLog = useCallback4((chunk) => {
5400
+ const lines = chunk.split(/\r?\n/).filter(Boolean);
5401
+ setUi((prev) => ({
5402
+ ...prev,
5403
+ logLines: [...prev.logLines, ...lines].slice(-400)
5404
+ }));
5405
+ }, []);
5406
+ const handleQuit = useCallback4(() => {
5407
+ if (quitOnceRef.current) return;
5408
+ quitOnceRef.current = true;
5409
+ void cleanupRef.current?.();
5410
+ exit();
5411
+ }, [exit]);
5412
+ useInput((input, key) => {
5413
+ if (key.ctrl && key.name === "c") {
5414
+ handleQuit();
4906
5415
  return;
4907
5416
  }
4908
- if (reqPath === `${basePath}/meta.json`) {
4909
- const lanIp = getLanIp();
4910
- const nativeModules = discoverNativeExtensions(projectRoot);
4911
- const androidPackageName = config.android?.packageName?.trim();
4912
- const iosBundleId = config.ios?.bundleId?.trim();
4913
- const idParts = [androidPackageName?.toLowerCase(), iosBundleId?.toLowerCase()].filter(
4914
- (x) => Boolean(x)
4915
- );
4916
- const meta = {
4917
- name: projectName,
4918
- slug: projectName,
4919
- bundleUrl: `http://${lanIp}:${port}${basePath}/${lynxBundleFile}`,
4920
- bundleFile: lynxBundleFile,
4921
- hostUri: `http://${lanIp}:${port}${basePath}`,
4922
- debuggerHost: `${lanIp}:${port}`,
4923
- developer: { tool: "tamer4lynx" },
4924
- packagerStatus: "running",
4925
- nativeModules: nativeModules.map((m) => ({ packageName: m.packageName, moduleClassName: m.moduleClassName }))
4926
- };
4927
- if (androidPackageName) meta.androidPackageName = androidPackageName;
4928
- if (iosBundleId) meta.iosBundleId = iosBundleId;
4929
- if (idParts.length > 0) meta.tamerAppKey = idParts.join("|");
4930
- const rawIcon = config.icon;
4931
- if (rawIcon && typeof rawIcon === "object" && "source" in rawIcon && typeof rawIcon.source === "string") {
4932
- meta.iconSource = rawIcon.source;
4933
- } else if (typeof rawIcon === "string") {
4934
- meta.iconSource = rawIcon;
4935
- }
4936
- if (iconFilePath) {
4937
- meta.icon = `http://${lanIp}:${port}${basePath}/icon${iconExt}`;
4938
- }
4939
- res.setHeader("Content-Type", "application/json");
4940
- res.setHeader("Access-Control-Allow-Origin", "*");
4941
- res.end(JSON.stringify(meta, null, 2));
5417
+ if (input === "q") {
5418
+ handleQuit();
4942
5419
  return;
4943
5420
  }
4944
- if (iconFilePath && (reqPath === `${basePath}/icon` || reqPath === `${basePath}/icon${iconExt}`)) {
4945
- fs20.readFile(iconFilePath, (err, data) => {
4946
- if (err) {
4947
- res.writeHead(404);
4948
- res.end();
4949
- return;
4950
- }
4951
- res.setHeader("Content-Type", STATIC_MIME[iconExt] ?? "image/png");
4952
- res.setHeader("Access-Control-Allow-Origin", "*");
4953
- res.end(data);
4954
- });
5421
+ if (input === "r") {
5422
+ void rebuildRef.current();
4955
5423
  return;
4956
5424
  }
4957
- const lynxStaticMounts = [
4958
- { prefix: `${basePath}/src/assets/`, rootSub: "src/assets" },
4959
- { prefix: `${basePath}/assets/`, rootSub: "assets" }
4960
- ];
4961
- for (const { prefix, rootSub } of lynxStaticMounts) {
4962
- if (!reqPath.startsWith(prefix)) continue;
4963
- let rel = reqPath.slice(prefix.length);
4964
- try {
4965
- rel = decodeURIComponent(rel);
4966
- } catch {
4967
- res.writeHead(400);
4968
- res.end();
4969
- return;
4970
- }
4971
- const safe = path21.normalize(rel).replace(/^(\.\.(\/|\\|$))+/, "");
4972
- if (path21.isAbsolute(safe) || safe.startsWith("..")) {
4973
- res.writeHead(403);
4974
- res.end();
4975
- return;
4976
- }
4977
- const allowedRoot = path21.resolve(lynxProjectDir, rootSub);
4978
- const abs = path21.resolve(allowedRoot, safe);
4979
- if (!abs.startsWith(allowedRoot + path21.sep) && abs !== allowedRoot) {
4980
- res.writeHead(403);
4981
- res.end();
4982
- return;
4983
- }
4984
- if (!fs20.existsSync(abs) || !fs20.statSync(abs).isFile()) {
4985
- res.writeHead(404);
4986
- res.end("Not found");
4987
- return;
4988
- }
4989
- sendFileFromDisk(res, abs);
4990
- return;
4991
- }
4992
- if (reqPath === "/" || reqPath === basePath || reqPath === `${basePath}/`) {
4993
- reqPath = `${basePath}/${lynxBundleFile}`;
4994
- } else if (!reqPath.startsWith(basePath)) {
4995
- reqPath = basePath + (reqPath.startsWith("/") ? reqPath : "/" + reqPath);
4996
- }
4997
- const relPath = reqPath.replace(basePath, "").replace(/^\//, "") || lynxBundleFile;
4998
- const filePath = path21.resolve(distDir, relPath);
4999
- const distResolved = path21.resolve(distDir);
5000
- if (!filePath.startsWith(distResolved + path21.sep) && filePath !== distResolved) {
5001
- res.writeHead(403);
5002
- res.end();
5003
- return;
5004
- }
5005
- fs20.readFile(filePath, (err, data) => {
5006
- if (err) {
5007
- res.writeHead(404);
5008
- res.end("Not found");
5009
- return;
5010
- }
5011
- res.setHeader("Access-Control-Allow-Origin", "*");
5012
- res.setHeader("Content-Type", reqPath.endsWith(".bundle") ? "application/octet-stream" : "application/javascript");
5013
- res.end(data);
5014
- });
5015
- });
5016
- const wss = new WebSocketServer({ noServer: true });
5017
- httpServer.on("upgrade", (request, socket, head) => {
5018
- const reqPath = (request.url || "").split("?")[0];
5019
- if (reqPath === `${basePath}/__hmr` || reqPath === "/__hmr" || reqPath.endsWith("/__hmr")) {
5020
- wss.handleUpgrade(request, socket, head, (ws) => wss.emit("connection", ws, request));
5021
- } else {
5022
- socket.destroy();
5425
+ if (input === "l") {
5426
+ setUi((s) => ({ ...s, showLogs: !s.showLogs }));
5023
5427
  }
5024
5428
  });
5025
- wss.on("connection", (ws, req) => {
5026
- const clientIp = req.socket.remoteAddress ?? "unknown";
5027
- console.log(`\x1B[90m[WS] client connected: ${clientIp}\x1B[0m`);
5028
- ws.send(JSON.stringify({ type: "connected" }));
5029
- ws.on("close", () => {
5030
- console.log(`\x1B[90m[WS] client disconnected: ${clientIp}\x1B[0m`);
5031
- });
5032
- ws.on("message", (data) => {
5429
+ useEffect3(() => {
5430
+ const onSig = () => {
5431
+ handleQuit();
5432
+ };
5433
+ process.on("SIGINT", onSig);
5434
+ process.on("SIGTERM", onSig);
5435
+ return () => {
5436
+ process.off("SIGINT", onSig);
5437
+ process.off("SIGTERM", onSig);
5438
+ };
5439
+ }, [handleQuit]);
5440
+ useEffect3(() => {
5441
+ let alive = true;
5442
+ let buildProcess = null;
5443
+ let watcher = null;
5444
+ let stopBonjour;
5445
+ const run = async () => {
5033
5446
  try {
5034
- const msg = JSON.parse(data.toString());
5035
- if (msg?.type === "console_log" && Array.isArray(msg.message)) {
5036
- const skip = msg.message.includes("[rspeedy-dev-server]") || msg.message.includes("[HMR]");
5037
- if (skip) return;
5038
- const isJs = msg.tag === "lynx-console" || msg.tag == null;
5039
- if (!verbose && !isJs) return;
5040
- const prefix = isJs ? "\x1B[36m[APP]:\x1B[0m" : "\x1B[33m[NATIVE]:\x1B[0m";
5041
- console.log(prefix, ...msg.message);
5042
- }
5043
- } catch {
5044
- }
5045
- });
5046
- });
5047
- function broadcastReload() {
5048
- wss.clients.forEach((client) => {
5049
- if (client.readyState === 1) client.send(JSON.stringify({ type: "reload" }));
5050
- });
5051
- }
5052
- let chokidar = null;
5053
- try {
5054
- chokidar = await import("chokidar");
5055
- } catch {
5056
- }
5057
- if (chokidar) {
5058
- const watchPaths = [
5059
- path21.join(lynxProjectDir, "src"),
5060
- path21.join(lynxProjectDir, "lynx.config.ts"),
5061
- path21.join(lynxProjectDir, "lynx.config.js")
5062
- ].filter((p) => fs20.existsSync(p));
5063
- if (watchPaths.length > 0) {
5064
- const watcher = chokidar.watch(watchPaths, { ignoreInitial: true });
5065
- watcher.on("change", async () => {
5066
- try {
5067
- await runBuild();
5068
- broadcastReload();
5069
- console.log("\u{1F504} Rebuilt, clients notified");
5070
- } catch (e) {
5071
- console.error("Build failed:", e.message);
5447
+ const resolved = resolveHostPaths();
5448
+ const { projectRoot, lynxProjectDir, lynxBundlePath, lynxBundleFile, config } = resolved;
5449
+ const distDir = path22.dirname(lynxBundlePath);
5450
+ const projectName = path22.basename(lynxProjectDir);
5451
+ const basePath = `/${projectName}`;
5452
+ setUi((s) => ({ ...s, projectName, lynxBundleFile }));
5453
+ const preferredPort = config.devServer?.port ?? config.devServer?.httpPort ?? DEFAULT_PORT;
5454
+ const port = await findAvailablePort(preferredPort);
5455
+ if (port !== preferredPort) {
5456
+ appendLog(`Port ${preferredPort} in use, using ${port}`);
5072
5457
  }
5073
- });
5074
- }
5075
- }
5076
- try {
5077
- await runBuild();
5078
- } catch (e) {
5079
- console.error("\u274C Initial build failed:", e.message);
5080
- process.exit(1);
5081
- }
5082
- let stopBonjour;
5083
- httpServer.listen(port, "0.0.0.0", () => {
5084
- void import("dnssd-advertise").then(({ advertise }) => {
5085
- stopBonjour = advertise({
5086
- name: projectName,
5087
- type: "tamer",
5088
- protocol: "tcp",
5089
- port,
5090
- txt: {
5091
- name: projectName.slice(0, 255),
5092
- path: basePath.slice(0, 255)
5458
+ const iconPaths = resolveIconPaths(projectRoot, config);
5459
+ let iconFilePath = null;
5460
+ if (iconPaths?.source && fs21.statSync(iconPaths.source).isFile()) {
5461
+ iconFilePath = iconPaths.source;
5462
+ } else if (iconPaths?.androidAdaptiveForeground && fs21.statSync(iconPaths.androidAdaptiveForeground).isFile()) {
5463
+ iconFilePath = iconPaths.androidAdaptiveForeground;
5464
+ } else if (iconPaths?.android) {
5465
+ const androidIcon = path22.join(iconPaths.android, "mipmap-xxxhdpi", "ic_launcher.png");
5466
+ if (fs21.existsSync(androidIcon)) iconFilePath = androidIcon;
5467
+ } else if (iconPaths?.ios) {
5468
+ const iosIcon = path22.join(iconPaths.ios, "Icon-1024.png");
5469
+ if (fs21.existsSync(iosIcon)) iconFilePath = iosIcon;
5093
5470
  }
5094
- });
5095
- }).catch(() => {
5096
- });
5097
- const lanIp = getLanIp();
5098
- const devUrl = `http://${lanIp}:${port}${basePath}`;
5099
- const wsUrl = `ws://${lanIp}:${port}${basePath}/__hmr`;
5100
- console.log(`
5101
- \u{1F680} Tamer4Lynx dev server (${projectName})`);
5102
- if (verbose) console.log(` Logs: \x1B[33mverbose\x1B[0m (native + JS)`);
5103
- console.log(` Bundle: ${devUrl}/${lynxBundleFile}`);
5104
- console.log(` Meta: ${devUrl}/meta.json`);
5105
- console.log(` HMR WS: ${wsUrl}`);
5106
- if (stopBonjour) console.log(` mDNS: _tamer._tcp (discoverable on LAN)`);
5107
- console.log(`
5108
- Scan QR or enter in app: ${devUrl}
5109
- `);
5110
- void import("qrcode-terminal").then((mod) => {
5111
- const qrcode = mod.default ?? mod;
5112
- qrcode.generate(devUrl, { small: true });
5113
- }).catch(() => {
5114
- });
5115
- if (process.stdin.isTTY) {
5116
- readline3.emitKeypressEvents(process.stdin);
5117
- process.stdin.setRawMode(true);
5118
- process.stdin.resume();
5119
- process.stdin.setEncoding("utf8");
5120
- const help = "\x1B[90m r: refresh c/Ctrl+L: clear Ctrl+C: exit\x1B[0m";
5121
- console.log(help);
5122
- process.stdin.on("keypress", (str, key) => {
5123
- if (key.ctrl && key.name === "c") {
5124
- void cleanup();
5125
- return;
5471
+ const iconExt = iconFilePath ? path22.extname(iconFilePath) || ".png" : "";
5472
+ const runBuild = () => {
5473
+ return new Promise((resolve, reject) => {
5474
+ const { cmd, args } = detectPackageManager(lynxProjectDir);
5475
+ buildProcess = spawn(cmd, args, {
5476
+ cwd: lynxProjectDir,
5477
+ stdio: "pipe",
5478
+ shell: process.platform === "win32"
5479
+ });
5480
+ let stderr = "";
5481
+ buildProcess.stdout?.on("data", (d) => {
5482
+ appendLog(d.toString());
5483
+ });
5484
+ buildProcess.stderr?.on("data", (d) => {
5485
+ const t = d.toString();
5486
+ stderr += t;
5487
+ appendLog(t);
5488
+ });
5489
+ buildProcess.on("close", (code) => {
5490
+ buildProcess = null;
5491
+ if (code === 0) resolve();
5492
+ else reject(new Error(stderr || `Build exited ${code}`));
5493
+ });
5494
+ });
5495
+ };
5496
+ const doBuild = async () => {
5497
+ setUi((s) => ({ ...s, buildPhase: "building", buildError: void 0 }));
5498
+ try {
5499
+ await runBuild();
5500
+ if (!alive) return;
5501
+ setUi((s) => ({ ...s, buildPhase: "success" }));
5502
+ } catch (e) {
5503
+ if (!alive) return;
5504
+ const msg = e.message;
5505
+ setUi((s) => ({ ...s, buildPhase: "error", buildError: msg }));
5506
+ throw e;
5507
+ }
5508
+ };
5509
+ const httpSrv = http.createServer((req, res) => {
5510
+ let reqPath = (req.url || "/").split("?")[0];
5511
+ if (reqPath === `${basePath}/status`) {
5512
+ res.setHeader("Content-Type", "text/plain");
5513
+ res.setHeader("Access-Control-Allow-Origin", "*");
5514
+ res.end("packager-status:running");
5515
+ return;
5516
+ }
5517
+ if (reqPath === `${basePath}/meta.json`) {
5518
+ const lanIp2 = getLanIp();
5519
+ const nativeModules = discoverNativeExtensions(projectRoot);
5520
+ const androidPackageName = config.android?.packageName?.trim();
5521
+ const iosBundleId = config.ios?.bundleId?.trim();
5522
+ const idParts = [androidPackageName?.toLowerCase(), iosBundleId?.toLowerCase()].filter(
5523
+ (x) => Boolean(x)
5524
+ );
5525
+ const meta = {
5526
+ name: projectName,
5527
+ slug: projectName,
5528
+ bundleUrl: `http://${lanIp2}:${port}${basePath}/${lynxBundleFile}`,
5529
+ bundleFile: lynxBundleFile,
5530
+ hostUri: `http://${lanIp2}:${port}${basePath}`,
5531
+ debuggerHost: `${lanIp2}:${port}`,
5532
+ developer: { tool: "tamer4lynx" },
5533
+ packagerStatus: "running",
5534
+ nativeModules: nativeModules.map((m) => ({
5535
+ packageName: m.packageName,
5536
+ moduleClassName: m.moduleClassName
5537
+ }))
5538
+ };
5539
+ if (androidPackageName) meta.androidPackageName = androidPackageName;
5540
+ if (iosBundleId) meta.iosBundleId = iosBundleId;
5541
+ if (idParts.length > 0) meta.tamerAppKey = idParts.join("|");
5542
+ const rawIcon = config.icon;
5543
+ if (rawIcon && typeof rawIcon === "object" && "source" in rawIcon && typeof rawIcon.source === "string") {
5544
+ meta.iconSource = rawIcon.source;
5545
+ } else if (typeof rawIcon === "string") {
5546
+ meta.iconSource = rawIcon;
5547
+ }
5548
+ if (iconFilePath) {
5549
+ meta.icon = `http://${lanIp2}:${port}${basePath}/icon${iconExt}`;
5550
+ }
5551
+ res.setHeader("Content-Type", "application/json");
5552
+ res.setHeader("Access-Control-Allow-Origin", "*");
5553
+ res.end(JSON.stringify(meta, null, 2));
5554
+ return;
5555
+ }
5556
+ if (iconFilePath && (reqPath === `${basePath}/icon` || reqPath === `${basePath}/icon${iconExt}`)) {
5557
+ fs21.readFile(iconFilePath, (err, data) => {
5558
+ if (err) {
5559
+ res.writeHead(404);
5560
+ res.end();
5561
+ return;
5562
+ }
5563
+ res.setHeader("Content-Type", STATIC_MIME[iconExt] ?? "image/png");
5564
+ res.setHeader("Access-Control-Allow-Origin", "*");
5565
+ res.end(data);
5566
+ });
5567
+ return;
5568
+ }
5569
+ const lynxStaticMounts = [
5570
+ { prefix: `${basePath}/src/assets/`, rootSub: "src/assets" },
5571
+ { prefix: `${basePath}/assets/`, rootSub: "assets" }
5572
+ ];
5573
+ for (const { prefix, rootSub } of lynxStaticMounts) {
5574
+ if (!reqPath.startsWith(prefix)) continue;
5575
+ let rel = reqPath.slice(prefix.length);
5576
+ try {
5577
+ rel = decodeURIComponent(rel);
5578
+ } catch {
5579
+ res.writeHead(400);
5580
+ res.end();
5581
+ return;
5582
+ }
5583
+ const safe = path22.normalize(rel).replace(/^(\.\.(\/|\\|$))+/, "");
5584
+ if (path22.isAbsolute(safe) || safe.startsWith("..")) {
5585
+ res.writeHead(403);
5586
+ res.end();
5587
+ return;
5588
+ }
5589
+ const allowedRoot = path22.resolve(lynxProjectDir, rootSub);
5590
+ const abs = path22.resolve(allowedRoot, safe);
5591
+ if (!abs.startsWith(allowedRoot + path22.sep) && abs !== allowedRoot) {
5592
+ res.writeHead(403);
5593
+ res.end();
5594
+ return;
5595
+ }
5596
+ if (!fs21.existsSync(abs) || !fs21.statSync(abs).isFile()) {
5597
+ res.writeHead(404);
5598
+ res.end("Not found");
5599
+ return;
5600
+ }
5601
+ sendFileFromDisk(res, abs);
5602
+ return;
5603
+ }
5604
+ if (reqPath === "/" || reqPath === basePath || reqPath === `${basePath}/`) {
5605
+ reqPath = `${basePath}/${lynxBundleFile}`;
5606
+ } else if (!reqPath.startsWith(basePath)) {
5607
+ reqPath = basePath + (reqPath.startsWith("/") ? reqPath : "/" + reqPath);
5608
+ }
5609
+ 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) {
5613
+ res.writeHead(403);
5614
+ res.end();
5615
+ return;
5616
+ }
5617
+ fs21.readFile(filePath, (err, data) => {
5618
+ if (err) {
5619
+ res.writeHead(404);
5620
+ res.end("Not found");
5621
+ return;
5622
+ }
5623
+ res.setHeader("Access-Control-Allow-Origin", "*");
5624
+ res.setHeader("Content-Type", reqPath.endsWith(".bundle") ? "application/octet-stream" : "application/javascript");
5625
+ res.end(data);
5626
+ });
5627
+ });
5628
+ const wssInst = new WebSocketServer({ noServer: true });
5629
+ rebuildRef.current = async () => {
5630
+ try {
5631
+ await doBuild();
5632
+ if (!alive) return;
5633
+ wssInst.clients.forEach((client) => {
5634
+ if (client.readyState === 1) client.send(JSON.stringify({ type: "reload" }));
5635
+ });
5636
+ appendLog("Rebuilt, clients notified");
5637
+ } catch {
5638
+ }
5639
+ };
5640
+ httpSrv.on("upgrade", (request, socket, head) => {
5641
+ const p = (request.url || "").split("?")[0];
5642
+ if (p === `${basePath}/__hmr` || p === "/__hmr" || p.endsWith("/__hmr")) {
5643
+ wssInst.handleUpgrade(request, socket, head, (ws) => wssInst.emit("connection", ws, request));
5644
+ } else {
5645
+ socket.destroy();
5646
+ }
5647
+ });
5648
+ wssInst.on("connection", (ws, req) => {
5649
+ const clientIp = req.socket.remoteAddress ?? "unknown";
5650
+ setUi((s) => ({ ...s, wsConnections: s.wsConnections + 1 }));
5651
+ appendLog(`[WS] connected: ${clientIp}`);
5652
+ ws.send(JSON.stringify({ type: "connected" }));
5653
+ ws.on("close", () => {
5654
+ setUi((s) => ({ ...s, wsConnections: Math.max(0, s.wsConnections - 1) }));
5655
+ appendLog(`[WS] disconnected: ${clientIp}`);
5656
+ });
5657
+ ws.on("message", (data) => {
5658
+ try {
5659
+ const msg = JSON.parse(data.toString());
5660
+ if (msg?.type === "console_log" && Array.isArray(msg.message)) {
5661
+ const skip = msg.message.includes("[rspeedy-dev-server]") || msg.message.includes("[HMR]");
5662
+ if (skip) return;
5663
+ const isJs = msg.tag === "lynx-console" || msg.tag == null;
5664
+ if (!verbose && !isJs) return;
5665
+ appendLog(`${isJs ? "[APP]" : "[NATIVE]"} ${msg.message.join(" ")}`);
5666
+ }
5667
+ } catch {
5668
+ }
5669
+ });
5670
+ });
5671
+ let chokidar = null;
5672
+ try {
5673
+ chokidar = await import("chokidar");
5674
+ } catch {
5126
5675
  }
5127
- switch (key.name) {
5128
- case "r":
5129
- runBuild().then(() => {
5130
- broadcastReload();
5131
- console.log("\u{1F504} Refreshed, clients notified");
5132
- }).catch((e) => console.error("Build failed:", e.message));
5133
- break;
5134
- case "c":
5135
- process.stdout.write("\x1B[2J\x1B[H");
5136
- break;
5137
- case "l":
5138
- if (key.ctrl) process.stdout.write("\x1B[2J\x1B[H");
5139
- break;
5140
- default:
5141
- break;
5676
+ if (chokidar) {
5677
+ 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));
5682
+ if (watchPaths.length > 0) {
5683
+ const w = chokidar.watch(watchPaths, { ignoreInitial: true });
5684
+ w.on("change", async () => {
5685
+ try {
5686
+ await rebuildRef.current();
5687
+ } catch {
5688
+ }
5689
+ });
5690
+ watcher = {
5691
+ close: async () => {
5692
+ await w.close();
5693
+ }
5694
+ };
5695
+ }
5142
5696
  }
5143
- });
5697
+ await doBuild();
5698
+ if (!alive) return;
5699
+ await new Promise((listenResolve, listenReject) => {
5700
+ httpSrv.listen(port, "0.0.0.0", () => {
5701
+ listenResolve();
5702
+ });
5703
+ httpSrv.once("error", (err) => listenReject(err));
5704
+ });
5705
+ if (!alive) return;
5706
+ void import("dnssd-advertise").then(({ advertise }) => {
5707
+ stopBonjour = advertise({
5708
+ name: projectName,
5709
+ type: "tamer",
5710
+ protocol: "tcp",
5711
+ port,
5712
+ txt: {
5713
+ name: projectName.slice(0, 255),
5714
+ path: basePath.slice(0, 255)
5715
+ }
5716
+ });
5717
+ setUi((s) => ({ ...s, bonjour: true }));
5718
+ }).catch(() => {
5719
+ });
5720
+ const lanIp = getLanIp();
5721
+ const devUrl = `http://${lanIp}:${port}${basePath}`;
5722
+ const wsUrl = `ws://${lanIp}:${port}${basePath}/__hmr`;
5723
+ setUi((s) => ({
5724
+ ...s,
5725
+ phase: "running",
5726
+ port,
5727
+ lanIp,
5728
+ devUrl,
5729
+ wsUrl
5730
+ }));
5731
+ void import("qrcode-terminal").then((mod) => {
5732
+ const qrcode = mod.default ?? mod;
5733
+ qrcode.generate(devUrl, { small: true }, (qr) => {
5734
+ if (!alive) return;
5735
+ setUi((s) => ({ ...s, qrLines: qr.split("\n").filter(Boolean) }));
5736
+ });
5737
+ }).catch(() => {
5738
+ });
5739
+ cleanupRef.current = async () => {
5740
+ buildProcess?.kill();
5741
+ await watcher?.close().catch(() => {
5742
+ });
5743
+ await stopBonjour?.();
5744
+ httpSrv.close();
5745
+ wssInst.close();
5746
+ };
5747
+ } catch (e) {
5748
+ if (!alive) return;
5749
+ setUi((s) => ({
5750
+ ...s,
5751
+ phase: "failed",
5752
+ startError: e.message
5753
+ }));
5754
+ }
5755
+ };
5756
+ void run();
5757
+ return () => {
5758
+ alive = false;
5759
+ void cleanupRef.current?.();
5760
+ };
5761
+ }, [appendLog, verbose]);
5762
+ return /* @__PURE__ */ jsx10(
5763
+ ServerDashboard,
5764
+ {
5765
+ projectName: ui.projectName,
5766
+ port: ui.port,
5767
+ lanIp: ui.lanIp,
5768
+ devUrl: ui.devUrl,
5769
+ wsUrl: ui.wsUrl,
5770
+ lynxBundleFile: ui.lynxBundleFile,
5771
+ bonjour: ui.bonjour,
5772
+ verbose: ui.verbose,
5773
+ buildPhase: ui.buildPhase,
5774
+ buildError: ui.buildError,
5775
+ wsConnections: ui.wsConnections,
5776
+ logLines: ui.logLines,
5777
+ showLogs: ui.showLogs,
5778
+ qrLines: ui.qrLines,
5779
+ phase: ui.phase,
5780
+ startError: ui.startError
5144
5781
  }
5782
+ );
5783
+ }
5784
+ async function startDevServer(opts) {
5785
+ const verbose = opts?.verbose ?? false;
5786
+ const { waitUntilExit } = render2(/* @__PURE__ */ jsx10(DevServerApp, { verbose }), {
5787
+ exitOnCtrlC: false,
5788
+ patchConsole: false
5145
5789
  });
5146
- const cleanup = async () => {
5147
- buildProcess?.kill();
5148
- await stopBonjour?.();
5149
- httpServer.close();
5150
- wss.close();
5151
- process.exit(0);
5152
- };
5153
- process.on("SIGINT", () => {
5154
- void cleanup();
5155
- });
5156
- process.on("SIGTERM", () => {
5157
- void cleanup();
5158
- });
5159
- await new Promise(() => {
5160
- });
5790
+ await waitUntilExit();
5161
5791
  }
5162
5792
  var devServer_default = startDevServer;
5163
5793
 
@@ -5168,10 +5798,10 @@ async function start(opts) {
5168
5798
  var start_default = start;
5169
5799
 
5170
5800
  // src/common/injectHost.ts
5171
- import fs21 from "fs";
5172
- import path22 from "path";
5801
+ import fs22 from "fs";
5802
+ import path23 from "path";
5173
5803
  function readAndSubstitute(templatePath, vars) {
5174
- const raw = fs21.readFileSync(templatePath, "utf-8");
5804
+ const raw = fs22.readFileSync(templatePath, "utf-8");
5175
5805
  return Object.entries(vars).reduce(
5176
5806
  (s, [k, v]) => s.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), v),
5177
5807
  raw
@@ -5192,32 +5822,32 @@ async function injectHostAndroid(opts) {
5192
5822
  process.exit(1);
5193
5823
  }
5194
5824
  const androidDir = config.paths?.androidDir ?? "android";
5195
- const rootDir = path22.join(projectRoot, androidDir);
5825
+ const rootDir = path23.join(projectRoot, androidDir);
5196
5826
  const packagePath = packageName.replace(/\./g, "/");
5197
- const javaDir = path22.join(rootDir, "app", "src", "main", "java", packagePath);
5198
- const kotlinDir = path22.join(rootDir, "app", "src", "main", "kotlin", packagePath);
5199
- if (!fs21.existsSync(javaDir) || !fs21.existsSync(kotlinDir)) {
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)) {
5200
5830
  console.error("\u274C Android project not found. Run `t4l android create` first or ensure android/ exists.");
5201
5831
  process.exit(1);
5202
5832
  }
5203
- const templateDir = path22.join(hostPkg, "android", "templates");
5833
+ const templateDir = path23.join(hostPkg, "android", "templates");
5204
5834
  const vars = { PACKAGE_NAME: packageName, APP_NAME: appName };
5205
5835
  const files = [
5206
- { src: "App.java", dst: path22.join(javaDir, "App.java") },
5207
- { src: "TemplateProvider.java", dst: path22.join(javaDir, "TemplateProvider.java") },
5208
- { src: "MainActivity.kt", dst: path22.join(kotlinDir, "MainActivity.kt") }
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") }
5209
5839
  ];
5210
5840
  for (const { src, dst } of files) {
5211
- const srcPath = path22.join(templateDir, src);
5212
- if (!fs21.existsSync(srcPath)) continue;
5213
- if (fs21.existsSync(dst) && !opts?.force) {
5214
- console.log(`\u23ED\uFE0F Skipping ${path22.basename(dst)} (use --force to overwrite)`);
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)`);
5215
5845
  continue;
5216
5846
  }
5217
5847
  const content = readAndSubstitute(srcPath, vars);
5218
- fs21.mkdirSync(path22.dirname(dst), { recursive: true });
5219
- fs21.writeFileSync(dst, content);
5220
- console.log(`\u2705 Injected ${path22.basename(dst)}`);
5848
+ fs22.mkdirSync(path23.dirname(dst), { recursive: true });
5849
+ fs22.writeFileSync(dst, content);
5850
+ console.log(`\u2705 Injected ${path23.basename(dst)}`);
5221
5851
  }
5222
5852
  }
5223
5853
  async function injectHostIos(opts) {
@@ -5235,13 +5865,13 @@ async function injectHostIos(opts) {
5235
5865
  process.exit(1);
5236
5866
  }
5237
5867
  const iosDir = config.paths?.iosDir ?? "ios";
5238
- const rootDir = path22.join(projectRoot, iosDir);
5239
- const projectDir = path22.join(rootDir, appName);
5240
- if (!fs21.existsSync(projectDir)) {
5868
+ const rootDir = path23.join(projectRoot, iosDir);
5869
+ const projectDir = path23.join(rootDir, appName);
5870
+ if (!fs22.existsSync(projectDir)) {
5241
5871
  console.error("\u274C iOS project not found. Run `t4l ios create` first or ensure ios/ exists.");
5242
5872
  process.exit(1);
5243
5873
  }
5244
- const templateDir = path22.join(hostPkg, "ios", "templates");
5874
+ const templateDir = path23.join(hostPkg, "ios", "templates");
5245
5875
  const vars = { PACKAGE_NAME: bundleId, APP_NAME: appName, BUNDLE_ID: bundleId };
5246
5876
  const files = [
5247
5877
  "AppDelegate.swift",
@@ -5251,22 +5881,22 @@ async function injectHostIos(opts) {
5251
5881
  "LynxInitProcessor.swift"
5252
5882
  ];
5253
5883
  for (const f of files) {
5254
- const srcPath = path22.join(templateDir, f);
5255
- const dstPath = path22.join(projectDir, f);
5256
- if (!fs21.existsSync(srcPath)) continue;
5257
- if (fs21.existsSync(dstPath) && !opts?.force) {
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) {
5258
5888
  console.log(`\u23ED\uFE0F Skipping ${f} (use --force to overwrite)`);
5259
5889
  continue;
5260
5890
  }
5261
5891
  const content = readAndSubstitute(srcPath, vars);
5262
- fs21.writeFileSync(dstPath, content);
5892
+ fs22.writeFileSync(dstPath, content);
5263
5893
  console.log(`\u2705 Injected ${f}`);
5264
5894
  }
5265
5895
  }
5266
5896
 
5267
5897
  // src/common/buildEmbeddable.ts
5268
- import fs22 from "fs";
5269
- import path23 from "path";
5898
+ import fs23 from "fs";
5899
+ import path24 from "path";
5270
5900
  import { execSync as execSync9 } from "child_process";
5271
5901
  var EMBEDDABLE_DIR = "embeddable";
5272
5902
  var LIB_PACKAGE = "com.tamer.embeddable";
@@ -5343,14 +5973,14 @@ object LynxEmbeddable {
5343
5973
  }
5344
5974
  `;
5345
5975
  function generateAndroidLibrary(outDir, androidDir, projectRoot, lynxBundlePath, lynxBundleFile, modules, abiFilters) {
5346
- const libDir = path23.join(androidDir, "lib");
5347
- const libSrcMain = path23.join(libDir, "src", "main");
5348
- const assetsDir = path23.join(libSrcMain, "assets");
5349
- const kotlinDir = path23.join(libSrcMain, "kotlin", LIB_PACKAGE.replace(/\./g, "/"));
5350
- const generatedDir = path23.join(kotlinDir, "generated");
5351
- fs22.mkdirSync(path23.join(androidDir, "gradle"), { recursive: true });
5352
- fs22.mkdirSync(generatedDir, { recursive: true });
5353
- fs22.mkdirSync(assetsDir, { recursive: true });
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 });
5354
5984
  const androidModules = modules.filter((m) => m.config.android);
5355
5985
  const abiList = abiFilters.map((a) => `"${a}"`).join(", ");
5356
5986
  const settingsContent = `pluginManagement {
@@ -5370,7 +6000,7 @@ include(":lib")
5370
6000
  ${androidModules.map((p) => {
5371
6001
  const gradleName = p.name.replace(/^@/, "").replace(/\//g, "_");
5372
6002
  const sourceDir = p.config.android?.sourceDir || "android";
5373
- const absPath = path23.join(p.packagePath, sourceDir).replace(/\\/g, "/");
6003
+ const absPath = path24.join(p.packagePath, sourceDir).replace(/\\/g, "/");
5374
6004
  return `include(":${gradleName}")
5375
6005
  project(":${gradleName}").projectDir = file("${absPath}")`;
5376
6006
  }).join("\n")}
@@ -5419,10 +6049,10 @@ dependencies {
5419
6049
  ${libDeps}
5420
6050
  }
5421
6051
  `;
5422
- fs22.writeFileSync(path23.join(androidDir, "gradle", "libs.versions.toml"), LIBS_VERSIONS_TOML);
5423
- fs22.writeFileSync(path23.join(androidDir, "settings.gradle.kts"), settingsContent);
5424
- fs22.writeFileSync(
5425
- path23.join(androidDir, "build.gradle.kts"),
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"),
5426
6056
  `plugins {
5427
6057
  alias(libs.plugins.android.library) apply false
5428
6058
  alias(libs.plugins.kotlin.android) apply false
@@ -5430,26 +6060,26 @@ ${libDeps}
5430
6060
  }
5431
6061
  `
5432
6062
  );
5433
- fs22.writeFileSync(
5434
- path23.join(androidDir, "gradle.properties"),
6063
+ fs23.writeFileSync(
6064
+ path24.join(androidDir, "gradle.properties"),
5435
6065
  `org.gradle.jvmargs=-Xmx2048m
5436
6066
  android.useAndroidX=true
5437
6067
  kotlin.code.style=official
5438
6068
  `
5439
6069
  );
5440
- fs22.writeFileSync(path23.join(libDir, "build.gradle.kts"), libBuildContent);
5441
- fs22.writeFileSync(
5442
- path23.join(libSrcMain, "AndroidManifest.xml"),
6070
+ fs23.writeFileSync(path24.join(libDir, "build.gradle.kts"), libBuildContent);
6071
+ fs23.writeFileSync(
6072
+ path24.join(libSrcMain, "AndroidManifest.xml"),
5443
6073
  '<?xml version="1.0" encoding="utf-8"?>\n<manifest />'
5444
6074
  );
5445
- fs22.copyFileSync(lynxBundlePath, path23.join(assetsDir, lynxBundleFile));
5446
- fs22.writeFileSync(path23.join(kotlinDir, "LynxEmbeddable.kt"), LYNX_EMBEDDABLE_KT);
5447
- fs22.writeFileSync(
5448
- path23.join(generatedDir, "GeneratedLynxExtensions.kt"),
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"),
5449
6079
  generateLynxExtensionsKotlin(modules, LIB_PACKAGE)
5450
6080
  );
5451
- fs22.writeFileSync(
5452
- path23.join(generatedDir, "GeneratedActivityLifecycle.kt"),
6081
+ fs23.writeFileSync(
6082
+ path24.join(generatedDir, "GeneratedActivityLifecycle.kt"),
5453
6083
  generateActivityLifecycleKotlin(modules, LIB_PACKAGE)
5454
6084
  );
5455
6085
  }
@@ -5458,20 +6088,20 @@ async function buildEmbeddable(opts = {}) {
5458
6088
  const { lynxProjectDir, lynxBundlePath, lynxBundleFile, projectRoot, config } = resolved;
5459
6089
  console.log("\u{1F4E6} Building Lynx project (release)...");
5460
6090
  execSync9("npm run build", { stdio: "inherit", cwd: lynxProjectDir });
5461
- if (!fs22.existsSync(lynxBundlePath)) {
6091
+ if (!fs23.existsSync(lynxBundlePath)) {
5462
6092
  console.error(`\u274C Bundle not found at ${lynxBundlePath}`);
5463
6093
  process.exit(1);
5464
6094
  }
5465
- const outDir = path23.join(projectRoot, EMBEDDABLE_DIR);
5466
- fs22.mkdirSync(outDir, { recursive: true });
5467
- const distDir = path23.dirname(lynxBundlePath);
6095
+ const outDir = path24.join(projectRoot, EMBEDDABLE_DIR);
6096
+ fs23.mkdirSync(outDir, { recursive: true });
6097
+ const distDir = path24.dirname(lynxBundlePath);
5468
6098
  copyDistAssets(distDir, outDir, lynxBundleFile);
5469
6099
  const modules = discoverModules(projectRoot);
5470
6100
  const androidModules = modules.filter((m) => m.config.android);
5471
6101
  const abiFilters = resolveAbiFilters(config);
5472
- const androidDir = path23.join(outDir, "android");
5473
- if (fs22.existsSync(androidDir)) fs22.rmSync(androidDir, { recursive: true });
5474
- fs22.mkdirSync(androidDir, { recursive: true });
6102
+ const androidDir = path24.join(outDir, "android");
6103
+ if (fs23.existsSync(androidDir)) fs23.rmSync(androidDir, { recursive: true });
6104
+ fs23.mkdirSync(androidDir, { recursive: true });
5475
6105
  generateAndroidLibrary(
5476
6106
  outDir,
5477
6107
  androidDir,
@@ -5481,23 +6111,23 @@ async function buildEmbeddable(opts = {}) {
5481
6111
  modules,
5482
6112
  abiFilters
5483
6113
  );
5484
- const gradlewPath = path23.join(androidDir, "gradlew");
6114
+ const gradlewPath = path24.join(androidDir, "gradlew");
5485
6115
  const devAppDir = findDevAppPackage(projectRoot);
5486
6116
  const existingGradleDirs = [
5487
- path23.join(projectRoot, "android"),
5488
- devAppDir ? path23.join(devAppDir, "android") : null
6117
+ path24.join(projectRoot, "android"),
6118
+ devAppDir ? path24.join(devAppDir, "android") : null
5489
6119
  ].filter(Boolean);
5490
6120
  let hasWrapper = false;
5491
6121
  for (const d of existingGradleDirs) {
5492
- if (fs22.existsSync(path23.join(d, "gradlew"))) {
6122
+ if (fs23.existsSync(path24.join(d, "gradlew"))) {
5493
6123
  for (const name of ["gradlew", "gradlew.bat", "gradle"]) {
5494
- const src = path23.join(d, name);
5495
- if (fs22.existsSync(src)) {
5496
- const dest = path23.join(androidDir, name);
5497
- if (fs22.statSync(src).isDirectory()) {
5498
- fs22.cpSync(src, dest, { recursive: true });
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 });
5499
6129
  } else {
5500
- fs22.copyFileSync(src, dest);
6130
+ fs23.copyFileSync(src, dest);
5501
6131
  }
5502
6132
  }
5503
6133
  }
@@ -5516,10 +6146,10 @@ async function buildEmbeddable(opts = {}) {
5516
6146
  console.error("\u274C Android AAR build failed. Run manually: cd embeddable/android && ./gradlew :lib:assembleRelease");
5517
6147
  throw e;
5518
6148
  }
5519
- const aarSrc = path23.join(androidDir, "lib", "build", "outputs", "aar", "lib-release.aar");
5520
- const aarDest = path23.join(outDir, "tamer-embeddable.aar");
5521
- if (fs22.existsSync(aarSrc)) {
5522
- fs22.copyFileSync(aarSrc, aarDest);
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);
5523
6153
  console.log(` - tamer-embeddable.aar`);
5524
6154
  }
5525
6155
  const snippetAndroid = `// Add to your app's build.gradle:
@@ -5530,7 +6160,7 @@ async function buildEmbeddable(opts = {}) {
5530
6160
  // LynxEmbeddable.init(applicationContext)
5531
6161
  // val lynxView = LynxEmbeddable.buildLynxView(containerViewGroup)
5532
6162
  `;
5533
- fs22.writeFileSync(path23.join(outDir, "snippet-android.kt"), snippetAndroid);
6163
+ fs23.writeFileSync(path24.join(outDir, "snippet-android.kt"), snippetAndroid);
5534
6164
  generateIosPod(outDir, projectRoot, lynxBundlePath, lynxBundleFile, modules);
5535
6165
  const readme = `# Embeddable Lynx Bundle
5536
6166
 
@@ -5561,7 +6191,7 @@ Add the \`Podfile.snippet\` entries to your Podfile (inside your app target), th
5561
6191
 
5562
6192
  - [Embedding LynxView](https://lynxjs.org/guide/embed-lynx-to-native)
5563
6193
  `;
5564
- fs22.writeFileSync(path23.join(outDir, "README.md"), readme);
6194
+ fs23.writeFileSync(path24.join(outDir, "README.md"), readme);
5565
6195
  console.log(`
5566
6196
  \u2705 Embeddable output at ${outDir}/`);
5567
6197
  console.log(" - main.lynx.bundle");
@@ -5573,20 +6203,20 @@ Add the \`Podfile.snippet\` entries to your Podfile (inside your app target), th
5573
6203
  console.log(" - README.md");
5574
6204
  }
5575
6205
  function generateIosPod(outDir, projectRoot, lynxBundlePath, lynxBundleFile, modules) {
5576
- const iosDir = path23.join(outDir, "ios");
5577
- const podDir = path23.join(iosDir, "TamerEmbeddable");
5578
- const resourcesDir = path23.join(podDir, "Resources");
5579
- fs22.mkdirSync(resourcesDir, { recursive: true });
5580
- fs22.copyFileSync(lynxBundlePath, path23.join(resourcesDir, lynxBundleFile));
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));
5581
6211
  const iosModules = modules.filter((m) => m.config.ios);
5582
6212
  const podDeps = iosModules.map((p) => {
5583
6213
  const podspecPath = p.config.ios?.podspecPath || ".";
5584
- const podspecDir = path23.join(p.packagePath, podspecPath);
5585
- if (!fs22.existsSync(podspecDir)) return null;
5586
- const files = fs22.readdirSync(podspecDir);
6214
+ const podspecDir = path24.join(p.packagePath, podspecPath);
6215
+ if (!fs23.existsSync(podspecDir)) return null;
6216
+ const files = fs23.readdirSync(podspecDir);
5587
6217
  const podspecFile = files.find((f) => f.endsWith(".podspec"));
5588
6218
  const podName = podspecFile ? podspecFile.replace(".podspec", "") : p.name.split("/").pop().replace(/-/g, "");
5589
- const absPath = path23.resolve(podspecDir);
6219
+ const absPath = path24.resolve(podspecDir);
5590
6220
  return { podName, absPath };
5591
6221
  }).filter(Boolean);
5592
6222
  const podDepLines = podDeps.map((d) => ` s.dependency '${d.podName}'`).join("\n");
@@ -5626,9 +6256,9 @@ end
5626
6256
  });
5627
6257
  const swiftImports = iosModules.map((p) => {
5628
6258
  const podspecPath = p.config.ios?.podspecPath || ".";
5629
- const podspecDir = path23.join(p.packagePath, podspecPath);
5630
- if (!fs22.existsSync(podspecDir)) return null;
5631
- const files = fs22.readdirSync(podspecDir);
6259
+ const podspecDir = path24.join(p.packagePath, podspecPath);
6260
+ if (!fs23.existsSync(podspecDir)) return null;
6261
+ const files = fs23.readdirSync(podspecDir);
5632
6262
  const podspecFile = files.find((f) => f.endsWith(".podspec"));
5633
6263
  return podspecFile ? podspecFile.replace(".podspec", "") : null;
5634
6264
  }).filter(Boolean);
@@ -5647,17 +6277,17 @@ ${regBlock}
5647
6277
  }
5648
6278
  }
5649
6279
  `;
5650
- fs22.writeFileSync(path23.join(iosDir, "TamerEmbeddable.podspec"), podspecContent);
5651
- fs22.writeFileSync(path23.join(podDir, "LynxEmbeddable.swift"), lynxEmbeddableSwift);
5652
- const absIosDir = path23.resolve(iosDir);
6280
+ fs23.writeFileSync(path24.join(iosDir, "TamerEmbeddable.podspec"), podspecContent);
6281
+ fs23.writeFileSync(path24.join(podDir, "LynxEmbeddable.swift"), lynxEmbeddableSwift);
6282
+ const absIosDir = path24.resolve(iosDir);
5653
6283
  const podfileSnippet = `# Paste into your app target in Podfile:
5654
6284
 
5655
6285
  pod 'TamerEmbeddable', :path => '${absIosDir}'
5656
6286
  ${podDeps.map((d) => `pod '${d.podName}', :path => '${d.absPath}'`).join("\n")}
5657
6287
  `;
5658
- fs22.writeFileSync(path23.join(iosDir, "Podfile.snippet"), podfileSnippet);
5659
- fs22.writeFileSync(
5660
- path23.join(outDir, "snippet-ios.swift"),
6288
+ fs23.writeFileSync(path24.join(iosDir, "Podfile.snippet"), podfileSnippet);
6289
+ fs23.writeFileSync(
6290
+ path24.join(outDir, "snippet-ios.swift"),
5661
6291
  `// Add LynxEmbeddable.initEnvironment() in your AppDelegate/SceneDelegate before presenting LynxView.
5662
6292
  // Then create LynxView with your bundle URL (main.lynx.bundle is in the pod resources).
5663
6293
  `
@@ -5665,8 +6295,8 @@ ${podDeps.map((d) => `pod '${d.podName}', :path => '${d.absPath}'`).join("\n")}
5665
6295
  }
5666
6296
 
5667
6297
  // src/common/add.ts
5668
- import fs23 from "fs";
5669
- import path24 from "path";
6298
+ import fs24 from "fs";
6299
+ import path25 from "path";
5670
6300
  import { execFile, execSync as execSync10 } from "child_process";
5671
6301
  import { promisify } from "util";
5672
6302
  import semver from "semver";
@@ -5680,6 +6310,23 @@ var CORE_PACKAGES = [
5680
6310
  "@tamer4lynx/tamer-system-ui",
5681
6311
  "@tamer4lynx/tamer-icons"
5682
6312
  ];
6313
+ var DEV_STACK_PACKAGES = [
6314
+ "@tamer4lynx/jiggle",
6315
+ "@tamer4lynx/tamer-app-shell",
6316
+ "@tamer4lynx/tamer-biometric",
6317
+ "@tamer4lynx/tamer-dev-app",
6318
+ "@tamer4lynx/tamer-dev-client",
6319
+ "@tamer4lynx/tamer-display-browser",
6320
+ "@tamer4lynx/tamer-icons",
6321
+ "@tamer4lynx/tamer-insets",
6322
+ "@tamer4lynx/tamer-linking",
6323
+ "@tamer4lynx/tamer-plugin",
6324
+ "@tamer4lynx/tamer-router",
6325
+ "@tamer4lynx/tamer-screen",
6326
+ "@tamer4lynx/tamer-secure-store",
6327
+ "@tamer4lynx/tamer-system-ui",
6328
+ "@tamer4lynx/tamer-transports"
6329
+ ];
5683
6330
  var PACKAGE_ALIASES = {};
5684
6331
  async function getHighestPublishedVersion(fullName) {
5685
6332
  try {
@@ -5705,10 +6352,10 @@ async function normalizeTamerInstallSpec(pkg) {
5705
6352
  console.warn(`\u26A0\uFE0F Could not resolve published versions for ${pkg}; using @prerelease`);
5706
6353
  return `${pkg}@prerelease`;
5707
6354
  }
5708
- function detectPackageManager(cwd) {
5709
- const dir = path24.resolve(cwd);
5710
- if (fs23.existsSync(path24.join(dir, "pnpm-lock.yaml"))) return "pnpm";
5711
- if (fs23.existsSync(path24.join(dir, "bun.lockb"))) return "bun";
6355
+ 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";
5712
6359
  return "npm";
5713
6360
  }
5714
6361
  function runInstall(cwd, packages, pm) {
@@ -5718,13 +6365,22 @@ function runInstall(cwd, packages, pm) {
5718
6365
  }
5719
6366
  async function addCore() {
5720
6367
  const { lynxProjectDir } = resolveHostPaths();
5721
- const pm = detectPackageManager(lynxProjectDir);
6368
+ const pm = detectPackageManager2(lynxProjectDir);
5722
6369
  console.log(`Resolving latest published versions (npm)\u2026`);
5723
6370
  const resolved = await Promise.all(CORE_PACKAGES.map(normalizeTamerInstallSpec));
5724
6371
  console.log(`Adding core packages to ${lynxProjectDir} (using ${pm})\u2026`);
5725
6372
  runInstall(lynxProjectDir, resolved, pm);
5726
6373
  console.log("\u2705 Core packages installed. Run `t4l link` to link native modules.");
5727
6374
  }
6375
+ async function addDev() {
6376
+ const { lynxProjectDir } = resolveHostPaths();
6377
+ const pm = detectPackageManager2(lynxProjectDir);
6378
+ console.log(`Resolving latest published versions (npm)\u2026`);
6379
+ const resolved = await Promise.all([...DEV_STACK_PACKAGES].map(normalizeTamerInstallSpec));
6380
+ console.log(`Adding dev stack (${DEV_STACK_PACKAGES.length} @tamer4lynx packages) to ${lynxProjectDir} (using ${pm})\u2026`);
6381
+ runInstall(lynxProjectDir, resolved, pm);
6382
+ console.log("\u2705 Dev stack installed. Run `t4l link` to link native modules.");
6383
+ }
5728
6384
  async function add(packages = []) {
5729
6385
  const list = Array.isArray(packages) ? packages : [];
5730
6386
  if (list.length === 0) {
@@ -5735,7 +6391,7 @@ async function add(packages = []) {
5735
6391
  return;
5736
6392
  }
5737
6393
  const { lynxProjectDir } = resolveHostPaths();
5738
- const pm = detectPackageManager(lynxProjectDir);
6394
+ const pm = detectPackageManager2(lynxProjectDir);
5739
6395
  console.log(`Resolving latest published versions (npm)\u2026`);
5740
6396
  const normalized = await Promise.all(
5741
6397
  list.map(async (p) => {
@@ -5748,18 +6404,682 @@ async function add(packages = []) {
5748
6404
  console.log("\u2705 Packages installed. Run `t4l link` to link native modules.");
5749
6405
  }
5750
6406
 
6407
+ // src/common/signing.tsx
6408
+ import { useState as useState6, useEffect as useEffect4, useRef as useRef2 } from "react";
6409
+ import { render as render3, Text as Text10, Box as Box9 } from "ink";
6410
+ import fs27 from "fs";
6411
+ import path28 from "path";
6412
+
6413
+ // src/common/androidKeystore.ts
6414
+ import { execFileSync } from "child_process";
6415
+ import fs25 from "fs";
6416
+ import path26 from "path";
6417
+ function normalizeJavaHome(raw) {
6418
+ if (!raw) return void 0;
6419
+ const t = raw.trim().replace(/^["']|["']$/g, "");
6420
+ return t || void 0;
6421
+ }
6422
+ function discoverJavaHomeMacOs() {
6423
+ if (process.platform !== "darwin") return void 0;
6424
+ try {
6425
+ const out = execFileSync("/usr/libexec/java_home", [], {
6426
+ encoding: "utf8",
6427
+ stdio: ["pipe", "pipe", "pipe"]
6428
+ }).trim().split("\n")[0]?.trim();
6429
+ if (out && fs25.existsSync(path26.join(out, "bin", "keytool"))) return out;
6430
+ } catch {
6431
+ }
6432
+ return void 0;
6433
+ }
6434
+ function resolveKeytoolPath() {
6435
+ const jh = normalizeJavaHome(process.env.JAVA_HOME);
6436
+ const win = process.platform === "win32";
6437
+ const keytoolName = win ? "keytool.exe" : "keytool";
6438
+ if (jh) {
6439
+ const p = path26.join(jh, "bin", keytoolName);
6440
+ if (fs25.existsSync(p)) return p;
6441
+ }
6442
+ const mac = discoverJavaHomeMacOs();
6443
+ if (mac) {
6444
+ const p = path26.join(mac, "bin", keytoolName);
6445
+ if (fs25.existsSync(p)) return p;
6446
+ }
6447
+ return "keytool";
6448
+ }
6449
+ function keytoolAvailable() {
6450
+ const tryRun = (cmd) => {
6451
+ try {
6452
+ execFileSync(cmd, ["-help"], { stdio: "pipe" });
6453
+ return true;
6454
+ } catch {
6455
+ return false;
6456
+ }
6457
+ };
6458
+ if (tryRun("keytool")) return true;
6459
+ const fromJavaHome = resolveKeytoolPath();
6460
+ if (fromJavaHome !== "keytool" && fs25.existsSync(fromJavaHome)) {
6461
+ return tryRun(fromJavaHome);
6462
+ }
6463
+ return false;
6464
+ }
6465
+ function generateReleaseKeystore(opts) {
6466
+ const keytool = resolveKeytoolPath();
6467
+ const dir = path26.dirname(opts.keystoreAbsPath);
6468
+ fs25.mkdirSync(dir, { recursive: true });
6469
+ if (fs25.existsSync(opts.keystoreAbsPath)) {
6470
+ throw new Error(`Keystore already exists: ${opts.keystoreAbsPath}`);
6471
+ }
6472
+ if (!opts.storePassword || !opts.keyPassword) {
6473
+ throw new Error(
6474
+ "JDK keytool requires a keystore and key password of at least 6 characters for -genkeypair. Enter a password or use an existing keystore."
6475
+ );
6476
+ }
6477
+ const args = [
6478
+ "-genkeypair",
6479
+ "-v",
6480
+ "-keystore",
6481
+ opts.keystoreAbsPath,
6482
+ "-alias",
6483
+ opts.alias,
6484
+ "-keyalg",
6485
+ "RSA",
6486
+ "-keysize",
6487
+ "2048",
6488
+ "-validity",
6489
+ "10000",
6490
+ "-storepass",
6491
+ opts.storePassword,
6492
+ "-keypass",
6493
+ opts.keyPassword,
6494
+ "-dname",
6495
+ opts.dname
6496
+ ];
6497
+ try {
6498
+ execFileSync(keytool, args, { stdio: ["pipe", "pipe", "pipe"] });
6499
+ } catch (e) {
6500
+ const err = e;
6501
+ const fromKeytool = [err.stdout, err.stderr].filter(Boolean).map((b) => Buffer.from(b).toString("utf8")).join("\n").trim();
6502
+ throw new Error(fromKeytool || err.message || "keytool failed");
6503
+ }
6504
+ }
6505
+
6506
+ // src/common/appendEnvFile.ts
6507
+ import fs26 from "fs";
6508
+ import path27 from "path";
6509
+ import { parse } from "dotenv";
6510
+ function keysDefinedInFile(filePath) {
6511
+ if (!fs26.existsSync(filePath)) return /* @__PURE__ */ new Set();
6512
+ try {
6513
+ return new Set(Object.keys(parse(fs26.readFileSync(filePath, "utf8"))));
6514
+ } catch {
6515
+ return /* @__PURE__ */ new Set();
6516
+ }
6517
+ }
6518
+ function formatEnvLine(key, value) {
6519
+ if (/[\r\n]/.test(value) || /^\s|\s$/.test(value) || /[#"'\\=]/.test(value)) {
6520
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
6521
+ return `${key}="${escaped}"`;
6522
+ }
6523
+ return `${key}=${value}`;
6524
+ }
6525
+ function appendEnvVarsIfMissing(projectRoot, vars) {
6526
+ const entries = Object.entries(vars).filter(([, v]) => v !== void 0 && v !== "");
6527
+ if (entries.length === 0) return null;
6528
+ const envLocal = path27.join(projectRoot, ".env.local");
6529
+ const envDefault = path27.join(projectRoot, ".env");
6530
+ let target;
6531
+ if (fs26.existsSync(envLocal)) target = envLocal;
6532
+ else if (fs26.existsSync(envDefault)) target = envDefault;
6533
+ else target = envLocal;
6534
+ const existing = keysDefinedInFile(target);
6535
+ const lines = [];
6536
+ const appendedKeys = [];
6537
+ for (const [k, v] of entries) {
6538
+ if (existing.has(k)) continue;
6539
+ lines.push(formatEnvLine(k, v));
6540
+ appendedKeys.push(k);
6541
+ }
6542
+ if (lines.length === 0) {
6543
+ return {
6544
+ file: path27.basename(target),
6545
+ keys: [],
6546
+ skippedAll: entries.length > 0
6547
+ };
6548
+ }
6549
+ let prefix = "";
6550
+ if (fs26.existsSync(target)) {
6551
+ const cur = fs26.readFileSync(target, "utf8");
6552
+ prefix = cur.length === 0 ? "" : cur.endsWith("\n") ? cur : `${cur}
6553
+ `;
6554
+ }
6555
+ const block = lines.join("\n") + "\n";
6556
+ fs26.writeFileSync(target, prefix + block, "utf8");
6557
+ return { file: path27.basename(target), keys: appendedKeys };
6558
+ }
6559
+
6560
+ // src/common/signing.tsx
6561
+ import { Fragment, jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
6562
+ function AndroidKeystoreModeSelect({
6563
+ onSelect
6564
+ }) {
6565
+ const canGen = keytoolAvailable();
6566
+ const items = canGen ? [
6567
+ { label: "Generate a new release keystore (JDK keytool)", value: "generate" },
6568
+ { label: "Use an existing keystore file", value: "existing" }
6569
+ ] : [
6570
+ {
6571
+ label: "Use an existing keystore file (install a JDK for keytool to generate)",
6572
+ value: "existing"
6573
+ }
6574
+ ];
6575
+ return /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", children: [
6576
+ /* @__PURE__ */ jsx11(
6577
+ TuiSelectInput,
6578
+ {
6579
+ label: "Android release keystore:",
6580
+ items,
6581
+ onSelect
6582
+ }
6583
+ ),
6584
+ !canGen && /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: "keytool not found on PATH / JAVA_HOME. Install a JDK or set JAVA_HOME, then run signing again to generate." })
6585
+ ] });
6586
+ }
6587
+ function firstStepForPlatform(p) {
6588
+ if (p === "ios") return "ios-team";
6589
+ if (p === "android" || p === "both") return "android-keystore-mode";
6590
+ return "platform";
6591
+ }
6592
+ function SigningWizard({ platform: initialPlatform }) {
6593
+ const [state, setState] = useState6({
6594
+ platform: initialPlatform || null,
6595
+ android: {
6596
+ keystoreFile: "",
6597
+ keyAlias: "release",
6598
+ storePasswordEnv: "ANDROID_KEYSTORE_PASSWORD",
6599
+ keyPasswordEnv: "ANDROID_KEY_PASSWORD",
6600
+ keystoreMode: null,
6601
+ genKeystorePath: "android/release.keystore",
6602
+ genPassword: ""
6603
+ },
6604
+ ios: {
6605
+ developmentTeam: "",
6606
+ codeSignIdentity: "",
6607
+ provisioningProfileSpecifier: ""
6608
+ },
6609
+ step: initialPlatform ? firstStepForPlatform(initialPlatform) : "platform",
6610
+ generateError: null,
6611
+ androidEnvAppend: null
6612
+ });
6613
+ const nextStep = () => {
6614
+ setState((s) => {
6615
+ if (s.step === "android-gen-path") {
6616
+ return { ...s, step: "android-gen-alias" };
6617
+ }
6618
+ if (s.step === "android-gen-alias") {
6619
+ return { ...s, step: "android-gen-password" };
6620
+ }
6621
+ if (s.step === "android-keystore") {
6622
+ return { ...s, step: "android-alias" };
6623
+ }
6624
+ if (s.step === "android-alias") {
6625
+ return { ...s, step: "android-password-env" };
6626
+ }
6627
+ if (s.step === "android-password-env") {
6628
+ return { ...s, step: "android-key-password-env" };
6629
+ }
6630
+ if (s.step === "android-key-password-env") {
6631
+ if (s.platform === "both") {
6632
+ return { ...s, step: "ios-team" };
6633
+ }
6634
+ return { ...s, step: "saving" };
6635
+ }
6636
+ if (s.step === "ios-team") {
6637
+ return { ...s, step: "ios-identity" };
6638
+ }
6639
+ if (s.step === "ios-identity") {
6640
+ return { ...s, step: "ios-profile" };
6641
+ }
6642
+ if (s.step === "ios-profile") {
6643
+ return { ...s, step: "saving" };
6644
+ }
6645
+ return s;
6646
+ });
6647
+ };
6648
+ useEffect4(() => {
6649
+ if (state.step === "saving") {
6650
+ saveConfig();
6651
+ }
6652
+ }, [state.step]);
6653
+ const generateRunId = useRef2(0);
6654
+ useEffect4(() => {
6655
+ if (state.step !== "android-generating") return;
6656
+ const runId = ++generateRunId.current;
6657
+ let cancelled = false;
6658
+ const run = () => {
6659
+ let abs = "";
6660
+ try {
6661
+ const resolved = resolveHostPaths();
6662
+ const rel = state.android.genKeystorePath.trim() || "android/release.keystore";
6663
+ abs = path28.isAbsolute(rel) ? rel : path28.join(resolved.projectRoot, rel);
6664
+ const alias = state.android.keyAlias.trim() || "release";
6665
+ const pw = state.android.genPassword;
6666
+ const pkg = resolved.config.android?.packageName ?? "com.example.app";
6667
+ const safeOU = pkg.replace(/[,=+]/g, "_");
6668
+ const dname = `CN=Android Release, OU=${safeOU}, O=Android, C=US`;
6669
+ generateReleaseKeystore({
6670
+ keystoreAbsPath: abs,
6671
+ alias,
6672
+ storePassword: pw,
6673
+ keyPassword: pw,
6674
+ dname
6675
+ });
6676
+ if (cancelled || runId !== generateRunId.current) return;
6677
+ setState((s) => ({
6678
+ ...s,
6679
+ android: {
6680
+ ...s.android,
6681
+ keystoreFile: rel,
6682
+ keyAlias: alias,
6683
+ keystoreMode: "generate"
6684
+ },
6685
+ step: "android-password-env",
6686
+ generateError: null
6687
+ }));
6688
+ } catch (e) {
6689
+ const msg = e.message;
6690
+ if (abs && fs27.existsSync(abs) && (msg.includes("already exists") || msg.includes("Keystore already exists"))) {
6691
+ if (cancelled || runId !== generateRunId.current) return;
6692
+ const rel = state.android.genKeystorePath.trim() || "android/release.keystore";
6693
+ const alias = state.android.keyAlias.trim() || "release";
6694
+ setState((s) => ({
6695
+ ...s,
6696
+ android: {
6697
+ ...s.android,
6698
+ keystoreFile: rel,
6699
+ keyAlias: alias,
6700
+ keystoreMode: "generate"
6701
+ },
6702
+ step: "android-password-env",
6703
+ generateError: null
6704
+ }));
6705
+ return;
6706
+ }
6707
+ if (cancelled || runId !== generateRunId.current) return;
6708
+ setState((s) => ({
6709
+ ...s,
6710
+ step: "android-gen-password",
6711
+ generateError: msg
6712
+ }));
6713
+ }
6714
+ };
6715
+ run();
6716
+ return () => {
6717
+ cancelled = true;
6718
+ };
6719
+ }, [state.step, state.android.genKeystorePath, state.android.keyAlias, state.android.genPassword]);
6720
+ useEffect4(() => {
6721
+ if (state.step === "done") {
6722
+ setTimeout(() => {
6723
+ process.exit(0);
6724
+ }, 3e3);
6725
+ }
6726
+ }, [state.step]);
6727
+ const saveConfig = async () => {
6728
+ try {
6729
+ const resolved = resolveHostPaths();
6730
+ const configPath = path28.join(resolved.projectRoot, "tamer.config.json");
6731
+ let config = {};
6732
+ let androidEnvAppend = null;
6733
+ if (fs27.existsSync(configPath)) {
6734
+ config = JSON.parse(fs27.readFileSync(configPath, "utf8"));
6735
+ }
6736
+ if (state.platform === "android" || state.platform === "both") {
6737
+ config.android = config.android || {};
6738
+ config.android.signing = {
6739
+ keystoreFile: state.android.keystoreFile,
6740
+ keyAlias: state.android.keyAlias,
6741
+ storePasswordEnv: state.android.storePasswordEnv,
6742
+ keyPasswordEnv: state.android.keyPasswordEnv
6743
+ };
6744
+ if (state.android.keystoreMode === "generate" && state.android.genPassword) {
6745
+ const storeEnv = state.android.storePasswordEnv.trim() || "ANDROID_KEYSTORE_PASSWORD";
6746
+ const keyEnv = state.android.keyPasswordEnv.trim() || "ANDROID_KEY_PASSWORD";
6747
+ androidEnvAppend = appendEnvVarsIfMissing(resolved.projectRoot, {
6748
+ [storeEnv]: state.android.genPassword,
6749
+ [keyEnv]: state.android.genPassword
6750
+ });
6751
+ }
6752
+ }
6753
+ if (state.platform === "ios" || state.platform === "both") {
6754
+ config.ios = config.ios || {};
6755
+ config.ios.signing = {
6756
+ developmentTeam: state.ios.developmentTeam,
6757
+ ...state.ios.codeSignIdentity && { codeSignIdentity: state.ios.codeSignIdentity },
6758
+ ...state.ios.provisioningProfileSpecifier && { provisioningProfileSpecifier: state.ios.provisioningProfileSpecifier }
6759
+ };
6760
+ }
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");
6765
+ const additions = [
6766
+ ".env.local",
6767
+ "*.jks",
6768
+ "*.keystore"
6769
+ ];
6770
+ for (const addition of additions) {
6771
+ if (!gitignore.includes(addition)) {
6772
+ gitignore += `
6773
+ ${addition}
6774
+ `;
6775
+ }
6776
+ }
6777
+ fs27.writeFileSync(gitignorePath, gitignore);
6778
+ }
6779
+ setState((s) => ({
6780
+ ...s,
6781
+ step: "done",
6782
+ androidEnvAppend: state.platform === "android" || state.platform === "both" ? androidEnvAppend : null
6783
+ }));
6784
+ } catch (error) {
6785
+ console.error("Error saving config:", error);
6786
+ process.exit(1);
6787
+ }
6788
+ };
6789
+ if (state.step === "done") {
6790
+ return /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", children: [
6791
+ /* @__PURE__ */ jsx11(Text10, { color: "green", children: "\u2705 Signing configuration saved to tamer.config.json" }),
6792
+ (state.platform === "android" || state.platform === "both") && /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", marginTop: 1, children: [
6793
+ /* @__PURE__ */ jsx11(Text10, { children: "Android signing configured:" }),
6794
+ /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
6795
+ " Keystore: ",
6796
+ state.android.keystoreFile
6797
+ ] }),
6798
+ /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
6799
+ " Alias: ",
6800
+ state.android.keyAlias
6801
+ ] }),
6802
+ state.androidEnvAppend?.keys.length ? /* @__PURE__ */ jsxs10(Text10, { children: [
6803
+ "Appended ",
6804
+ state.androidEnvAppend.keys.join(", "),
6805
+ " to ",
6806
+ state.androidEnvAppend.file,
6807
+ " (existing keys left unchanged)."
6808
+ ] }) : state.androidEnvAppend?.skippedAll ? /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
6809
+ state.androidEnvAppend.file,
6810
+ " already defines the signing env vars; left unchanged."
6811
+ ] }) : /* @__PURE__ */ jsxs10(Fragment, { children: [
6812
+ /* @__PURE__ */ jsx11(Text10, { children: "Set environment variables (or add them to .env / .env.local):" }),
6813
+ /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
6814
+ " export ",
6815
+ state.android.storePasswordEnv,
6816
+ '="your-keystore-password"'
6817
+ ] }),
6818
+ /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
6819
+ " export ",
6820
+ state.android.keyPasswordEnv,
6821
+ '="your-key-password"'
6822
+ ] })
6823
+ ] })
6824
+ ] }),
6825
+ (state.platform === "ios" || state.platform === "both") && /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", marginTop: 1, children: [
6826
+ /* @__PURE__ */ jsx11(Text10, { children: "iOS signing configured:" }),
6827
+ /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
6828
+ " Team ID: ",
6829
+ state.ios.developmentTeam
6830
+ ] }),
6831
+ state.ios.codeSignIdentity && /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
6832
+ " Identity: ",
6833
+ state.ios.codeSignIdentity
6834
+ ] })
6835
+ ] }),
6836
+ /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", marginTop: 1, children: [
6837
+ state.platform === "android" && /* @__PURE__ */ jsxs10(Fragment, { children: [
6838
+ /* @__PURE__ */ jsx11(Text10, { children: "Run `t4l build android -p` to build this platform with signing." }),
6839
+ /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: "`t4l build -p` (no platform) builds both Android and iOS." })
6840
+ ] }),
6841
+ state.platform === "ios" && /* @__PURE__ */ jsxs10(Fragment, { children: [
6842
+ /* @__PURE__ */ jsx11(Text10, { children: "Run `t4l build ios -p` to build this platform with signing." }),
6843
+ /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: "`t4l build -p` (no platform) builds both Android and iOS." })
6844
+ ] }),
6845
+ state.platform === "both" && /* @__PURE__ */ jsxs10(Fragment, { children: [
6846
+ /* @__PURE__ */ jsx11(Text10, { children: "Run `t4l build -p` to build both platforms with signing." }),
6847
+ /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: "Or: `t4l build android -p` / `t4l build ios -p` for one platform." })
6848
+ ] })
6849
+ ] })
6850
+ ] });
6851
+ }
6852
+ if (state.step === "saving") {
6853
+ return /* @__PURE__ */ jsx11(Box9, { children: /* @__PURE__ */ jsx11(TuiSpinner, { label: "Saving configuration..." }) });
6854
+ }
6855
+ if (state.step === "android-generating") {
6856
+ return /* @__PURE__ */ jsx11(Box9, { flexDirection: "column", children: /* @__PURE__ */ jsx11(TuiSpinner, { label: "Running keytool to create release keystore..." }) });
6857
+ }
6858
+ return /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", children: [
6859
+ state.step === "platform" && /* @__PURE__ */ jsx11(
6860
+ TuiSelectInput,
6861
+ {
6862
+ label: "Select platform(s) to configure signing:",
6863
+ items: [
6864
+ { label: "Android", value: "android" },
6865
+ { label: "iOS", value: "ios" },
6866
+ { label: "Both", value: "both" }
6867
+ ],
6868
+ onSelect: (platform) => {
6869
+ setState((s) => ({ ...s, platform, step: firstStepForPlatform(platform) }));
6870
+ }
6871
+ }
6872
+ ),
6873
+ state.step === "android-keystore-mode" && /* @__PURE__ */ jsx11(
6874
+ AndroidKeystoreModeSelect,
6875
+ {
6876
+ onSelect: (mode) => {
6877
+ setState((s) => ({
6878
+ ...s,
6879
+ android: { ...s.android, keystoreMode: mode },
6880
+ step: mode === "generate" ? "android-gen-path" : "android-keystore",
6881
+ generateError: null
6882
+ }));
6883
+ }
6884
+ }
6885
+ ),
6886
+ state.step === "android-gen-path" && /* @__PURE__ */ jsx11(
6887
+ TuiTextInput,
6888
+ {
6889
+ label: "Keystore output path (relative to project root):",
6890
+ defaultValue: state.android.genKeystorePath,
6891
+ onSubmitValue: (v) => {
6892
+ const p = v.trim() || "android/release.keystore";
6893
+ setState((s) => ({ ...s, android: { ...s.android, genKeystorePath: p } }));
6894
+ },
6895
+ onSubmit: nextStep,
6896
+ hint: "Default: android/release.keystore (gitignored pattern *.keystore)"
6897
+ }
6898
+ ),
6899
+ state.step === "android-gen-alias" && /* @__PURE__ */ jsx11(
6900
+ TuiTextInput,
6901
+ {
6902
+ label: "Android key alias:",
6903
+ defaultValue: state.android.keyAlias,
6904
+ onSubmitValue: (v) => {
6905
+ setState((s) => ({ ...s, android: { ...s.android, keyAlias: v } }));
6906
+ },
6907
+ onSubmit: nextStep
6908
+ }
6909
+ ),
6910
+ state.step === "android-gen-password" && /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", children: [
6911
+ state.generateError ? /* @__PURE__ */ jsx11(Text10, { color: "red", children: state.generateError }) : null,
6912
+ /* @__PURE__ */ jsx11(
6913
+ TuiTextInput,
6914
+ {
6915
+ label: "Keystore and key password (same for both; shown as you type):",
6916
+ value: state.android.genPassword,
6917
+ onChange: (v) => setState((s) => ({ ...s, android: { ...s.android, genPassword: v } })),
6918
+ onSubmitValue: (pw) => {
6919
+ setState((s) => ({
6920
+ ...s,
6921
+ android: { ...s.android, genPassword: pw.trim() },
6922
+ step: "android-generating",
6923
+ generateError: null
6924
+ }));
6925
+ },
6926
+ onSubmit: () => {
6927
+ },
6928
+ hint: "At least 6 characters (JDK keytool). Same value used for -storepass and -keypass."
6929
+ }
6930
+ )
6931
+ ] }),
6932
+ state.step === "android-keystore" && /* @__PURE__ */ jsx11(
6933
+ TuiTextInput,
6934
+ {
6935
+ label: "Android keystore file path (relative to project root or android/):",
6936
+ defaultValue: state.android.keystoreFile,
6937
+ onSubmitValue: (v) => {
6938
+ setState((s) => ({ ...s, android: { ...s.android, keystoreFile: v } }));
6939
+ },
6940
+ onSubmit: nextStep,
6941
+ hint: "Example: android/app/my-release-key.keystore or ./my-release-key.keystore"
6942
+ }
6943
+ ),
6944
+ state.step === "android-alias" && /* @__PURE__ */ jsx11(
6945
+ TuiTextInput,
6946
+ {
6947
+ label: "Android key alias:",
6948
+ defaultValue: state.android.keyAlias,
6949
+ onSubmitValue: (v) => {
6950
+ setState((s) => ({ ...s, android: { ...s.android, keyAlias: v } }));
6951
+ },
6952
+ onSubmit: nextStep
6953
+ }
6954
+ ),
6955
+ state.step === "android-password-env" && /* @__PURE__ */ jsx11(
6956
+ TuiTextInput,
6957
+ {
6958
+ label: "Keystore password environment variable name:",
6959
+ defaultValue: state.android.storePasswordEnv || "ANDROID_KEYSTORE_PASSWORD",
6960
+ onSubmitValue: (v) => {
6961
+ setState((s) => ({ ...s, android: { ...s.android, storePasswordEnv: v } }));
6962
+ },
6963
+ onSubmit: () => {
6964
+ setState((s) => ({ ...s, step: "android-key-password-env" }));
6965
+ },
6966
+ hint: "Default: ANDROID_KEYSTORE_PASSWORD (will be written to .env / .env.local)"
6967
+ }
6968
+ ),
6969
+ state.step === "android-key-password-env" && /* @__PURE__ */ jsx11(
6970
+ TuiTextInput,
6971
+ {
6972
+ label: "Key password environment variable name:",
6973
+ defaultValue: state.android.keyPasswordEnv || "ANDROID_KEY_PASSWORD",
6974
+ onSubmitValue: (v) => {
6975
+ setState((s) => ({ ...s, android: { ...s.android, keyPasswordEnv: v } }));
6976
+ },
6977
+ onSubmit: () => {
6978
+ if (state.platform === "both") {
6979
+ setState((s) => ({ ...s, step: "ios-team" }));
6980
+ } else {
6981
+ setState((s) => ({ ...s, step: "saving" }));
6982
+ }
6983
+ },
6984
+ hint: "Default: ANDROID_KEY_PASSWORD (will be written to .env / .env.local)"
6985
+ }
6986
+ ),
6987
+ state.step === "ios-team" && /* @__PURE__ */ jsx11(
6988
+ TuiTextInput,
6989
+ {
6990
+ label: "iOS Development Team ID:",
6991
+ defaultValue: state.ios.developmentTeam,
6992
+ onSubmitValue: (v) => {
6993
+ setState((s) => ({ ...s, ios: { ...s.ios, developmentTeam: v } }));
6994
+ },
6995
+ onSubmit: nextStep,
6996
+ hint: "Example: ABC123DEF4 (found in Apple Developer account)"
6997
+ }
6998
+ ),
6999
+ state.step === "ios-identity" && /* @__PURE__ */ jsx11(
7000
+ TuiTextInput,
7001
+ {
7002
+ label: "iOS Code Sign Identity (optional, press Enter to skip):",
7003
+ defaultValue: state.ios.codeSignIdentity,
7004
+ onSubmitValue: (v) => {
7005
+ setState((s) => ({ ...s, ios: { ...s.ios, codeSignIdentity: v } }));
7006
+ },
7007
+ onSubmit: () => {
7008
+ setState((s) => ({ ...s, step: "ios-profile" }));
7009
+ },
7010
+ hint: 'Example: "iPhone Developer" or "Apple Development"'
7011
+ }
7012
+ ),
7013
+ state.step === "ios-profile" && /* @__PURE__ */ jsx11(
7014
+ TuiTextInput,
7015
+ {
7016
+ label: "iOS Provisioning Profile Specifier (optional, press Enter to skip):",
7017
+ defaultValue: state.ios.provisioningProfileSpecifier,
7018
+ onSubmitValue: (v) => {
7019
+ setState((s) => ({ ...s, ios: { ...s.ios, provisioningProfileSpecifier: v } }));
7020
+ },
7021
+ onSubmit: () => {
7022
+ setState((s) => ({ ...s, step: "saving" }));
7023
+ },
7024
+ hint: "UUID of the provisioning profile"
7025
+ }
7026
+ )
7027
+ ] });
7028
+ }
7029
+ async function signing(platform) {
7030
+ const { waitUntilExit } = render3(/* @__PURE__ */ jsx11(SigningWizard, { platform }));
7031
+ await waitUntilExit();
7032
+ }
7033
+
7034
+ // src/common/productionSigning.ts
7035
+ import fs28 from "fs";
7036
+ import path29 from "path";
7037
+ function isAndroidSigningConfigured(resolved) {
7038
+ const signing2 = resolved.config.android?.signing;
7039
+ const hasConfig = Boolean(signing2?.keystoreFile?.trim() && signing2?.keyAlias?.trim());
7040
+ const signingProps = path29.join(resolved.androidDir, "signing.properties");
7041
+ const hasProps = fs28.existsSync(signingProps);
7042
+ return hasConfig || hasProps;
7043
+ }
7044
+ function isIosSigningConfigured(resolved) {
7045
+ const team = resolved.config.ios?.signing?.developmentTeam?.trim();
7046
+ return Boolean(team);
7047
+ }
7048
+ function assertProductionSigningReady(filter) {
7049
+ const resolved = resolveHostPaths();
7050
+ const needAndroid = filter === "android" || filter === "all";
7051
+ const needIos = filter === "ios" || filter === "all";
7052
+ const missing = [];
7053
+ if (needAndroid && !isAndroidSigningConfigured(resolved)) {
7054
+ missing.push("Android: run `t4l signing android`, then `t4l build android -p`.");
7055
+ }
7056
+ if (needIos && !isIosSigningConfigured(resolved)) {
7057
+ missing.push("iOS: run `t4l signing ios`, then `t4l build ios -p`.");
7058
+ }
7059
+ if (missing.length === 0) return;
7060
+ console.error("\n\u274C Production build (`-p`) needs signing configured for the platform(s) you are building.");
7061
+ for (const line of missing) {
7062
+ console.error(` ${line}`);
7063
+ }
7064
+ console.error(
7065
+ "\n `t4l build -p` (no platform) builds both Android and iOS; use `t4l build android -p` or `t4l build ios -p` for one platform only.\n"
7066
+ );
7067
+ process.exit(1);
7068
+ }
7069
+
5751
7070
  // index.ts
5752
7071
  function readCliVersion() {
5753
- const root = path25.dirname(fileURLToPath(import.meta.url));
5754
- const here = path25.join(root, "package.json");
5755
- const parent = path25.join(root, "..", "package.json");
5756
- const pkgPath = fs24.existsSync(here) ? here : parent;
5757
- return JSON.parse(fs24.readFileSync(pkgPath, "utf8")).version;
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;
5758
7077
  }
5759
7078
  var version = readCliVersion();
5760
- function validateDebugRelease(debug, release) {
5761
- if (debug && release) {
5762
- console.error("Cannot use --debug and --release together.");
7079
+ function validateBuildMode(debug, release, production) {
7080
+ const modes = [debug, release, production].filter(Boolean).length;
7081
+ if (modes > 1) {
7082
+ console.error("Cannot use --debug, --release, and --production together. Use only one.");
5763
7083
  process.exit(1);
5764
7084
  }
5765
7085
  }
@@ -5771,7 +7091,7 @@ function parsePlatform(value) {
5771
7091
  }
5772
7092
  program.version(version).description("Tamer4Lynx CLI - A tool for managing Lynx projects");
5773
7093
  program.command("init").description("Initialize tamer.config.json interactively").action(() => {
5774
- init_default();
7094
+ init();
5775
7095
  });
5776
7096
  program.command("create <target>").description("Create a project or extension. Target: ios | android | module | element | service | combo").option("-d, --debug", "For android: create host project (default)").option("-r, --release", "For android: create dev-app project").action(async (target, opts) => {
5777
7097
  const t = target.toLowerCase();
@@ -5794,22 +7114,26 @@ program.command("create <target>").description("Create a project or extension. T
5794
7114
  console.error(`Invalid create target: ${target}. Use ios | android | module | element | service | combo`);
5795
7115
  process.exit(1);
5796
7116
  });
5797
- program.command("build [platform]").description("Build app. Platform: ios | android (default: both)").option("-e, --embeddable", "Output embeddable bundle + code for existing apps. Use with --release.").option("-d, --debug", "Debug build with dev client embedded (default)").option("-r, --release", "Release build without dev client").option("-i, --install", "Install after building").action(async (platform, opts) => {
5798
- validateDebugRelease(opts.debug, opts.release);
5799
- const release = opts.release === true;
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) => {
7118
+ validateBuildMode(opts.debug, opts.release, opts.production);
7119
+ const release = opts.release === true || opts.production === true;
7120
+ const production = opts.production === true;
5800
7121
  if (opts.embeddable) {
5801
7122
  await buildEmbeddable({ release: true });
5802
7123
  return;
5803
7124
  }
5804
7125
  const p = parsePlatform(platform ?? "all") ?? "all";
7126
+ if (production) {
7127
+ assertProductionSigningReady(p);
7128
+ }
5805
7129
  if (p === "android" || p === "all") {
5806
- await build_default({ install: opts.install, release });
7130
+ await build_default({ install: opts.install, release, production });
5807
7131
  }
5808
7132
  if (p === "ios" || p === "all") {
5809
- await build_default2({ install: opts.install, release });
7133
+ await build_default2({ install: opts.install, release, production });
5810
7134
  }
5811
7135
  });
5812
- program.command("link [platform]").description("Link native modules. Platform: ios | android | both (default: both)").option("-s, --silent", "Run in silent mode (e.g. for postinstall)").action((platform, opts) => {
7136
+ program.command("link [platform]").description("Link native modules. Platform: ios | android | both (default: both)").option("-s, --silent", "Run without output").action((platform, opts) => {
5813
7137
  if (opts.silent) {
5814
7138
  console.log = () => {
5815
7139
  };
@@ -5830,14 +7154,15 @@ program.command("link [platform]").description("Link native modules. Platform: i
5830
7154
  autolink_default2();
5831
7155
  autolink_default();
5832
7156
  });
5833
- program.command("bundle [platform]").description("Build Lynx bundle and copy to native project. Platform: ios | android (default: both)").option("-d, --debug", "Debug bundle with dev client embedded (default)").option("-r, --release", "Release bundle without dev client").action(async (platform, opts) => {
5834
- validateDebugRelease(opts.debug, opts.release);
5835
- const release = opts.release === true;
7157
+ program.command("bundle [platform]").description("Build Lynx bundle and copy to native project. Platform: ios | android (default: both)").option("-d, --debug", "Debug bundle with dev client embedded (default)").option("-r, --release", "Release bundle without dev client (unsigned)").option("-p, --production", "Production bundle for app store (signed)").action(async (platform, opts) => {
7158
+ validateBuildMode(opts.debug, opts.release, opts.production);
7159
+ const release = opts.release === true || opts.production === true;
7160
+ const production = opts.production === true;
5836
7161
  const p = parsePlatform(platform ?? "both") ?? "both";
5837
- if (p === "android" || p === "all") await bundle_default({ release });
5838
- if (p === "ios" || p === "all") bundle_default2({ release });
7162
+ if (p === "android" || p === "all") await bundle_default({ release, production });
7163
+ if (p === "ios" || p === "all") bundle_default2({ release, production });
5839
7164
  });
5840
- program.command("inject <platform>").description("Inject tamer-host templates into an existing project. Platform: ios | android").option("-f, --force", "Overwrite existing files").action(async (platform, opts) => {
7165
+ program.command("inject <platform>").description("Inject host templates into an existing project. Platform: ios | android").option("-f, --force", "Overwrite existing files").action(async (platform, opts) => {
5841
7166
  const p = platform?.toLowerCase();
5842
7167
  if (p === "ios") {
5843
7168
  await injectHostIos({ force: opts.force });
@@ -5850,7 +7175,7 @@ program.command("inject <platform>").description("Inject tamer-host templates in
5850
7175
  console.error(`Invalid inject platform: ${platform}. Use ios | android`);
5851
7176
  process.exit(1);
5852
7177
  });
5853
- program.command("sync [platform]").description("Sync dev client files from tamer.config.json. Platform: android (default)").action(async (platform) => {
7178
+ program.command("sync [platform]").description("Sync dev client. Platform: android (default)").action(async (platform) => {
5854
7179
  const p = (platform ?? "android").toLowerCase();
5855
7180
  if (p !== "android") {
5856
7181
  console.error("sync only supports android.");
@@ -5871,16 +7196,27 @@ program.command("build-dev-app").option("-p, --platform <platform>", "Platform:
5871
7196
  await build_default2({ install: opts.install, release: false });
5872
7197
  }
5873
7198
  });
5874
- program.command("add [packages...]").description("Add @tamer4lynx packages to the Lynx project. Future: will track versions for compatibility (Expo-style).").action(async (packages) => {
7199
+ program.command("add [packages...]").description("Add @tamer4lynx packages to the Lynx project").action(async (packages) => {
5875
7200
  await add(packages);
5876
7201
  });
5877
- program.command("add-core").description("Add core packages (app-shell, screen, router, insets, transports, system-ui, icons)").action(async () => {
7202
+ program.command("add-core").description("Add core packages").action(async () => {
5878
7203
  await addCore();
5879
7204
  });
7205
+ program.command("add-dev").description("Add dev-app, dev-client, and their dependencies").action(async () => {
7206
+ await addDev();
7207
+ });
7208
+ program.command("signing [platform]").description("Configure Android and iOS signing interactively").action(async (platform) => {
7209
+ const p = platform?.toLowerCase();
7210
+ if (p === "android" || p === "ios") {
7211
+ await signing(p);
7212
+ } else {
7213
+ await signing();
7214
+ }
7215
+ });
5880
7216
  program.command("codegen").description("Generate code from @lynxmodule declarations").action(() => {
5881
7217
  codegen_default();
5882
7218
  });
5883
- program.command("android <subcommand>").description("(Legacy) Use: t4l <command> android. e.g. t4l create android").option("-d, --debug", "Create: host project. Bundle/build: debug with dev client.").option("-r, --release", "Create: dev-app project. Bundle/build: release without dev client.").option("-i, --install", "Install after build").option("-e, --embeddable", "Build embeddable").option("-f, --force", "Force (inject)").action(async (subcommand, opts) => {
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) => {
5884
7220
  const sub = subcommand?.toLowerCase();
5885
7221
  if (sub === "create") {
5886
7222
  if (opts.debug && opts.release) {
@@ -5895,14 +7231,19 @@ program.command("android <subcommand>").description("(Legacy) Use: t4l <command>
5895
7231
  return;
5896
7232
  }
5897
7233
  if (sub === "bundle") {
5898
- validateDebugRelease(opts.debug, opts.release);
5899
- await bundle_default({ release: opts.release === true });
7234
+ validateBuildMode(opts.debug, opts.release, opts.production);
7235
+ const release = opts.release === true || opts.production === true;
7236
+ await bundle_default({ release, production: opts.production === true });
5900
7237
  return;
5901
7238
  }
5902
7239
  if (sub === "build") {
5903
- validateDebugRelease(opts.debug, opts.release);
7240
+ validateBuildMode(opts.debug, opts.release, opts.production);
7241
+ const release = opts.release === true || opts.production === true;
5904
7242
  if (opts.embeddable) await buildEmbeddable({ release: true });
5905
- else await build_default({ install: opts.install, release: opts.release === true });
7243
+ else {
7244
+ if (opts.production === true) assertProductionSigningReady("android");
7245
+ await build_default({ install: opts.install, release, production: opts.production === true });
7246
+ }
5906
7247
  return;
5907
7248
  }
5908
7249
  if (sub === "sync") {
@@ -5916,7 +7257,7 @@ program.command("android <subcommand>").description("(Legacy) Use: t4l <command>
5916
7257
  console.error(`Unknown android subcommand: ${subcommand}. Use: create | link | bundle | build | sync | inject`);
5917
7258
  process.exit(1);
5918
7259
  });
5919
- program.command("ios <subcommand>").description("(Legacy) Use: t4l <command> ios. e.g. t4l create ios").option("-d, --debug", "Debug (bundle/build)").option("-r, --release", "Release (bundle/build)").option("-i, --install", "Install after build").option("-e, --embeddable", "Build embeddable").option("-f, --force", "Force (inject)").action(async (subcommand, opts) => {
7260
+ program.command("ios <subcommand>").description("(Legacy) Use: t4l <command> ios. e.g. t4l create ios").option("-d, --debug", "Debug (bundle/build)").option("-r, --release", "Release (bundle/build)").option("-p, --production", "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) => {
5920
7261
  const sub = subcommand?.toLowerCase();
5921
7262
  if (sub === "create") {
5922
7263
  create_default2();
@@ -5927,14 +7268,19 @@ program.command("ios <subcommand>").description("(Legacy) Use: t4l <command> ios
5927
7268
  return;
5928
7269
  }
5929
7270
  if (sub === "bundle") {
5930
- validateDebugRelease(opts.debug, opts.release);
5931
- bundle_default2({ release: opts.release === true });
7271
+ validateBuildMode(opts.debug, opts.release, opts.production);
7272
+ const release = opts.release === true || opts.production === true;
7273
+ bundle_default2({ release, production: opts.production === true });
5932
7274
  return;
5933
7275
  }
5934
7276
  if (sub === "build") {
5935
- validateDebugRelease(opts.debug, opts.release);
7277
+ validateBuildMode(opts.debug, opts.release, opts.production);
7278
+ const release = opts.release === true || opts.production === true;
5936
7279
  if (opts.embeddable) await buildEmbeddable({ release: true });
5937
- else await build_default2({ install: opts.install, release: opts.release === true });
7280
+ else {
7281
+ if (opts.production === true) assertProductionSigningReady("ios");
7282
+ await build_default2({ install: opts.install, release, production: opts.production === true });
7283
+ }
5938
7284
  return;
5939
7285
  }
5940
7286
  if (sub === "inject") {
@@ -5945,10 +7291,10 @@ program.command("ios <subcommand>").description("(Legacy) Use: t4l <command> ios
5945
7291
  process.exit(1);
5946
7292
  });
5947
7293
  program.command("autolink-toggle").alias("autolink").description("Toggle autolink on/off in tamer.config.json (controls postinstall linking)").action(async () => {
5948
- const configPath = path25.join(process.cwd(), "tamer.config.json");
7294
+ const configPath = path30.join(process.cwd(), "tamer.config.json");
5949
7295
  let config = {};
5950
- if (fs24.existsSync(configPath)) {
5951
- config = JSON.parse(fs24.readFileSync(configPath, "utf8"));
7296
+ if (fs29.existsSync(configPath)) {
7297
+ config = JSON.parse(fs29.readFileSync(configPath, "utf8"));
5952
7298
  }
5953
7299
  if (config.autolink) {
5954
7300
  delete config.autolink;
@@ -5957,11 +7303,11 @@ program.command("autolink-toggle").alias("autolink").description("Toggle autolin
5957
7303
  config.autolink = true;
5958
7304
  console.log("Autolink enabled in tamer.config.json");
5959
7305
  }
5960
- fs24.writeFileSync(configPath, JSON.stringify(config, null, 2));
7306
+ fs29.writeFileSync(configPath, JSON.stringify(config, null, 2));
5961
7307
  console.log(`Updated ${configPath}`);
5962
7308
  });
5963
7309
  if (process.argv.length <= 2 || process.argv.length === 3 && process.argv[2] === "init") {
5964
- Promise.resolve(init_default()).then(() => process.exit(0));
7310
+ Promise.resolve(init()).then(() => process.exit(0));
5965
7311
  } else {
5966
7312
  program.parseAsync().then(() => process.exit(0)).catch(() => process.exit(1));
5967
7313
  }