@tamer4lynx/cli 0.0.13 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +1675 -437
  2. package/package.json +2 -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 fs28 from "fs";
11
+ import path29 from "path";
12
12
  import { fileURLToPath } from "url";
13
13
  import { program } from "commander";
14
14
 
@@ -2391,7 +2391,7 @@ var syncDevClient_default = syncDevClient;
2391
2391
 
2392
2392
  // src/android/bundle.ts
2393
2393
  async function bundleAndDeploy(opts = {}) {
2394
- const release = opts.release === true;
2394
+ const release = opts.release === true || opts.production === true;
2395
2395
  let resolved;
2396
2396
  try {
2397
2397
  resolved = resolveHostPaths();
@@ -2468,10 +2468,11 @@ async function buildApk(opts = {}) {
2468
2468
  } catch (error) {
2469
2469
  throw error;
2470
2470
  }
2471
- await bundle_default({ release: opts.release });
2471
+ const release = opts.release === true || opts.production === true;
2472
+ await bundle_default({ release, production: opts.production });
2472
2473
  const androidDir = resolved.androidDir;
2473
2474
  const gradlew = path11.join(androidDir, process.platform === "win32" ? "gradlew.bat" : "gradlew");
2474
- const variant = opts.release ? "Release" : "Debug";
2475
+ const variant = release ? "Release" : "Debug";
2475
2476
  const task = opts.install ? `install${variant}` : `assemble${variant}`;
2476
2477
  console.log(`
2477
2478
  \u{1F528} Building ${variant.toLowerCase()} APK${opts.install ? " and installing" : ""}...`);
@@ -4241,7 +4242,7 @@ import fs15 from "fs";
4241
4242
  import path16 from "path";
4242
4243
  import { execSync as execSync7 } from "child_process";
4243
4244
  function bundleAndDeploy2(opts = {}) {
4244
- const release = opts.release === true;
4245
+ const release = opts.release === true || opts.production === true;
4245
4246
  let resolved;
4246
4247
  try {
4247
4248
  resolved = resolveHostPaths();
@@ -4351,8 +4352,9 @@ async function buildIpa(opts = {}) {
4351
4352
  const appName = resolved.config.ios.appName;
4352
4353
  const bundleId = resolved.config.ios.bundleId;
4353
4354
  const iosDir = resolved.iosDir;
4354
- const configuration = opts.release ? "Release" : "Debug";
4355
- bundle_default2({ release: opts.release });
4355
+ const release = opts.release === true || opts.production === true;
4356
+ const configuration = release ? "Release" : "Debug";
4357
+ bundle_default2({ release, production: opts.production });
4356
4358
  const scheme = appName;
4357
4359
  const workspacePath = path17.join(iosDir, `${appName}.xcworkspace`);
4358
4360
  const projectPath = path17.join(iosDir, `${appName}.xcodeproj`);
@@ -4403,101 +4405,507 @@ async function buildIpa(opts = {}) {
4403
4405
  }
4404
4406
  var build_default2 = buildIpa;
4405
4407
 
4406
- // src/common/init.ts
4408
+ // src/common/init.tsx
4407
4409
  import fs17 from "fs";
4408
4410
  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): ");
4411
+ import { useState as useState4, useEffect as useEffect2, useCallback as useCallback3 } from "react";
4412
+ import { render, Text as Text9, Box as Box8 } from "ink";
4413
+
4414
+ // src/common/tui/components/TextInput.tsx
4415
+ import { useState, useEffect } from "react";
4416
+ import { Box, Text } from "ink";
4417
+ import InkTextInput from "ink-text-input";
4418
+ import { jsx, jsxs } from "react/jsx-runtime";
4419
+ function TuiTextInput({
4420
+ label,
4421
+ value: valueProp,
4422
+ defaultValue = "",
4423
+ onChange: onChangeProp,
4424
+ onSubmitValue,
4425
+ onSubmit,
4426
+ hint,
4427
+ error
4428
+ }) {
4429
+ const controlled = valueProp !== void 0;
4430
+ const [internal, setInternal] = useState(defaultValue);
4431
+ useEffect(() => {
4432
+ if (!controlled) setInternal(defaultValue);
4433
+ }, [defaultValue, controlled]);
4434
+ const value = controlled ? valueProp : internal;
4435
+ const onChange = (v) => {
4436
+ if (!controlled) setInternal(v);
4437
+ onChangeProp?.(v);
4438
+ };
4439
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
4440
+ label ? /* @__PURE__ */ jsx(Text, { children: label }) : null,
4441
+ /* @__PURE__ */ jsx(
4442
+ InkTextInput,
4443
+ {
4444
+ value,
4445
+ onChange,
4446
+ onSubmit: () => {
4447
+ const r = onSubmitValue?.(value);
4448
+ if (r === false) return;
4449
+ onSubmit();
4450
+ }
4451
+ }
4452
+ ),
4453
+ error ? /* @__PURE__ */ jsx(Text, { color: "red", children: error }) : hint ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: hint }) : null
4454
+ ] });
4455
+ }
4456
+
4457
+ // src/common/tui/components/SelectInput.tsx
4458
+ import "react";
4459
+ import { Box as Box2, Text as Text2 } from "ink";
4460
+ import InkSelectInput from "ink-select-input";
4461
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
4462
+ function TuiSelectInput({
4463
+ label,
4464
+ items,
4465
+ onSelect,
4466
+ hint
4467
+ }) {
4468
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
4469
+ label ? /* @__PURE__ */ jsx2(Text2, { children: label }) : null,
4470
+ /* @__PURE__ */ jsx2(
4471
+ InkSelectInput,
4472
+ {
4473
+ items,
4474
+ onSelect: (item) => onSelect(item.value)
4475
+ }
4476
+ ),
4477
+ hint ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: hint }) : null
4478
+ ] });
4479
+ }
4480
+
4481
+ // src/common/tui/components/PasswordInput.tsx
4482
+ import "react";
4483
+ import { Box as Box3, Text as Text3 } from "ink";
4484
+ import InkTextInput2 from "ink-text-input";
4485
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
4486
+
4487
+ // src/common/tui/components/ConfirmInput.tsx
4488
+ import "react";
4489
+ import { Box as Box4, Text as Text4 } from "ink";
4490
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
4491
+ function TuiConfirmInput({
4492
+ label,
4493
+ onConfirm,
4494
+ defaultYes = false,
4495
+ hint
4496
+ }) {
4497
+ const items = defaultYes ? [
4498
+ { label: "Yes (default)", value: "yes" },
4499
+ { label: "No", value: "no" }
4500
+ ] : [
4501
+ { label: "No (default)", value: "no" },
4502
+ { label: "Yes", value: "yes" }
4503
+ ];
4504
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
4505
+ label ? /* @__PURE__ */ jsx4(Text4, { children: label }) : null,
4506
+ /* @__PURE__ */ jsx4(
4507
+ TuiSelectInput,
4508
+ {
4509
+ items,
4510
+ onSelect: (v) => onConfirm(v === "yes"),
4511
+ hint
4512
+ }
4513
+ )
4514
+ ] });
4515
+ }
4516
+
4517
+ // src/common/tui/components/Spinner.tsx
4518
+ import "react";
4519
+ import { Text as Text5 } from "ink";
4520
+ import InkSpinner from "ink-spinner";
4521
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
4522
+ function TuiSpinner({ label, type = "dots" }) {
4523
+ return /* @__PURE__ */ jsxs5(Text5, { color: "cyan", children: [
4524
+ /* @__PURE__ */ jsx5(InkSpinner, { type }),
4525
+ label ? ` ${label}` : ""
4526
+ ] });
4527
+ }
4528
+
4529
+ // src/common/tui/components/StatusBox.tsx
4530
+ import "react";
4531
+ import { Box as Box5, Text as Text6 } from "ink";
4532
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
4533
+ var colors = {
4534
+ success: "green",
4535
+ error: "red",
4536
+ warning: "yellow",
4537
+ info: "cyan"
4538
+ };
4539
+ function StatusBox({ variant, children, title }) {
4540
+ const c = colors[variant];
4541
+ return /* @__PURE__ */ jsxs6(Box5, { flexDirection: "column", borderStyle: "round", borderColor: c, paddingX: 1, children: [
4542
+ title ? /* @__PURE__ */ jsx6(Text6, { bold: true, color: c, children: title }) : null,
4543
+ /* @__PURE__ */ jsx6(Box5, { flexDirection: "column", children })
4544
+ ] });
4545
+ }
4546
+
4547
+ // src/common/tui/components/Wizard.tsx
4548
+ import "react";
4549
+ import { Box as Box6, Text as Text7 } from "ink";
4550
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
4551
+ function Wizard({ step, total, title, children }) {
4552
+ return /* @__PURE__ */ jsxs7(Box6, { flexDirection: "column", children: [
4553
+ /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
4554
+ "Step ",
4555
+ step,
4556
+ "/",
4557
+ total,
4558
+ title ? ` \u2014 ${title}` : ""
4559
+ ] }),
4560
+ /* @__PURE__ */ jsx7(Box6, { marginTop: 1, flexDirection: "column", children })
4561
+ ] });
4562
+ }
4563
+
4564
+ // src/common/tui/components/ServerDashboard.tsx
4565
+ import "react";
4566
+ import { Box as Box7, Text as Text8 } from "ink";
4567
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
4568
+ function ServerDashboard({
4569
+ projectName,
4570
+ port,
4571
+ lanIp,
4572
+ devUrl,
4573
+ wsUrl,
4574
+ lynxBundleFile,
4575
+ bonjour,
4576
+ verbose,
4577
+ buildPhase,
4578
+ buildError,
4579
+ wsConnections,
4580
+ logLines,
4581
+ showLogs,
4582
+ qrLines,
4583
+ phase,
4584
+ startError
4585
+ }) {
4586
+ if (phase === "failed") {
4587
+ return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", children: [
4588
+ /* @__PURE__ */ jsx8(Text8, { color: "red", bold: true, children: "Dev server failed to start" }),
4589
+ startError ? /* @__PURE__ */ jsx8(Text8, { color: "red", children: startError }) : null,
4590
+ /* @__PURE__ */ jsx8(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Press Ctrl+C or 'q' to quit" }) })
4591
+ ] });
4592
+ }
4593
+ const bundlePath = `${devUrl}/${lynxBundleFile}`;
4594
+ return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", children: [
4595
+ /* @__PURE__ */ jsxs8(Text8, { bold: true, color: "green", children: [
4596
+ "Tamer4Lynx dev server (",
4597
+ projectName,
4598
+ ")"
4599
+ ] }),
4600
+ verbose ? /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Logs: verbose (native + JS)" }) : null,
4601
+ /* @__PURE__ */ jsxs8(Box7, { marginTop: 1, flexDirection: "row", columnGap: 3, alignItems: "flex-start", children: [
4602
+ qrLines.length > 0 ? /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", flexShrink: 0, children: [
4603
+ /* @__PURE__ */ jsx8(Text8, { bold: true, children: "Scan" }),
4604
+ qrLines.map((line, i) => /* @__PURE__ */ jsx8(Text8, { children: line }, i)),
4605
+ /* @__PURE__ */ jsxs8(Box7, { marginTop: 1, flexDirection: "column", children: [
4606
+ /* @__PURE__ */ jsx8(Text8, { bold: true, children: "Open" }),
4607
+ /* @__PURE__ */ jsx8(Text8, { color: "cyan", wrap: "truncate-end", children: devUrl })
4608
+ ] })
4609
+ ] }) : /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", flexShrink: 0, children: [
4610
+ /* @__PURE__ */ jsx8(Text8, { bold: true, children: "Open" }),
4611
+ /* @__PURE__ */ jsx8(Text8, { color: "cyan", wrap: "truncate-end", children: devUrl })
4612
+ ] }),
4613
+ /* @__PURE__ */ jsxs8(
4614
+ Box7,
4615
+ {
4616
+ flexDirection: "column",
4617
+ flexGrow: 1,
4618
+ minWidth: 28,
4619
+ marginTop: qrLines.length > 0 ? 2 : 0,
4620
+ children: [
4621
+ /* @__PURE__ */ jsxs8(Text8, { children: [
4622
+ "Port: ",
4623
+ /* @__PURE__ */ jsx8(Text8, { color: "cyan", children: port }),
4624
+ " \xB7 LAN: ",
4625
+ /* @__PURE__ */ jsx8(Text8, { color: "cyan", children: lanIp })
4626
+ ] }),
4627
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, wrap: "truncate-end", children: bundlePath }),
4628
+ /* @__PURE__ */ jsxs8(Text8, { dimColor: true, wrap: "truncate-end", children: [
4629
+ devUrl,
4630
+ "/meta.json"
4631
+ ] }),
4632
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, wrap: "truncate-end", children: wsUrl }),
4633
+ bonjour ? /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "mDNS: _tamer._tcp" }) : null,
4634
+ /* @__PURE__ */ jsxs8(Box7, { marginTop: 1, flexDirection: "column", children: [
4635
+ /* @__PURE__ */ jsx8(Text8, { bold: true, children: "Build" }),
4636
+ 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" })
4637
+ ] }),
4638
+ /* @__PURE__ */ jsxs8(Box7, { marginTop: 1, flexDirection: "column", children: [
4639
+ /* @__PURE__ */ jsx8(Text8, { bold: true, children: "Connections" }),
4640
+ /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
4641
+ "WebSocket clients: ",
4642
+ wsConnections
4643
+ ] })
4644
+ ] })
4645
+ ]
4646
+ }
4647
+ )
4648
+ ] }),
4649
+ showLogs && logLines.length > 0 ? /* @__PURE__ */ jsxs8(Box7, { marginTop: 1, flexDirection: "column", borderStyle: "single", paddingX: 1, children: [
4650
+ /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
4651
+ "Build / output (last ",
4652
+ logLines.length,
4653
+ " lines)"
4654
+ ] }),
4655
+ logLines.slice(-12).map((line, i) => /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: line }, i))
4656
+ ] }) : null,
4657
+ /* @__PURE__ */ jsx8(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "r rebuild \xB7 l toggle logs \xB7 q quit" }) })
4658
+ ] });
4659
+ }
4660
+
4661
+ // src/common/tui/hooks/useInputState.ts
4662
+ import { useState as useState2, useCallback } from "react";
4663
+
4664
+ // src/common/tui/hooks/useValidation.ts
4665
+ function isValidAndroidPackage(name) {
4666
+ const s = name.trim();
4667
+ if (!s) return false;
4668
+ return /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/.test(s);
4669
+ }
4670
+ function isValidIosBundleId(id) {
4671
+ const s = id.trim();
4672
+ if (!s) return false;
4673
+ return /^[a-zA-Z][a-zA-Z0-9_-]*(\.[a-zA-Z0-9][a-zA-Z0-9_-]*)+$/.test(s);
4674
+ }
4675
+
4676
+ // src/common/tui/hooks/useServerStatus.ts
4677
+ import { useState as useState3, useCallback as useCallback2 } from "react";
4678
+
4679
+ // src/common/init.tsx
4680
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
4681
+ function resolveSdkInput(raw) {
4682
+ let androidSdk = raw.trim();
4425
4683
  if (androidSdk.startsWith("$") && /^[A-Z0-9_]+$/.test(androidSdk.slice(1))) {
4426
4684
  const envVar = androidSdk.slice(1);
4427
4685
  const envValue = process.env[envVar];
4428
4686
  if (envValue) {
4429
4687
  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.`);
4688
+ return { resolved: androidSdk, message: `Using ${androidSdk} from $${envVar}` };
4433
4689
  }
4690
+ return {
4691
+ resolved: androidSdk,
4692
+ message: `Environment variable $${envVar} not found \u2014 path saved as typed.`
4693
+ };
4434
4694
  }
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);
4695
+ return { resolved: androidSdk };
4696
+ }
4697
+ function InitWizard() {
4698
+ const [step, setStep] = useState4("welcome");
4699
+ const [androidAppName, setAndroidAppName] = useState4("");
4700
+ const [androidPackageName, setAndroidPackageName] = useState4("");
4701
+ const [androidSdk, setAndroidSdk] = useState4("");
4702
+ const [sdkHint, setSdkHint] = useState4();
4703
+ const [iosAppName, setIosAppName] = useState4("");
4704
+ const [iosBundleId, setIosBundleId] = useState4("");
4705
+ const [lynxProject, setLynxProject] = useState4("");
4706
+ const [pkgError, setPkgError] = useState4();
4707
+ const [bundleError, setBundleError] = useState4();
4708
+ const [doneMessage, setDoneMessage] = useState4([]);
4709
+ const writeConfigAndTsconfig = useCallback3(() => {
4710
+ const config = {
4711
+ android: {
4712
+ appName: androidAppName || void 0,
4713
+ packageName: androidPackageName || void 0,
4714
+ sdk: androidSdk || void 0
4715
+ },
4716
+ ios: {
4717
+ appName: iosAppName || void 0,
4718
+ bundleId: iosBundleId || void 0
4719
+ },
4720
+ paths: { androidDir: "android", iosDir: "ios" }
4721
+ };
4722
+ if (lynxProject.trim()) config.lynxProject = lynxProject.trim();
4723
+ const configPath = path18.join(process.cwd(), "tamer.config.json");
4724
+ fs17.writeFileSync(configPath, JSON.stringify(config, null, 2));
4725
+ const lines = [`Generated tamer.config.json at ${configPath}`];
4726
+ const tamerTypesInclude = "node_modules/@tamer4lynx/tamer-*/src/**/*.d.ts";
4727
+ const tsconfigCandidates = lynxProject.trim() ? [
4728
+ path18.join(process.cwd(), lynxProject.trim(), "tsconfig.json"),
4729
+ path18.join(process.cwd(), "tsconfig.json")
4730
+ ] : [path18.join(process.cwd(), "tsconfig.json")];
4731
+ function parseTsconfigJson(raw) {
4732
+ try {
4733
+ return JSON.parse(raw);
4734
+ } catch {
4735
+ const noTrailingCommas = raw.replace(/,\s*([\]}])/g, "$1");
4736
+ return JSON.parse(noTrailingCommas);
4737
+ }
4471
4738
  }
4472
- }
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);
4739
+ for (const tsconfigPath of tsconfigCandidates) {
4740
+ if (!fs17.existsSync(tsconfigPath)) continue;
4741
+ try {
4742
+ const raw = fs17.readFileSync(tsconfigPath, "utf-8");
4743
+ const tsconfig = parseTsconfigJson(raw);
4744
+ const include = tsconfig.include ?? [];
4745
+ const arr = Array.isArray(include) ? include : [include];
4746
+ if (arr.some((p) => (typeof p === "string" ? p : "").includes("tamer-"))) continue;
4747
+ arr.push(tamerTypesInclude);
4748
+ tsconfig.include = arr;
4749
+ fs17.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
4750
+ lines.push(`Updated ${path18.relative(process.cwd(), tsconfigPath)} for tamer types`);
4751
+ break;
4752
+ } catch (e) {
4753
+ lines.push(`Could not update ${tsconfigPath}: ${e.message}`);
4754
+ }
4488
4755
  }
4756
+ setDoneMessage(lines);
4757
+ setStep("done");
4758
+ setTimeout(() => process.exit(0), 2e3);
4759
+ }, [androidAppName, androidPackageName, androidSdk, iosAppName, iosBundleId, lynxProject]);
4760
+ useEffect2(() => {
4761
+ if (step !== "saving") return;
4762
+ writeConfigAndTsconfig();
4763
+ }, [step, writeConfigAndTsconfig]);
4764
+ if (step === "welcome") {
4765
+ return /* @__PURE__ */ jsxs9(Box8, { flexDirection: "column", children: [
4766
+ /* @__PURE__ */ jsx9(Text9, { bold: true, children: "Tamer4Lynx init" }),
4767
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Set up tamer.config.json for your project." }),
4768
+ /* @__PURE__ */ jsx9(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx9(
4769
+ TuiSelectInput,
4770
+ {
4771
+ label: "Continue?",
4772
+ items: [{ label: "Start", value: "start" }],
4773
+ onSelect: () => setStep("android-app")
4774
+ }
4775
+ ) })
4776
+ ] });
4777
+ }
4778
+ if (step === "android-app") {
4779
+ return /* @__PURE__ */ jsx9(Wizard, { step: 1, total: 6, title: "Android app name", children: /* @__PURE__ */ jsx9(
4780
+ TuiTextInput,
4781
+ {
4782
+ label: "Android app name:",
4783
+ defaultValue: androidAppName,
4784
+ onSubmitValue: (v) => setAndroidAppName(v),
4785
+ onSubmit: () => setStep("android-pkg")
4786
+ }
4787
+ ) });
4788
+ }
4789
+ if (step === "android-pkg") {
4790
+ return /* @__PURE__ */ jsx9(Wizard, { step: 2, total: 6, title: "Android package name", children: /* @__PURE__ */ jsx9(
4791
+ TuiTextInput,
4792
+ {
4793
+ label: "Android package name (e.g. com.example.app):",
4794
+ defaultValue: androidPackageName,
4795
+ error: pkgError,
4796
+ onChange: () => setPkgError(void 0),
4797
+ onSubmitValue: (v) => {
4798
+ const t = v.trim();
4799
+ if (t && !isValidAndroidPackage(t)) {
4800
+ setPkgError("Use reverse-DNS form: com.mycompany.app");
4801
+ return false;
4802
+ }
4803
+ setAndroidPackageName(t);
4804
+ setPkgError(void 0);
4805
+ },
4806
+ onSubmit: () => setStep("android-sdk")
4807
+ }
4808
+ ) });
4809
+ }
4810
+ if (step === "android-sdk") {
4811
+ return /* @__PURE__ */ jsx9(Wizard, { step: 3, total: 6, title: "Android SDK", children: /* @__PURE__ */ jsx9(
4812
+ TuiTextInput,
4813
+ {
4814
+ label: "Android SDK path (e.g. ~/Library/Android/sdk or $ANDROID_HOME):",
4815
+ defaultValue: androidSdk,
4816
+ onSubmitValue: (v) => {
4817
+ const { resolved, message } = resolveSdkInput(v);
4818
+ setAndroidSdk(resolved);
4819
+ setSdkHint(message);
4820
+ },
4821
+ onSubmit: () => setStep("ios-reuse"),
4822
+ hint: sdkHint
4823
+ }
4824
+ ) });
4825
+ }
4826
+ if (step === "ios-reuse") {
4827
+ return /* @__PURE__ */ jsx9(Wizard, { step: 4, total: 6, title: "iOS", children: /* @__PURE__ */ jsx9(
4828
+ TuiConfirmInput,
4829
+ {
4830
+ label: "Use the same app name and bundle ID for iOS as Android?",
4831
+ defaultYes: false,
4832
+ onConfirm: (yes) => {
4833
+ if (yes) {
4834
+ setIosAppName(androidAppName);
4835
+ setIosBundleId(androidPackageName);
4836
+ setStep("lynx-path");
4837
+ } else {
4838
+ setStep("ios-app");
4839
+ }
4840
+ },
4841
+ hint: "No = enter iOS-specific values next"
4842
+ }
4843
+ ) });
4844
+ }
4845
+ if (step === "ios-app") {
4846
+ return /* @__PURE__ */ jsx9(Wizard, { step: 4, total: 6, title: "iOS app name", children: /* @__PURE__ */ jsx9(
4847
+ TuiTextInput,
4848
+ {
4849
+ label: "iOS app name:",
4850
+ defaultValue: iosAppName,
4851
+ onSubmitValue: (v) => setIosAppName(v),
4852
+ onSubmit: () => setStep("ios-bundle")
4853
+ }
4854
+ ) });
4855
+ }
4856
+ if (step === "ios-bundle") {
4857
+ return /* @__PURE__ */ jsx9(Wizard, { step: 5, total: 6, title: "iOS bundle ID", children: /* @__PURE__ */ jsx9(
4858
+ TuiTextInput,
4859
+ {
4860
+ label: "iOS bundle ID (e.g. com.example.app):",
4861
+ defaultValue: iosBundleId,
4862
+ error: bundleError,
4863
+ onChange: () => setBundleError(void 0),
4864
+ onSubmitValue: (v) => {
4865
+ const t = v.trim();
4866
+ if (t && !isValidIosBundleId(t)) {
4867
+ setBundleError("Use reverse-DNS form: com.mycompany.App");
4868
+ return false;
4869
+ }
4870
+ setIosBundleId(t);
4871
+ setBundleError(void 0);
4872
+ },
4873
+ onSubmit: () => setStep("lynx-path")
4874
+ }
4875
+ ) });
4876
+ }
4877
+ if (step === "lynx-path") {
4878
+ return /* @__PURE__ */ jsx9(Wizard, { step: 6, total: 6, title: "Lynx project", children: /* @__PURE__ */ jsx9(
4879
+ TuiTextInput,
4880
+ {
4881
+ label: "Lynx project path relative to project root (optional, e.g. packages/example):",
4882
+ defaultValue: lynxProject,
4883
+ onSubmitValue: (v) => setLynxProject(v),
4884
+ onSubmit: () => setStep("saving"),
4885
+ hint: "Press Enter with empty to skip"
4886
+ }
4887
+ ) });
4489
4888
  }
4490
- rl.close();
4889
+ if (step === "saving") {
4890
+ return /* @__PURE__ */ jsx9(Box8, { children: /* @__PURE__ */ jsx9(TuiSpinner, { label: "Writing tamer.config.json and updating tsconfig\u2026" }) });
4891
+ }
4892
+ if (step === "done") {
4893
+ 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)) }) });
4894
+ }
4895
+ return null;
4896
+ }
4897
+ async function init() {
4898
+ const { waitUntilExit } = render(/* @__PURE__ */ jsx9(InitWizard, {}));
4899
+ await waitUntilExit();
4491
4900
  }
4492
- var init_default = init;
4493
4901
 
4494
4902
  // src/common/create.ts
4495
4903
  import fs18 from "fs";
4496
4904
  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())));
4905
+ import readline from "readline";
4906
+ var rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false });
4907
+ function ask(question) {
4908
+ return new Promise((resolve) => rl.question(question, (answer) => resolve(answer.trim())));
4501
4909
  }
4502
4910
  async function create3(opts) {
4503
4911
  console.log("Tamer4Lynx: Create Lynx Extension\n");
@@ -4536,29 +4944,29 @@ async function create3(opts) {
4536
4944
  console.log(" [ ] Native Module");
4537
4945
  console.log(" [ ] Element");
4538
4946
  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");
4947
+ includeModule = /^y(es)?$/i.test(await ask("Include Native Module? (Y/n): ") || "y");
4948
+ includeElement = /^y(es)?$/i.test(await ask("Include Element? (y/N): ") || "n");
4949
+ includeService = /^y(es)?$/i.test(await ask("Include Service? (y/N): ") || "n");
4542
4950
  }
4543
4951
  if (!includeModule && !includeElement && !includeService) {
4544
4952
  console.error("\u274C At least one extension type is required.");
4545
- rl2.close();
4953
+ rl.close();
4546
4954
  process.exit(1);
4547
4955
  }
4548
- const extName = await ask2("Extension package name (e.g. my-lynx-module): ");
4956
+ const extName = await ask("Extension package name (e.g. my-lynx-module): ");
4549
4957
  if (!extName || !/^[a-z0-9-_]+$/.test(extName)) {
4550
4958
  console.error("\u274C Invalid package name. Use lowercase letters, numbers, hyphens, underscores.");
4551
- rl2.close();
4959
+ rl.close();
4552
4960
  process.exit(1);
4553
4961
  }
4554
- const packageName = await ask2("Android package name (e.g. com.example.mymodule): ") || `com.example.${extName.replace(/-/g, "")}`;
4962
+ const packageName = await ask("Android package name (e.g. com.example.mymodule): ") || `com.example.${extName.replace(/-/g, "")}`;
4555
4963
  const simpleModuleName = extName.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("") + "Module";
4556
4964
  const fullModuleClassName = `${packageName}.${simpleModuleName}`;
4557
4965
  const cwd = process.cwd();
4558
4966
  const root = path19.join(cwd, extName);
4559
4967
  if (fs18.existsSync(root)) {
4560
4968
  console.error(`\u274C Directory ${extName} already exists.`);
4561
- rl2.close();
4969
+ rl.close();
4562
4970
  process.exit(1);
4563
4971
  }
4564
4972
  fs18.mkdirSync(root, { recursive: true });
@@ -4712,7 +5120,7 @@ This package uses \`lynx.ext.json\` (RFC-compliant) for autolinking.
4712
5120
  console.log(` cd ${extName}`);
4713
5121
  console.log(" npm install");
4714
5122
  if (includeModule) console.log(" npm run codegen");
4715
- rl2.close();
5123
+ rl.close();
4716
5124
  }
4717
5125
  var create_default3 = create3;
4718
5126
 
@@ -4783,14 +5191,16 @@ function extractLynxModules(files) {
4783
5191
  }
4784
5192
  var codegen_default = codegen;
4785
5193
 
4786
- // src/common/devServer.ts
5194
+ // src/common/devServer.tsx
5195
+ import { useState as useState5, useEffect as useEffect3, useRef, useCallback as useCallback4 } from "react";
4787
5196
  import { spawn } from "child_process";
4788
5197
  import fs20 from "fs";
4789
5198
  import http from "http";
4790
5199
  import os4 from "os";
4791
5200
  import path21 from "path";
4792
- import readline3 from "readline";
5201
+ import { render as render2, useInput, useApp } from "ink";
4793
5202
  import { WebSocketServer } from "ws";
5203
+ import { jsx as jsx10 } from "react/jsx-runtime";
4794
5204
  var DEFAULT_PORT = 3e3;
4795
5205
  var STATIC_MIME = {
4796
5206
  ".png": "image/png",
@@ -4845,319 +5255,431 @@ function getLanIp() {
4845
5255
  }
4846
5256
  return "localhost";
4847
5257
  }
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");
5258
+ function detectPackageManager(cwd) {
5259
+ const dir = path21.resolve(cwd);
5260
+ if (fs20.existsSync(path21.join(dir, "pnpm-lock.yaml"))) return { cmd: "pnpm", args: ["run", "build"] };
5261
+ if (fs20.existsSync(path21.join(dir, "bun.lockb")) || fs20.existsSync(path21.join(dir, "bun.lock")))
5262
+ return { cmd: "bun", args: ["run", "build"] };
5263
+ return { cmd: "npm", args: ["run", "build"] };
5264
+ }
5265
+ var initialUi = () => ({
5266
+ phase: "starting",
5267
+ projectName: "",
5268
+ port: 0,
5269
+ lanIp: "localhost",
5270
+ devUrl: "",
5271
+ wsUrl: "",
5272
+ lynxBundleFile: "main.lynx.bundle",
5273
+ bonjour: false,
5274
+ verbose: false,
5275
+ buildPhase: "idle",
5276
+ wsConnections: 0,
5277
+ logLines: [],
5278
+ showLogs: false,
5279
+ qrLines: []
5280
+ });
5281
+ function DevServerApp({ verbose }) {
5282
+ const { exit } = useApp();
5283
+ const [ui, setUi] = useState5(() => {
5284
+ const s = initialUi();
5285
+ s.verbose = verbose;
5286
+ return s;
5287
+ });
5288
+ const cleanupRef = useRef(null);
5289
+ const rebuildRef = useRef(() => Promise.resolve());
5290
+ const quitOnceRef = useRef(false);
5291
+ const appendLog = useCallback4((chunk) => {
5292
+ const lines = chunk.split(/\r?\n/).filter(Boolean);
5293
+ setUi((prev) => ({
5294
+ ...prev,
5295
+ logLines: [...prev.logLines, ...lines].slice(-400)
5296
+ }));
5297
+ }, []);
5298
+ const handleQuit = useCallback4(() => {
5299
+ if (quitOnceRef.current) return;
5300
+ quitOnceRef.current = true;
5301
+ void cleanupRef.current?.();
5302
+ exit();
5303
+ }, [exit]);
5304
+ useInput((input, key) => {
5305
+ if (key.ctrl && key.name === "c") {
5306
+ handleQuit();
4906
5307
  return;
4907
5308
  }
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));
5309
+ if (input === "q") {
5310
+ handleQuit();
4942
5311
  return;
4943
5312
  }
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
- });
5313
+ if (input === "r") {
5314
+ void rebuildRef.current();
4955
5315
  return;
4956
5316
  }
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;
5317
+ if (input === "l") {
5318
+ setUi((s) => ({ ...s, showLogs: !s.showLogs }));
5004
5319
  }
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
5320
  });
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();
5023
- }
5024
- });
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) => {
5321
+ useEffect3(() => {
5322
+ const onSig = () => {
5323
+ handleQuit();
5324
+ };
5325
+ process.on("SIGINT", onSig);
5326
+ process.on("SIGTERM", onSig);
5327
+ return () => {
5328
+ process.off("SIGINT", onSig);
5329
+ process.off("SIGTERM", onSig);
5330
+ };
5331
+ }, [handleQuit]);
5332
+ useEffect3(() => {
5333
+ let alive = true;
5334
+ let buildProcess = null;
5335
+ let watcher = null;
5336
+ let stopBonjour;
5337
+ const run = async () => {
5033
5338
  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);
5339
+ const resolved = resolveHostPaths();
5340
+ const { projectRoot, lynxProjectDir, lynxBundlePath, lynxBundleFile, config } = resolved;
5341
+ const distDir = path21.dirname(lynxBundlePath);
5342
+ const projectName = path21.basename(lynxProjectDir);
5343
+ const basePath = `/${projectName}`;
5344
+ setUi((s) => ({ ...s, projectName, lynxBundleFile }));
5345
+ const preferredPort = config.devServer?.port ?? config.devServer?.httpPort ?? DEFAULT_PORT;
5346
+ const port = await findAvailablePort(preferredPort);
5347
+ if (port !== preferredPort) {
5348
+ appendLog(`Port ${preferredPort} in use, using ${port}`);
5042
5349
  }
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);
5350
+ const iconPaths = resolveIconPaths(projectRoot, config);
5351
+ let iconFilePath = null;
5352
+ if (iconPaths?.source && fs20.statSync(iconPaths.source).isFile()) {
5353
+ iconFilePath = iconPaths.source;
5354
+ } else if (iconPaths?.androidAdaptiveForeground && fs20.statSync(iconPaths.androidAdaptiveForeground).isFile()) {
5355
+ iconFilePath = iconPaths.androidAdaptiveForeground;
5356
+ } else if (iconPaths?.android) {
5357
+ const androidIcon = path21.join(iconPaths.android, "mipmap-xxxhdpi", "ic_launcher.png");
5358
+ if (fs20.existsSync(androidIcon)) iconFilePath = androidIcon;
5359
+ } else if (iconPaths?.ios) {
5360
+ const iosIcon = path21.join(iconPaths.ios, "Icon-1024.png");
5361
+ if (fs20.existsSync(iosIcon)) iconFilePath = iosIcon;
5072
5362
  }
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)
5093
- }
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;
5363
+ const iconExt = iconFilePath ? path21.extname(iconFilePath) || ".png" : "";
5364
+ const runBuild = () => {
5365
+ return new Promise((resolve, reject) => {
5366
+ const { cmd, args } = detectPackageManager(lynxProjectDir);
5367
+ buildProcess = spawn(cmd, args, {
5368
+ cwd: lynxProjectDir,
5369
+ stdio: "pipe",
5370
+ shell: process.platform === "win32"
5371
+ });
5372
+ let stderr = "";
5373
+ buildProcess.stdout?.on("data", (d) => {
5374
+ appendLog(d.toString());
5375
+ });
5376
+ buildProcess.stderr?.on("data", (d) => {
5377
+ const t = d.toString();
5378
+ stderr += t;
5379
+ appendLog(t);
5380
+ });
5381
+ buildProcess.on("close", (code) => {
5382
+ buildProcess = null;
5383
+ if (code === 0) resolve();
5384
+ else reject(new Error(stderr || `Build exited ${code}`));
5385
+ });
5386
+ });
5387
+ };
5388
+ const doBuild = async () => {
5389
+ setUi((s) => ({ ...s, buildPhase: "building", buildError: void 0 }));
5390
+ try {
5391
+ await runBuild();
5392
+ if (!alive) return;
5393
+ setUi((s) => ({ ...s, buildPhase: "success" }));
5394
+ } catch (e) {
5395
+ if (!alive) return;
5396
+ const msg = e.message;
5397
+ setUi((s) => ({ ...s, buildPhase: "error", buildError: msg }));
5398
+ throw e;
5399
+ }
5400
+ };
5401
+ const httpSrv = http.createServer((req, res) => {
5402
+ let reqPath = (req.url || "/").split("?")[0];
5403
+ if (reqPath === `${basePath}/status`) {
5404
+ res.setHeader("Content-Type", "text/plain");
5405
+ res.setHeader("Access-Control-Allow-Origin", "*");
5406
+ res.end("packager-status:running");
5407
+ return;
5408
+ }
5409
+ if (reqPath === `${basePath}/meta.json`) {
5410
+ const lanIp2 = getLanIp();
5411
+ const nativeModules = discoverNativeExtensions(projectRoot);
5412
+ const androidPackageName = config.android?.packageName?.trim();
5413
+ const iosBundleId = config.ios?.bundleId?.trim();
5414
+ const idParts = [androidPackageName?.toLowerCase(), iosBundleId?.toLowerCase()].filter(
5415
+ (x) => Boolean(x)
5416
+ );
5417
+ const meta = {
5418
+ name: projectName,
5419
+ slug: projectName,
5420
+ bundleUrl: `http://${lanIp2}:${port}${basePath}/${lynxBundleFile}`,
5421
+ bundleFile: lynxBundleFile,
5422
+ hostUri: `http://${lanIp2}:${port}${basePath}`,
5423
+ debuggerHost: `${lanIp2}:${port}`,
5424
+ developer: { tool: "tamer4lynx" },
5425
+ packagerStatus: "running",
5426
+ nativeModules: nativeModules.map((m) => ({
5427
+ packageName: m.packageName,
5428
+ moduleClassName: m.moduleClassName
5429
+ }))
5430
+ };
5431
+ if (androidPackageName) meta.androidPackageName = androidPackageName;
5432
+ if (iosBundleId) meta.iosBundleId = iosBundleId;
5433
+ if (idParts.length > 0) meta.tamerAppKey = idParts.join("|");
5434
+ const rawIcon = config.icon;
5435
+ if (rawIcon && typeof rawIcon === "object" && "source" in rawIcon && typeof rawIcon.source === "string") {
5436
+ meta.iconSource = rawIcon.source;
5437
+ } else if (typeof rawIcon === "string") {
5438
+ meta.iconSource = rawIcon;
5439
+ }
5440
+ if (iconFilePath) {
5441
+ meta.icon = `http://${lanIp2}:${port}${basePath}/icon${iconExt}`;
5442
+ }
5443
+ res.setHeader("Content-Type", "application/json");
5444
+ res.setHeader("Access-Control-Allow-Origin", "*");
5445
+ res.end(JSON.stringify(meta, null, 2));
5446
+ return;
5447
+ }
5448
+ if (iconFilePath && (reqPath === `${basePath}/icon` || reqPath === `${basePath}/icon${iconExt}`)) {
5449
+ fs20.readFile(iconFilePath, (err, data) => {
5450
+ if (err) {
5451
+ res.writeHead(404);
5452
+ res.end();
5453
+ return;
5454
+ }
5455
+ res.setHeader("Content-Type", STATIC_MIME[iconExt] ?? "image/png");
5456
+ res.setHeader("Access-Control-Allow-Origin", "*");
5457
+ res.end(data);
5458
+ });
5459
+ return;
5460
+ }
5461
+ const lynxStaticMounts = [
5462
+ { prefix: `${basePath}/src/assets/`, rootSub: "src/assets" },
5463
+ { prefix: `${basePath}/assets/`, rootSub: "assets" }
5464
+ ];
5465
+ for (const { prefix, rootSub } of lynxStaticMounts) {
5466
+ if (!reqPath.startsWith(prefix)) continue;
5467
+ let rel = reqPath.slice(prefix.length);
5468
+ try {
5469
+ rel = decodeURIComponent(rel);
5470
+ } catch {
5471
+ res.writeHead(400);
5472
+ res.end();
5473
+ return;
5474
+ }
5475
+ const safe = path21.normalize(rel).replace(/^(\.\.(\/|\\|$))+/, "");
5476
+ if (path21.isAbsolute(safe) || safe.startsWith("..")) {
5477
+ res.writeHead(403);
5478
+ res.end();
5479
+ return;
5480
+ }
5481
+ const allowedRoot = path21.resolve(lynxProjectDir, rootSub);
5482
+ const abs = path21.resolve(allowedRoot, safe);
5483
+ if (!abs.startsWith(allowedRoot + path21.sep) && abs !== allowedRoot) {
5484
+ res.writeHead(403);
5485
+ res.end();
5486
+ return;
5487
+ }
5488
+ if (!fs20.existsSync(abs) || !fs20.statSync(abs).isFile()) {
5489
+ res.writeHead(404);
5490
+ res.end("Not found");
5491
+ return;
5492
+ }
5493
+ sendFileFromDisk(res, abs);
5494
+ return;
5495
+ }
5496
+ if (reqPath === "/" || reqPath === basePath || reqPath === `${basePath}/`) {
5497
+ reqPath = `${basePath}/${lynxBundleFile}`;
5498
+ } else if (!reqPath.startsWith(basePath)) {
5499
+ reqPath = basePath + (reqPath.startsWith("/") ? reqPath : "/" + reqPath);
5500
+ }
5501
+ const relPath = reqPath.replace(basePath, "").replace(/^\//, "") || lynxBundleFile;
5502
+ const filePath = path21.resolve(distDir, relPath);
5503
+ const distResolved = path21.resolve(distDir);
5504
+ if (!filePath.startsWith(distResolved + path21.sep) && filePath !== distResolved) {
5505
+ res.writeHead(403);
5506
+ res.end();
5507
+ return;
5508
+ }
5509
+ fs20.readFile(filePath, (err, data) => {
5510
+ if (err) {
5511
+ res.writeHead(404);
5512
+ res.end("Not found");
5513
+ return;
5514
+ }
5515
+ res.setHeader("Access-Control-Allow-Origin", "*");
5516
+ res.setHeader("Content-Type", reqPath.endsWith(".bundle") ? "application/octet-stream" : "application/javascript");
5517
+ res.end(data);
5518
+ });
5519
+ });
5520
+ const wssInst = new WebSocketServer({ noServer: true });
5521
+ rebuildRef.current = async () => {
5522
+ try {
5523
+ await doBuild();
5524
+ if (!alive) return;
5525
+ wssInst.clients.forEach((client) => {
5526
+ if (client.readyState === 1) client.send(JSON.stringify({ type: "reload" }));
5527
+ });
5528
+ appendLog("Rebuilt, clients notified");
5529
+ } catch {
5530
+ }
5531
+ };
5532
+ httpSrv.on("upgrade", (request, socket, head) => {
5533
+ const p = (request.url || "").split("?")[0];
5534
+ if (p === `${basePath}/__hmr` || p === "/__hmr" || p.endsWith("/__hmr")) {
5535
+ wssInst.handleUpgrade(request, socket, head, (ws) => wssInst.emit("connection", ws, request));
5536
+ } else {
5537
+ socket.destroy();
5538
+ }
5539
+ });
5540
+ wssInst.on("connection", (ws, req) => {
5541
+ const clientIp = req.socket.remoteAddress ?? "unknown";
5542
+ setUi((s) => ({ ...s, wsConnections: s.wsConnections + 1 }));
5543
+ appendLog(`[WS] connected: ${clientIp}`);
5544
+ ws.send(JSON.stringify({ type: "connected" }));
5545
+ ws.on("close", () => {
5546
+ setUi((s) => ({ ...s, wsConnections: Math.max(0, s.wsConnections - 1) }));
5547
+ appendLog(`[WS] disconnected: ${clientIp}`);
5548
+ });
5549
+ ws.on("message", (data) => {
5550
+ try {
5551
+ const msg = JSON.parse(data.toString());
5552
+ if (msg?.type === "console_log" && Array.isArray(msg.message)) {
5553
+ const skip = msg.message.includes("[rspeedy-dev-server]") || msg.message.includes("[HMR]");
5554
+ if (skip) return;
5555
+ const isJs = msg.tag === "lynx-console" || msg.tag == null;
5556
+ if (!verbose && !isJs) return;
5557
+ appendLog(`${isJs ? "[APP]" : "[NATIVE]"} ${msg.message.join(" ")}`);
5558
+ }
5559
+ } catch {
5560
+ }
5561
+ });
5562
+ });
5563
+ let chokidar = null;
5564
+ try {
5565
+ chokidar = await import("chokidar");
5566
+ } catch {
5126
5567
  }
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;
5568
+ if (chokidar) {
5569
+ const watchPaths = [
5570
+ path21.join(lynxProjectDir, "src"),
5571
+ path21.join(lynxProjectDir, "lynx.config.ts"),
5572
+ path21.join(lynxProjectDir, "lynx.config.js")
5573
+ ].filter((p) => fs20.existsSync(p));
5574
+ if (watchPaths.length > 0) {
5575
+ const w = chokidar.watch(watchPaths, { ignoreInitial: true });
5576
+ w.on("change", async () => {
5577
+ try {
5578
+ await rebuildRef.current();
5579
+ } catch {
5580
+ }
5581
+ });
5582
+ watcher = {
5583
+ close: async () => {
5584
+ await w.close();
5585
+ }
5586
+ };
5587
+ }
5142
5588
  }
5143
- });
5589
+ await doBuild();
5590
+ if (!alive) return;
5591
+ await new Promise((listenResolve, listenReject) => {
5592
+ httpSrv.listen(port, "0.0.0.0", () => {
5593
+ listenResolve();
5594
+ });
5595
+ httpSrv.once("error", (err) => listenReject(err));
5596
+ });
5597
+ if (!alive) return;
5598
+ void import("dnssd-advertise").then(({ advertise }) => {
5599
+ stopBonjour = advertise({
5600
+ name: projectName,
5601
+ type: "tamer",
5602
+ protocol: "tcp",
5603
+ port,
5604
+ txt: {
5605
+ name: projectName.slice(0, 255),
5606
+ path: basePath.slice(0, 255)
5607
+ }
5608
+ });
5609
+ setUi((s) => ({ ...s, bonjour: true }));
5610
+ }).catch(() => {
5611
+ });
5612
+ const lanIp = getLanIp();
5613
+ const devUrl = `http://${lanIp}:${port}${basePath}`;
5614
+ const wsUrl = `ws://${lanIp}:${port}${basePath}/__hmr`;
5615
+ setUi((s) => ({
5616
+ ...s,
5617
+ phase: "running",
5618
+ port,
5619
+ lanIp,
5620
+ devUrl,
5621
+ wsUrl
5622
+ }));
5623
+ void import("qrcode-terminal").then((mod) => {
5624
+ const qrcode = mod.default ?? mod;
5625
+ qrcode.generate(devUrl, { small: true }, (qr) => {
5626
+ if (!alive) return;
5627
+ setUi((s) => ({ ...s, qrLines: qr.split("\n").filter(Boolean) }));
5628
+ });
5629
+ }).catch(() => {
5630
+ });
5631
+ cleanupRef.current = async () => {
5632
+ buildProcess?.kill();
5633
+ await watcher?.close().catch(() => {
5634
+ });
5635
+ await stopBonjour?.();
5636
+ httpSrv.close();
5637
+ wssInst.close();
5638
+ };
5639
+ } catch (e) {
5640
+ if (!alive) return;
5641
+ setUi((s) => ({
5642
+ ...s,
5643
+ phase: "failed",
5644
+ startError: e.message
5645
+ }));
5646
+ }
5647
+ };
5648
+ void run();
5649
+ return () => {
5650
+ alive = false;
5651
+ void cleanupRef.current?.();
5652
+ };
5653
+ }, [appendLog, verbose]);
5654
+ return /* @__PURE__ */ jsx10(
5655
+ ServerDashboard,
5656
+ {
5657
+ projectName: ui.projectName,
5658
+ port: ui.port,
5659
+ lanIp: ui.lanIp,
5660
+ devUrl: ui.devUrl,
5661
+ wsUrl: ui.wsUrl,
5662
+ lynxBundleFile: ui.lynxBundleFile,
5663
+ bonjour: ui.bonjour,
5664
+ verbose: ui.verbose,
5665
+ buildPhase: ui.buildPhase,
5666
+ buildError: ui.buildError,
5667
+ wsConnections: ui.wsConnections,
5668
+ logLines: ui.logLines,
5669
+ showLogs: ui.showLogs,
5670
+ qrLines: ui.qrLines,
5671
+ phase: ui.phase,
5672
+ startError: ui.startError
5144
5673
  }
5674
+ );
5675
+ }
5676
+ async function startDevServer(opts) {
5677
+ const verbose = opts?.verbose ?? false;
5678
+ const { waitUntilExit } = render2(/* @__PURE__ */ jsx10(DevServerApp, { verbose }), {
5679
+ exitOnCtrlC: false,
5680
+ patchConsole: false
5145
5681
  });
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
- });
5682
+ await waitUntilExit();
5161
5683
  }
5162
5684
  var devServer_default = startDevServer;
5163
5685
 
@@ -5680,6 +6202,23 @@ var CORE_PACKAGES = [
5680
6202
  "@tamer4lynx/tamer-system-ui",
5681
6203
  "@tamer4lynx/tamer-icons"
5682
6204
  ];
6205
+ var DEV_STACK_PACKAGES = [
6206
+ "@tamer4lynx/jiggle",
6207
+ "@tamer4lynx/tamer-app-shell",
6208
+ "@tamer4lynx/tamer-biometric",
6209
+ "@tamer4lynx/tamer-dev-app",
6210
+ "@tamer4lynx/tamer-dev-client",
6211
+ "@tamer4lynx/tamer-display-browser",
6212
+ "@tamer4lynx/tamer-icons",
6213
+ "@tamer4lynx/tamer-insets",
6214
+ "@tamer4lynx/tamer-linking",
6215
+ "@tamer4lynx/tamer-plugin",
6216
+ "@tamer4lynx/tamer-router",
6217
+ "@tamer4lynx/tamer-screen",
6218
+ "@tamer4lynx/tamer-secure-store",
6219
+ "@tamer4lynx/tamer-system-ui",
6220
+ "@tamer4lynx/tamer-transports"
6221
+ ];
5683
6222
  var PACKAGE_ALIASES = {};
5684
6223
  async function getHighestPublishedVersion(fullName) {
5685
6224
  try {
@@ -5705,7 +6244,7 @@ async function normalizeTamerInstallSpec(pkg) {
5705
6244
  console.warn(`\u26A0\uFE0F Could not resolve published versions for ${pkg}; using @prerelease`);
5706
6245
  return `${pkg}@prerelease`;
5707
6246
  }
5708
- function detectPackageManager(cwd) {
6247
+ function detectPackageManager2(cwd) {
5709
6248
  const dir = path24.resolve(cwd);
5710
6249
  if (fs23.existsSync(path24.join(dir, "pnpm-lock.yaml"))) return "pnpm";
5711
6250
  if (fs23.existsSync(path24.join(dir, "bun.lockb"))) return "bun";
@@ -5718,13 +6257,22 @@ function runInstall(cwd, packages, pm) {
5718
6257
  }
5719
6258
  async function addCore() {
5720
6259
  const { lynxProjectDir } = resolveHostPaths();
5721
- const pm = detectPackageManager(lynxProjectDir);
6260
+ const pm = detectPackageManager2(lynxProjectDir);
5722
6261
  console.log(`Resolving latest published versions (npm)\u2026`);
5723
6262
  const resolved = await Promise.all(CORE_PACKAGES.map(normalizeTamerInstallSpec));
5724
6263
  console.log(`Adding core packages to ${lynxProjectDir} (using ${pm})\u2026`);
5725
6264
  runInstall(lynxProjectDir, resolved, pm);
5726
6265
  console.log("\u2705 Core packages installed. Run `t4l link` to link native modules.");
5727
6266
  }
6267
+ async function addDev() {
6268
+ const { lynxProjectDir } = resolveHostPaths();
6269
+ const pm = detectPackageManager2(lynxProjectDir);
6270
+ console.log(`Resolving latest published versions (npm)\u2026`);
6271
+ const resolved = await Promise.all([...DEV_STACK_PACKAGES].map(normalizeTamerInstallSpec));
6272
+ console.log(`Adding dev stack (${DEV_STACK_PACKAGES.length} @tamer4lynx packages) to ${lynxProjectDir} (using ${pm})\u2026`);
6273
+ runInstall(lynxProjectDir, resolved, pm);
6274
+ console.log("\u2705 Dev stack installed. Run `t4l link` to link native modules.");
6275
+ }
5728
6276
  async function add(packages = []) {
5729
6277
  const list = Array.isArray(packages) ? packages : [];
5730
6278
  if (list.length === 0) {
@@ -5735,7 +6283,7 @@ async function add(packages = []) {
5735
6283
  return;
5736
6284
  }
5737
6285
  const { lynxProjectDir } = resolveHostPaths();
5738
- const pm = detectPackageManager(lynxProjectDir);
6286
+ const pm = detectPackageManager2(lynxProjectDir);
5739
6287
  console.log(`Resolving latest published versions (npm)\u2026`);
5740
6288
  const normalized = await Promise.all(
5741
6289
  list.map(async (p) => {
@@ -5748,18 +6296,682 @@ async function add(packages = []) {
5748
6296
  console.log("\u2705 Packages installed. Run `t4l link` to link native modules.");
5749
6297
  }
5750
6298
 
6299
+ // src/common/signing.tsx
6300
+ import { useState as useState6, useEffect as useEffect4, useRef as useRef2 } from "react";
6301
+ import { render as render3, Text as Text10, Box as Box9 } from "ink";
6302
+ import fs26 from "fs";
6303
+ import path27 from "path";
6304
+
6305
+ // src/common/androidKeystore.ts
6306
+ import { execFileSync } from "child_process";
6307
+ import fs24 from "fs";
6308
+ import path25 from "path";
6309
+ function normalizeJavaHome(raw) {
6310
+ if (!raw) return void 0;
6311
+ const t = raw.trim().replace(/^["']|["']$/g, "");
6312
+ return t || void 0;
6313
+ }
6314
+ function discoverJavaHomeMacOs() {
6315
+ if (process.platform !== "darwin") return void 0;
6316
+ try {
6317
+ const out = execFileSync("/usr/libexec/java_home", [], {
6318
+ encoding: "utf8",
6319
+ stdio: ["pipe", "pipe", "pipe"]
6320
+ }).trim().split("\n")[0]?.trim();
6321
+ if (out && fs24.existsSync(path25.join(out, "bin", "keytool"))) return out;
6322
+ } catch {
6323
+ }
6324
+ return void 0;
6325
+ }
6326
+ function resolveKeytoolPath() {
6327
+ const jh = normalizeJavaHome(process.env.JAVA_HOME);
6328
+ const win = process.platform === "win32";
6329
+ const keytoolName = win ? "keytool.exe" : "keytool";
6330
+ if (jh) {
6331
+ const p = path25.join(jh, "bin", keytoolName);
6332
+ if (fs24.existsSync(p)) return p;
6333
+ }
6334
+ const mac = discoverJavaHomeMacOs();
6335
+ if (mac) {
6336
+ const p = path25.join(mac, "bin", keytoolName);
6337
+ if (fs24.existsSync(p)) return p;
6338
+ }
6339
+ return "keytool";
6340
+ }
6341
+ function keytoolAvailable() {
6342
+ const tryRun = (cmd) => {
6343
+ try {
6344
+ execFileSync(cmd, ["-help"], { stdio: "pipe" });
6345
+ return true;
6346
+ } catch {
6347
+ return false;
6348
+ }
6349
+ };
6350
+ if (tryRun("keytool")) return true;
6351
+ const fromJavaHome = resolveKeytoolPath();
6352
+ if (fromJavaHome !== "keytool" && fs24.existsSync(fromJavaHome)) {
6353
+ return tryRun(fromJavaHome);
6354
+ }
6355
+ return false;
6356
+ }
6357
+ function generateReleaseKeystore(opts) {
6358
+ const keytool = resolveKeytoolPath();
6359
+ const dir = path25.dirname(opts.keystoreAbsPath);
6360
+ fs24.mkdirSync(dir, { recursive: true });
6361
+ if (fs24.existsSync(opts.keystoreAbsPath)) {
6362
+ throw new Error(`Keystore already exists: ${opts.keystoreAbsPath}`);
6363
+ }
6364
+ if (!opts.storePassword || !opts.keyPassword) {
6365
+ throw new Error(
6366
+ "JDK keytool requires a keystore and key password of at least 6 characters for -genkeypair. Enter a password or use an existing keystore."
6367
+ );
6368
+ }
6369
+ const args = [
6370
+ "-genkeypair",
6371
+ "-v",
6372
+ "-keystore",
6373
+ opts.keystoreAbsPath,
6374
+ "-alias",
6375
+ opts.alias,
6376
+ "-keyalg",
6377
+ "RSA",
6378
+ "-keysize",
6379
+ "2048",
6380
+ "-validity",
6381
+ "10000",
6382
+ "-storepass",
6383
+ opts.storePassword,
6384
+ "-keypass",
6385
+ opts.keyPassword,
6386
+ "-dname",
6387
+ opts.dname
6388
+ ];
6389
+ try {
6390
+ execFileSync(keytool, args, { stdio: ["pipe", "pipe", "pipe"] });
6391
+ } catch (e) {
6392
+ const err = e;
6393
+ const fromKeytool = [err.stdout, err.stderr].filter(Boolean).map((b) => Buffer.from(b).toString("utf8")).join("\n").trim();
6394
+ throw new Error(fromKeytool || err.message || "keytool failed");
6395
+ }
6396
+ }
6397
+
6398
+ // src/common/appendEnvFile.ts
6399
+ import fs25 from "fs";
6400
+ import path26 from "path";
6401
+ import { parse } from "dotenv";
6402
+ function keysDefinedInFile(filePath) {
6403
+ if (!fs25.existsSync(filePath)) return /* @__PURE__ */ new Set();
6404
+ try {
6405
+ return new Set(Object.keys(parse(fs25.readFileSync(filePath, "utf8"))));
6406
+ } catch {
6407
+ return /* @__PURE__ */ new Set();
6408
+ }
6409
+ }
6410
+ function formatEnvLine(key, value) {
6411
+ if (/[\r\n]/.test(value) || /^\s|\s$/.test(value) || /[#"'\\=]/.test(value)) {
6412
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
6413
+ return `${key}="${escaped}"`;
6414
+ }
6415
+ return `${key}=${value}`;
6416
+ }
6417
+ function appendEnvVarsIfMissing(projectRoot, vars) {
6418
+ const entries = Object.entries(vars).filter(([, v]) => v !== void 0 && v !== "");
6419
+ if (entries.length === 0) return null;
6420
+ const envLocal = path26.join(projectRoot, ".env.local");
6421
+ const envDefault = path26.join(projectRoot, ".env");
6422
+ let target;
6423
+ if (fs25.existsSync(envLocal)) target = envLocal;
6424
+ else if (fs25.existsSync(envDefault)) target = envDefault;
6425
+ else target = envLocal;
6426
+ const existing = keysDefinedInFile(target);
6427
+ const lines = [];
6428
+ const appendedKeys = [];
6429
+ for (const [k, v] of entries) {
6430
+ if (existing.has(k)) continue;
6431
+ lines.push(formatEnvLine(k, v));
6432
+ appendedKeys.push(k);
6433
+ }
6434
+ if (lines.length === 0) {
6435
+ return {
6436
+ file: path26.basename(target),
6437
+ keys: [],
6438
+ skippedAll: entries.length > 0
6439
+ };
6440
+ }
6441
+ let prefix = "";
6442
+ if (fs25.existsSync(target)) {
6443
+ const cur = fs25.readFileSync(target, "utf8");
6444
+ prefix = cur.length === 0 ? "" : cur.endsWith("\n") ? cur : `${cur}
6445
+ `;
6446
+ }
6447
+ const block = lines.join("\n") + "\n";
6448
+ fs25.writeFileSync(target, prefix + block, "utf8");
6449
+ return { file: path26.basename(target), keys: appendedKeys };
6450
+ }
6451
+
6452
+ // src/common/signing.tsx
6453
+ import { Fragment, jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
6454
+ function AndroidKeystoreModeSelect({
6455
+ onSelect
6456
+ }) {
6457
+ const canGen = keytoolAvailable();
6458
+ const items = canGen ? [
6459
+ { label: "Generate a new release keystore (JDK keytool)", value: "generate" },
6460
+ { label: "Use an existing keystore file", value: "existing" }
6461
+ ] : [
6462
+ {
6463
+ label: "Use an existing keystore file (install a JDK for keytool to generate)",
6464
+ value: "existing"
6465
+ }
6466
+ ];
6467
+ return /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", children: [
6468
+ /* @__PURE__ */ jsx11(
6469
+ TuiSelectInput,
6470
+ {
6471
+ label: "Android release keystore:",
6472
+ items,
6473
+ onSelect
6474
+ }
6475
+ ),
6476
+ !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." })
6477
+ ] });
6478
+ }
6479
+ function firstStepForPlatform(p) {
6480
+ if (p === "ios") return "ios-team";
6481
+ if (p === "android" || p === "both") return "android-keystore-mode";
6482
+ return "platform";
6483
+ }
6484
+ function SigningWizard({ platform: initialPlatform }) {
6485
+ const [state, setState] = useState6({
6486
+ platform: initialPlatform || null,
6487
+ android: {
6488
+ keystoreFile: "",
6489
+ keyAlias: "release",
6490
+ storePasswordEnv: "ANDROID_KEYSTORE_PASSWORD",
6491
+ keyPasswordEnv: "ANDROID_KEY_PASSWORD",
6492
+ keystoreMode: null,
6493
+ genKeystorePath: "android/release.keystore",
6494
+ genPassword: ""
6495
+ },
6496
+ ios: {
6497
+ developmentTeam: "",
6498
+ codeSignIdentity: "",
6499
+ provisioningProfileSpecifier: ""
6500
+ },
6501
+ step: initialPlatform ? firstStepForPlatform(initialPlatform) : "platform",
6502
+ generateError: null,
6503
+ androidEnvAppend: null
6504
+ });
6505
+ const nextStep = () => {
6506
+ setState((s) => {
6507
+ if (s.step === "android-gen-path") {
6508
+ return { ...s, step: "android-gen-alias" };
6509
+ }
6510
+ if (s.step === "android-gen-alias") {
6511
+ return { ...s, step: "android-gen-password" };
6512
+ }
6513
+ if (s.step === "android-keystore") {
6514
+ return { ...s, step: "android-alias" };
6515
+ }
6516
+ if (s.step === "android-alias") {
6517
+ return { ...s, step: "android-password-env" };
6518
+ }
6519
+ if (s.step === "android-password-env") {
6520
+ return { ...s, step: "android-key-password-env" };
6521
+ }
6522
+ if (s.step === "android-key-password-env") {
6523
+ if (s.platform === "both") {
6524
+ return { ...s, step: "ios-team" };
6525
+ }
6526
+ return { ...s, step: "saving" };
6527
+ }
6528
+ if (s.step === "ios-team") {
6529
+ return { ...s, step: "ios-identity" };
6530
+ }
6531
+ if (s.step === "ios-identity") {
6532
+ return { ...s, step: "ios-profile" };
6533
+ }
6534
+ if (s.step === "ios-profile") {
6535
+ return { ...s, step: "saving" };
6536
+ }
6537
+ return s;
6538
+ });
6539
+ };
6540
+ useEffect4(() => {
6541
+ if (state.step === "saving") {
6542
+ saveConfig();
6543
+ }
6544
+ }, [state.step]);
6545
+ const generateRunId = useRef2(0);
6546
+ useEffect4(() => {
6547
+ if (state.step !== "android-generating") return;
6548
+ const runId = ++generateRunId.current;
6549
+ let cancelled = false;
6550
+ const run = () => {
6551
+ let abs = "";
6552
+ try {
6553
+ const resolved = resolveHostPaths();
6554
+ const rel = state.android.genKeystorePath.trim() || "android/release.keystore";
6555
+ abs = path27.isAbsolute(rel) ? rel : path27.join(resolved.projectRoot, rel);
6556
+ const alias = state.android.keyAlias.trim() || "release";
6557
+ const pw = state.android.genPassword;
6558
+ const pkg = resolved.config.android?.packageName ?? "com.example.app";
6559
+ const safeOU = pkg.replace(/[,=+]/g, "_");
6560
+ const dname = `CN=Android Release, OU=${safeOU}, O=Android, C=US`;
6561
+ generateReleaseKeystore({
6562
+ keystoreAbsPath: abs,
6563
+ alias,
6564
+ storePassword: pw,
6565
+ keyPassword: pw,
6566
+ dname
6567
+ });
6568
+ if (cancelled || runId !== generateRunId.current) return;
6569
+ setState((s) => ({
6570
+ ...s,
6571
+ android: {
6572
+ ...s.android,
6573
+ keystoreFile: rel,
6574
+ keyAlias: alias,
6575
+ keystoreMode: "generate"
6576
+ },
6577
+ step: "android-password-env",
6578
+ generateError: null
6579
+ }));
6580
+ } catch (e) {
6581
+ const msg = e.message;
6582
+ if (abs && fs26.existsSync(abs) && (msg.includes("already exists") || msg.includes("Keystore already exists"))) {
6583
+ if (cancelled || runId !== generateRunId.current) return;
6584
+ const rel = state.android.genKeystorePath.trim() || "android/release.keystore";
6585
+ const alias = state.android.keyAlias.trim() || "release";
6586
+ setState((s) => ({
6587
+ ...s,
6588
+ android: {
6589
+ ...s.android,
6590
+ keystoreFile: rel,
6591
+ keyAlias: alias,
6592
+ keystoreMode: "generate"
6593
+ },
6594
+ step: "android-password-env",
6595
+ generateError: null
6596
+ }));
6597
+ return;
6598
+ }
6599
+ if (cancelled || runId !== generateRunId.current) return;
6600
+ setState((s) => ({
6601
+ ...s,
6602
+ step: "android-gen-password",
6603
+ generateError: msg
6604
+ }));
6605
+ }
6606
+ };
6607
+ run();
6608
+ return () => {
6609
+ cancelled = true;
6610
+ };
6611
+ }, [state.step, state.android.genKeystorePath, state.android.keyAlias, state.android.genPassword]);
6612
+ useEffect4(() => {
6613
+ if (state.step === "done") {
6614
+ setTimeout(() => {
6615
+ process.exit(0);
6616
+ }, 3e3);
6617
+ }
6618
+ }, [state.step]);
6619
+ const saveConfig = async () => {
6620
+ try {
6621
+ const resolved = resolveHostPaths();
6622
+ const configPath = path27.join(resolved.projectRoot, "tamer.config.json");
6623
+ let config = {};
6624
+ let androidEnvAppend = null;
6625
+ if (fs26.existsSync(configPath)) {
6626
+ config = JSON.parse(fs26.readFileSync(configPath, "utf8"));
6627
+ }
6628
+ if (state.platform === "android" || state.platform === "both") {
6629
+ config.android = config.android || {};
6630
+ config.android.signing = {
6631
+ keystoreFile: state.android.keystoreFile,
6632
+ keyAlias: state.android.keyAlias,
6633
+ storePasswordEnv: state.android.storePasswordEnv,
6634
+ keyPasswordEnv: state.android.keyPasswordEnv
6635
+ };
6636
+ if (state.android.keystoreMode === "generate" && state.android.genPassword) {
6637
+ const storeEnv = state.android.storePasswordEnv.trim() || "ANDROID_KEYSTORE_PASSWORD";
6638
+ const keyEnv = state.android.keyPasswordEnv.trim() || "ANDROID_KEY_PASSWORD";
6639
+ androidEnvAppend = appendEnvVarsIfMissing(resolved.projectRoot, {
6640
+ [storeEnv]: state.android.genPassword,
6641
+ [keyEnv]: state.android.genPassword
6642
+ });
6643
+ }
6644
+ }
6645
+ if (state.platform === "ios" || state.platform === "both") {
6646
+ config.ios = config.ios || {};
6647
+ config.ios.signing = {
6648
+ developmentTeam: state.ios.developmentTeam,
6649
+ ...state.ios.codeSignIdentity && { codeSignIdentity: state.ios.codeSignIdentity },
6650
+ ...state.ios.provisioningProfileSpecifier && { provisioningProfileSpecifier: state.ios.provisioningProfileSpecifier }
6651
+ };
6652
+ }
6653
+ fs26.writeFileSync(configPath, JSON.stringify(config, null, 2));
6654
+ const gitignorePath = path27.join(resolved.projectRoot, ".gitignore");
6655
+ if (fs26.existsSync(gitignorePath)) {
6656
+ let gitignore = fs26.readFileSync(gitignorePath, "utf8");
6657
+ const additions = [
6658
+ ".env.local",
6659
+ "*.jks",
6660
+ "*.keystore"
6661
+ ];
6662
+ for (const addition of additions) {
6663
+ if (!gitignore.includes(addition)) {
6664
+ gitignore += `
6665
+ ${addition}
6666
+ `;
6667
+ }
6668
+ }
6669
+ fs26.writeFileSync(gitignorePath, gitignore);
6670
+ }
6671
+ setState((s) => ({
6672
+ ...s,
6673
+ step: "done",
6674
+ androidEnvAppend: state.platform === "android" || state.platform === "both" ? androidEnvAppend : null
6675
+ }));
6676
+ } catch (error) {
6677
+ console.error("Error saving config:", error);
6678
+ process.exit(1);
6679
+ }
6680
+ };
6681
+ if (state.step === "done") {
6682
+ return /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", children: [
6683
+ /* @__PURE__ */ jsx11(Text10, { color: "green", children: "\u2705 Signing configuration saved to tamer.config.json" }),
6684
+ (state.platform === "android" || state.platform === "both") && /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", marginTop: 1, children: [
6685
+ /* @__PURE__ */ jsx11(Text10, { children: "Android signing configured:" }),
6686
+ /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
6687
+ " Keystore: ",
6688
+ state.android.keystoreFile
6689
+ ] }),
6690
+ /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
6691
+ " Alias: ",
6692
+ state.android.keyAlias
6693
+ ] }),
6694
+ state.androidEnvAppend?.keys.length ? /* @__PURE__ */ jsxs10(Text10, { children: [
6695
+ "Appended ",
6696
+ state.androidEnvAppend.keys.join(", "),
6697
+ " to ",
6698
+ state.androidEnvAppend.file,
6699
+ " (existing keys left unchanged)."
6700
+ ] }) : state.androidEnvAppend?.skippedAll ? /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
6701
+ state.androidEnvAppend.file,
6702
+ " already defines the signing env vars; left unchanged."
6703
+ ] }) : /* @__PURE__ */ jsxs10(Fragment, { children: [
6704
+ /* @__PURE__ */ jsx11(Text10, { children: "Set environment variables (or add them to .env / .env.local):" }),
6705
+ /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
6706
+ " export ",
6707
+ state.android.storePasswordEnv,
6708
+ '="your-keystore-password"'
6709
+ ] }),
6710
+ /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
6711
+ " export ",
6712
+ state.android.keyPasswordEnv,
6713
+ '="your-key-password"'
6714
+ ] })
6715
+ ] })
6716
+ ] }),
6717
+ (state.platform === "ios" || state.platform === "both") && /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", marginTop: 1, children: [
6718
+ /* @__PURE__ */ jsx11(Text10, { children: "iOS signing configured:" }),
6719
+ /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
6720
+ " Team ID: ",
6721
+ state.ios.developmentTeam
6722
+ ] }),
6723
+ state.ios.codeSignIdentity && /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
6724
+ " Identity: ",
6725
+ state.ios.codeSignIdentity
6726
+ ] })
6727
+ ] }),
6728
+ /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", marginTop: 1, children: [
6729
+ state.platform === "android" && /* @__PURE__ */ jsxs10(Fragment, { children: [
6730
+ /* @__PURE__ */ jsx11(Text10, { children: "Run `t4l build android -p` to build this platform with signing." }),
6731
+ /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: "`t4l build -p` (no platform) builds both Android and iOS." })
6732
+ ] }),
6733
+ state.platform === "ios" && /* @__PURE__ */ jsxs10(Fragment, { children: [
6734
+ /* @__PURE__ */ jsx11(Text10, { children: "Run `t4l build ios -p` to build this platform with signing." }),
6735
+ /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: "`t4l build -p` (no platform) builds both Android and iOS." })
6736
+ ] }),
6737
+ state.platform === "both" && /* @__PURE__ */ jsxs10(Fragment, { children: [
6738
+ /* @__PURE__ */ jsx11(Text10, { children: "Run `t4l build -p` to build both platforms with signing." }),
6739
+ /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: "Or: `t4l build android -p` / `t4l build ios -p` for one platform." })
6740
+ ] })
6741
+ ] })
6742
+ ] });
6743
+ }
6744
+ if (state.step === "saving") {
6745
+ return /* @__PURE__ */ jsx11(Box9, { children: /* @__PURE__ */ jsx11(TuiSpinner, { label: "Saving configuration..." }) });
6746
+ }
6747
+ if (state.step === "android-generating") {
6748
+ return /* @__PURE__ */ jsx11(Box9, { flexDirection: "column", children: /* @__PURE__ */ jsx11(TuiSpinner, { label: "Running keytool to create release keystore..." }) });
6749
+ }
6750
+ return /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", children: [
6751
+ state.step === "platform" && /* @__PURE__ */ jsx11(
6752
+ TuiSelectInput,
6753
+ {
6754
+ label: "Select platform(s) to configure signing:",
6755
+ items: [
6756
+ { label: "Android", value: "android" },
6757
+ { label: "iOS", value: "ios" },
6758
+ { label: "Both", value: "both" }
6759
+ ],
6760
+ onSelect: (platform) => {
6761
+ setState((s) => ({ ...s, platform, step: firstStepForPlatform(platform) }));
6762
+ }
6763
+ }
6764
+ ),
6765
+ state.step === "android-keystore-mode" && /* @__PURE__ */ jsx11(
6766
+ AndroidKeystoreModeSelect,
6767
+ {
6768
+ onSelect: (mode) => {
6769
+ setState((s) => ({
6770
+ ...s,
6771
+ android: { ...s.android, keystoreMode: mode },
6772
+ step: mode === "generate" ? "android-gen-path" : "android-keystore",
6773
+ generateError: null
6774
+ }));
6775
+ }
6776
+ }
6777
+ ),
6778
+ state.step === "android-gen-path" && /* @__PURE__ */ jsx11(
6779
+ TuiTextInput,
6780
+ {
6781
+ label: "Keystore output path (relative to project root):",
6782
+ defaultValue: state.android.genKeystorePath,
6783
+ onSubmitValue: (v) => {
6784
+ const p = v.trim() || "android/release.keystore";
6785
+ setState((s) => ({ ...s, android: { ...s.android, genKeystorePath: p } }));
6786
+ },
6787
+ onSubmit: nextStep,
6788
+ hint: "Default: android/release.keystore (gitignored pattern *.keystore)"
6789
+ }
6790
+ ),
6791
+ state.step === "android-gen-alias" && /* @__PURE__ */ jsx11(
6792
+ TuiTextInput,
6793
+ {
6794
+ label: "Android key alias:",
6795
+ defaultValue: state.android.keyAlias,
6796
+ onSubmitValue: (v) => {
6797
+ setState((s) => ({ ...s, android: { ...s.android, keyAlias: v } }));
6798
+ },
6799
+ onSubmit: nextStep
6800
+ }
6801
+ ),
6802
+ state.step === "android-gen-password" && /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", children: [
6803
+ state.generateError ? /* @__PURE__ */ jsx11(Text10, { color: "red", children: state.generateError }) : null,
6804
+ /* @__PURE__ */ jsx11(
6805
+ TuiTextInput,
6806
+ {
6807
+ label: "Keystore and key password (same for both; shown as you type):",
6808
+ value: state.android.genPassword,
6809
+ onChange: (v) => setState((s) => ({ ...s, android: { ...s.android, genPassword: v } })),
6810
+ onSubmitValue: (pw) => {
6811
+ setState((s) => ({
6812
+ ...s,
6813
+ android: { ...s.android, genPassword: pw.trim() },
6814
+ step: "android-generating",
6815
+ generateError: null
6816
+ }));
6817
+ },
6818
+ onSubmit: () => {
6819
+ },
6820
+ hint: "At least 6 characters (JDK keytool). Same value used for -storepass and -keypass."
6821
+ }
6822
+ )
6823
+ ] }),
6824
+ state.step === "android-keystore" && /* @__PURE__ */ jsx11(
6825
+ TuiTextInput,
6826
+ {
6827
+ label: "Android keystore file path (relative to project root or android/):",
6828
+ defaultValue: state.android.keystoreFile,
6829
+ onSubmitValue: (v) => {
6830
+ setState((s) => ({ ...s, android: { ...s.android, keystoreFile: v } }));
6831
+ },
6832
+ onSubmit: nextStep,
6833
+ hint: "Example: android/app/my-release-key.keystore or ./my-release-key.keystore"
6834
+ }
6835
+ ),
6836
+ state.step === "android-alias" && /* @__PURE__ */ jsx11(
6837
+ TuiTextInput,
6838
+ {
6839
+ label: "Android key alias:",
6840
+ defaultValue: state.android.keyAlias,
6841
+ onSubmitValue: (v) => {
6842
+ setState((s) => ({ ...s, android: { ...s.android, keyAlias: v } }));
6843
+ },
6844
+ onSubmit: nextStep
6845
+ }
6846
+ ),
6847
+ state.step === "android-password-env" && /* @__PURE__ */ jsx11(
6848
+ TuiTextInput,
6849
+ {
6850
+ label: "Keystore password environment variable name:",
6851
+ defaultValue: state.android.storePasswordEnv || "ANDROID_KEYSTORE_PASSWORD",
6852
+ onSubmitValue: (v) => {
6853
+ setState((s) => ({ ...s, android: { ...s.android, storePasswordEnv: v } }));
6854
+ },
6855
+ onSubmit: () => {
6856
+ setState((s) => ({ ...s, step: "android-key-password-env" }));
6857
+ },
6858
+ hint: "Default: ANDROID_KEYSTORE_PASSWORD (will be written to .env / .env.local)"
6859
+ }
6860
+ ),
6861
+ state.step === "android-key-password-env" && /* @__PURE__ */ jsx11(
6862
+ TuiTextInput,
6863
+ {
6864
+ label: "Key password environment variable name:",
6865
+ defaultValue: state.android.keyPasswordEnv || "ANDROID_KEY_PASSWORD",
6866
+ onSubmitValue: (v) => {
6867
+ setState((s) => ({ ...s, android: { ...s.android, keyPasswordEnv: v } }));
6868
+ },
6869
+ onSubmit: () => {
6870
+ if (state.platform === "both") {
6871
+ setState((s) => ({ ...s, step: "ios-team" }));
6872
+ } else {
6873
+ setState((s) => ({ ...s, step: "saving" }));
6874
+ }
6875
+ },
6876
+ hint: "Default: ANDROID_KEY_PASSWORD (will be written to .env / .env.local)"
6877
+ }
6878
+ ),
6879
+ state.step === "ios-team" && /* @__PURE__ */ jsx11(
6880
+ TuiTextInput,
6881
+ {
6882
+ label: "iOS Development Team ID:",
6883
+ defaultValue: state.ios.developmentTeam,
6884
+ onSubmitValue: (v) => {
6885
+ setState((s) => ({ ...s, ios: { ...s.ios, developmentTeam: v } }));
6886
+ },
6887
+ onSubmit: nextStep,
6888
+ hint: "Example: ABC123DEF4 (found in Apple Developer account)"
6889
+ }
6890
+ ),
6891
+ state.step === "ios-identity" && /* @__PURE__ */ jsx11(
6892
+ TuiTextInput,
6893
+ {
6894
+ label: "iOS Code Sign Identity (optional, press Enter to skip):",
6895
+ defaultValue: state.ios.codeSignIdentity,
6896
+ onSubmitValue: (v) => {
6897
+ setState((s) => ({ ...s, ios: { ...s.ios, codeSignIdentity: v } }));
6898
+ },
6899
+ onSubmit: () => {
6900
+ setState((s) => ({ ...s, step: "ios-profile" }));
6901
+ },
6902
+ hint: 'Example: "iPhone Developer" or "Apple Development"'
6903
+ }
6904
+ ),
6905
+ state.step === "ios-profile" && /* @__PURE__ */ jsx11(
6906
+ TuiTextInput,
6907
+ {
6908
+ label: "iOS Provisioning Profile Specifier (optional, press Enter to skip):",
6909
+ defaultValue: state.ios.provisioningProfileSpecifier,
6910
+ onSubmitValue: (v) => {
6911
+ setState((s) => ({ ...s, ios: { ...s.ios, provisioningProfileSpecifier: v } }));
6912
+ },
6913
+ onSubmit: () => {
6914
+ setState((s) => ({ ...s, step: "saving" }));
6915
+ },
6916
+ hint: "UUID of the provisioning profile"
6917
+ }
6918
+ )
6919
+ ] });
6920
+ }
6921
+ async function signing(platform) {
6922
+ const { waitUntilExit } = render3(/* @__PURE__ */ jsx11(SigningWizard, { platform }));
6923
+ await waitUntilExit();
6924
+ }
6925
+
6926
+ // src/common/productionSigning.ts
6927
+ import fs27 from "fs";
6928
+ import path28 from "path";
6929
+ function isAndroidSigningConfigured(resolved) {
6930
+ const signing2 = resolved.config.android?.signing;
6931
+ const hasConfig = Boolean(signing2?.keystoreFile?.trim() && signing2?.keyAlias?.trim());
6932
+ const signingProps = path28.join(resolved.androidDir, "signing.properties");
6933
+ const hasProps = fs27.existsSync(signingProps);
6934
+ return hasConfig || hasProps;
6935
+ }
6936
+ function isIosSigningConfigured(resolved) {
6937
+ const team = resolved.config.ios?.signing?.developmentTeam?.trim();
6938
+ return Boolean(team);
6939
+ }
6940
+ function assertProductionSigningReady(filter) {
6941
+ const resolved = resolveHostPaths();
6942
+ const needAndroid = filter === "android" || filter === "all";
6943
+ const needIos = filter === "ios" || filter === "all";
6944
+ const missing = [];
6945
+ if (needAndroid && !isAndroidSigningConfigured(resolved)) {
6946
+ missing.push("Android: run `t4l signing android`, then `t4l build android -p`.");
6947
+ }
6948
+ if (needIos && !isIosSigningConfigured(resolved)) {
6949
+ missing.push("iOS: run `t4l signing ios`, then `t4l build ios -p`.");
6950
+ }
6951
+ if (missing.length === 0) return;
6952
+ console.error("\n\u274C Production build (`-p`) needs signing configured for the platform(s) you are building.");
6953
+ for (const line of missing) {
6954
+ console.error(` ${line}`);
6955
+ }
6956
+ console.error(
6957
+ "\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"
6958
+ );
6959
+ process.exit(1);
6960
+ }
6961
+
5751
6962
  // index.ts
5752
6963
  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;
6964
+ const root = path29.dirname(fileURLToPath(import.meta.url));
6965
+ const here = path29.join(root, "package.json");
6966
+ const parent = path29.join(root, "..", "package.json");
6967
+ const pkgPath = fs28.existsSync(here) ? here : parent;
6968
+ return JSON.parse(fs28.readFileSync(pkgPath, "utf8")).version;
5758
6969
  }
5759
6970
  var version = readCliVersion();
5760
- function validateDebugRelease(debug, release) {
5761
- if (debug && release) {
5762
- console.error("Cannot use --debug and --release together.");
6971
+ function validateBuildMode(debug, release, production) {
6972
+ const modes = [debug, release, production].filter(Boolean).length;
6973
+ if (modes > 1) {
6974
+ console.error("Cannot use --debug, --release, and --production together. Use only one.");
5763
6975
  process.exit(1);
5764
6976
  }
5765
6977
  }
@@ -5771,7 +6983,7 @@ function parsePlatform(value) {
5771
6983
  }
5772
6984
  program.version(version).description("Tamer4Lynx CLI - A tool for managing Lynx projects");
5773
6985
  program.command("init").description("Initialize tamer.config.json interactively").action(() => {
5774
- init_default();
6986
+ init();
5775
6987
  });
5776
6988
  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
6989
  const t = target.toLowerCase();
@@ -5794,22 +7006,26 @@ program.command("create <target>").description("Create a project or extension. T
5794
7006
  console.error(`Invalid create target: ${target}. Use ios | android | module | element | service | combo`);
5795
7007
  process.exit(1);
5796
7008
  });
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;
7009
+ 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) => {
7010
+ validateBuildMode(opts.debug, opts.release, opts.production);
7011
+ const release = opts.release === true || opts.production === true;
7012
+ const production = opts.production === true;
5800
7013
  if (opts.embeddable) {
5801
7014
  await buildEmbeddable({ release: true });
5802
7015
  return;
5803
7016
  }
5804
7017
  const p = parsePlatform(platform ?? "all") ?? "all";
7018
+ if (production) {
7019
+ assertProductionSigningReady(p);
7020
+ }
5805
7021
  if (p === "android" || p === "all") {
5806
- await build_default({ install: opts.install, release });
7022
+ await build_default({ install: opts.install, release, production });
5807
7023
  }
5808
7024
  if (p === "ios" || p === "all") {
5809
- await build_default2({ install: opts.install, release });
7025
+ await build_default2({ install: opts.install, release, production });
5810
7026
  }
5811
7027
  });
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) => {
7028
+ program.command("link [platform]").description("Link native modules. Platform: ios | android | both (default: both)").option("-s, --silent", "Run without output").action((platform, opts) => {
5813
7029
  if (opts.silent) {
5814
7030
  console.log = () => {
5815
7031
  };
@@ -5830,14 +7046,15 @@ program.command("link [platform]").description("Link native modules. Platform: i
5830
7046
  autolink_default2();
5831
7047
  autolink_default();
5832
7048
  });
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;
7049
+ 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) => {
7050
+ validateBuildMode(opts.debug, opts.release, opts.production);
7051
+ const release = opts.release === true || opts.production === true;
7052
+ const production = opts.production === true;
5836
7053
  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 });
7054
+ if (p === "android" || p === "all") await bundle_default({ release, production });
7055
+ if (p === "ios" || p === "all") bundle_default2({ release, production });
5839
7056
  });
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) => {
7057
+ 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
7058
  const p = platform?.toLowerCase();
5842
7059
  if (p === "ios") {
5843
7060
  await injectHostIos({ force: opts.force });
@@ -5850,7 +7067,7 @@ program.command("inject <platform>").description("Inject tamer-host templates in
5850
7067
  console.error(`Invalid inject platform: ${platform}. Use ios | android`);
5851
7068
  process.exit(1);
5852
7069
  });
5853
- program.command("sync [platform]").description("Sync dev client files from tamer.config.json. Platform: android (default)").action(async (platform) => {
7070
+ program.command("sync [platform]").description("Sync dev client. Platform: android (default)").action(async (platform) => {
5854
7071
  const p = (platform ?? "android").toLowerCase();
5855
7072
  if (p !== "android") {
5856
7073
  console.error("sync only supports android.");
@@ -5871,16 +7088,27 @@ program.command("build-dev-app").option("-p, --platform <platform>", "Platform:
5871
7088
  await build_default2({ install: opts.install, release: false });
5872
7089
  }
5873
7090
  });
5874
- program.command("add [packages...]").description("Add @tamer4lynx packages to the Lynx project. Future: will track versions for compatibility (Expo-style).").action(async (packages) => {
7091
+ program.command("add [packages...]").description("Add @tamer4lynx packages to the Lynx project").action(async (packages) => {
5875
7092
  await add(packages);
5876
7093
  });
5877
- program.command("add-core").description("Add core packages (app-shell, screen, router, insets, transports, system-ui, icons)").action(async () => {
7094
+ program.command("add-core").description("Add core packages").action(async () => {
5878
7095
  await addCore();
5879
7096
  });
7097
+ program.command("add-dev").description("Add dev-app, dev-client, and their dependencies").action(async () => {
7098
+ await addDev();
7099
+ });
7100
+ program.command("signing [platform]").description("Configure Android and iOS signing interactively").action(async (platform) => {
7101
+ const p = platform?.toLowerCase();
7102
+ if (p === "android" || p === "ios") {
7103
+ await signing(p);
7104
+ } else {
7105
+ await signing();
7106
+ }
7107
+ });
5880
7108
  program.command("codegen").description("Generate code from @lynxmodule declarations").action(() => {
5881
7109
  codegen_default();
5882
7110
  });
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) => {
7111
+ 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
7112
  const sub = subcommand?.toLowerCase();
5885
7113
  if (sub === "create") {
5886
7114
  if (opts.debug && opts.release) {
@@ -5895,14 +7123,19 @@ program.command("android <subcommand>").description("(Legacy) Use: t4l <command>
5895
7123
  return;
5896
7124
  }
5897
7125
  if (sub === "bundle") {
5898
- validateDebugRelease(opts.debug, opts.release);
5899
- await bundle_default({ release: opts.release === true });
7126
+ validateBuildMode(opts.debug, opts.release, opts.production);
7127
+ const release = opts.release === true || opts.production === true;
7128
+ await bundle_default({ release, production: opts.production === true });
5900
7129
  return;
5901
7130
  }
5902
7131
  if (sub === "build") {
5903
- validateDebugRelease(opts.debug, opts.release);
7132
+ validateBuildMode(opts.debug, opts.release, opts.production);
7133
+ const release = opts.release === true || opts.production === true;
5904
7134
  if (opts.embeddable) await buildEmbeddable({ release: true });
5905
- else await build_default({ install: opts.install, release: opts.release === true });
7135
+ else {
7136
+ if (opts.production === true) assertProductionSigningReady("android");
7137
+ await build_default({ install: opts.install, release, production: opts.production === true });
7138
+ }
5906
7139
  return;
5907
7140
  }
5908
7141
  if (sub === "sync") {
@@ -5916,7 +7149,7 @@ program.command("android <subcommand>").description("(Legacy) Use: t4l <command>
5916
7149
  console.error(`Unknown android subcommand: ${subcommand}. Use: create | link | bundle | build | sync | inject`);
5917
7150
  process.exit(1);
5918
7151
  });
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) => {
7152
+ 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
7153
  const sub = subcommand?.toLowerCase();
5921
7154
  if (sub === "create") {
5922
7155
  create_default2();
@@ -5927,14 +7160,19 @@ program.command("ios <subcommand>").description("(Legacy) Use: t4l <command> ios
5927
7160
  return;
5928
7161
  }
5929
7162
  if (sub === "bundle") {
5930
- validateDebugRelease(opts.debug, opts.release);
5931
- bundle_default2({ release: opts.release === true });
7163
+ validateBuildMode(opts.debug, opts.release, opts.production);
7164
+ const release = opts.release === true || opts.production === true;
7165
+ bundle_default2({ release, production: opts.production === true });
5932
7166
  return;
5933
7167
  }
5934
7168
  if (sub === "build") {
5935
- validateDebugRelease(opts.debug, opts.release);
7169
+ validateBuildMode(opts.debug, opts.release, opts.production);
7170
+ const release = opts.release === true || opts.production === true;
5936
7171
  if (opts.embeddable) await buildEmbeddable({ release: true });
5937
- else await build_default2({ install: opts.install, release: opts.release === true });
7172
+ else {
7173
+ if (opts.production === true) assertProductionSigningReady("ios");
7174
+ await build_default2({ install: opts.install, release, production: opts.production === true });
7175
+ }
5938
7176
  return;
5939
7177
  }
5940
7178
  if (sub === "inject") {
@@ -5945,10 +7183,10 @@ program.command("ios <subcommand>").description("(Legacy) Use: t4l <command> ios
5945
7183
  process.exit(1);
5946
7184
  });
5947
7185
  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");
7186
+ const configPath = path29.join(process.cwd(), "tamer.config.json");
5949
7187
  let config = {};
5950
- if (fs24.existsSync(configPath)) {
5951
- config = JSON.parse(fs24.readFileSync(configPath, "utf8"));
7188
+ if (fs28.existsSync(configPath)) {
7189
+ config = JSON.parse(fs28.readFileSync(configPath, "utf8"));
5952
7190
  }
5953
7191
  if (config.autolink) {
5954
7192
  delete config.autolink;
@@ -5957,11 +7195,11 @@ program.command("autolink-toggle").alias("autolink").description("Toggle autolin
5957
7195
  config.autolink = true;
5958
7196
  console.log("Autolink enabled in tamer.config.json");
5959
7197
  }
5960
- fs24.writeFileSync(configPath, JSON.stringify(config, null, 2));
7198
+ fs28.writeFileSync(configPath, JSON.stringify(config, null, 2));
5961
7199
  console.log(`Updated ${configPath}`);
5962
7200
  });
5963
7201
  if (process.argv.length <= 2 || process.argv.length === 3 && process.argv[2] === "init") {
5964
- Promise.resolve(init_default()).then(() => process.exit(0));
7202
+ Promise.resolve(init()).then(() => process.exit(0));
5965
7203
  } else {
5966
7204
  program.parseAsync().then(() => process.exit(0)).catch(() => process.exit(1));
5967
7205
  }