@tamer4lynx/cli 0.0.12 → 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.
- package/dist/index.js +1722 -440
- package/package.json +6 -3
package/dist/index.js
CHANGED
|
@@ -7,8 +7,8 @@ process.on("warning", (w) => {
|
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
// index.ts
|
|
10
|
-
import
|
|
11
|
-
import
|
|
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
|
-
|
|
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 =
|
|
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
|
|
4355
|
-
|
|
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,93 +4405,507 @@ async function buildIpa(opts = {}) {
|
|
|
4403
4405
|
}
|
|
4404
4406
|
var build_default2 = buildIpa;
|
|
4405
4407
|
|
|
4406
|
-
// src/common/init.
|
|
4408
|
+
// src/common/init.tsx
|
|
4407
4409
|
import fs17 from "fs";
|
|
4408
4410
|
import path18 from "path";
|
|
4409
|
-
import
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
}
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
const lynxProject =
|
|
4446
|
-
const
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
} catch (e) {
|
|
4479
|
-
console.warn(`\u26A0 Could not update ${tsconfigPath}:`, e.message);
|
|
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
|
+
}
|
|
4480
4738
|
}
|
|
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
|
+
}
|
|
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
|
+
) });
|
|
4481
4888
|
}
|
|
4482
|
-
|
|
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();
|
|
4483
4900
|
}
|
|
4484
|
-
var init_default = init;
|
|
4485
4901
|
|
|
4486
4902
|
// src/common/create.ts
|
|
4487
4903
|
import fs18 from "fs";
|
|
4488
4904
|
import path19 from "path";
|
|
4489
|
-
import
|
|
4490
|
-
var
|
|
4491
|
-
function
|
|
4492
|
-
return new Promise((resolve) =>
|
|
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())));
|
|
4493
4909
|
}
|
|
4494
4910
|
async function create3(opts) {
|
|
4495
4911
|
console.log("Tamer4Lynx: Create Lynx Extension\n");
|
|
@@ -4528,29 +4944,29 @@ async function create3(opts) {
|
|
|
4528
4944
|
console.log(" [ ] Native Module");
|
|
4529
4945
|
console.log(" [ ] Element");
|
|
4530
4946
|
console.log(" [ ] Service\n");
|
|
4531
|
-
includeModule = /^y(es)?$/i.test(await
|
|
4532
|
-
includeElement = /^y(es)?$/i.test(await
|
|
4533
|
-
includeService = /^y(es)?$/i.test(await
|
|
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");
|
|
4534
4950
|
}
|
|
4535
4951
|
if (!includeModule && !includeElement && !includeService) {
|
|
4536
4952
|
console.error("\u274C At least one extension type is required.");
|
|
4537
|
-
|
|
4953
|
+
rl.close();
|
|
4538
4954
|
process.exit(1);
|
|
4539
4955
|
}
|
|
4540
|
-
const extName = await
|
|
4956
|
+
const extName = await ask("Extension package name (e.g. my-lynx-module): ");
|
|
4541
4957
|
if (!extName || !/^[a-z0-9-_]+$/.test(extName)) {
|
|
4542
4958
|
console.error("\u274C Invalid package name. Use lowercase letters, numbers, hyphens, underscores.");
|
|
4543
|
-
|
|
4959
|
+
rl.close();
|
|
4544
4960
|
process.exit(1);
|
|
4545
4961
|
}
|
|
4546
|
-
const packageName = await
|
|
4962
|
+
const packageName = await ask("Android package name (e.g. com.example.mymodule): ") || `com.example.${extName.replace(/-/g, "")}`;
|
|
4547
4963
|
const simpleModuleName = extName.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("") + "Module";
|
|
4548
4964
|
const fullModuleClassName = `${packageName}.${simpleModuleName}`;
|
|
4549
4965
|
const cwd = process.cwd();
|
|
4550
4966
|
const root = path19.join(cwd, extName);
|
|
4551
4967
|
if (fs18.existsSync(root)) {
|
|
4552
4968
|
console.error(`\u274C Directory ${extName} already exists.`);
|
|
4553
|
-
|
|
4969
|
+
rl.close();
|
|
4554
4970
|
process.exit(1);
|
|
4555
4971
|
}
|
|
4556
4972
|
fs18.mkdirSync(root, { recursive: true });
|
|
@@ -4704,7 +5120,7 @@ This package uses \`lynx.ext.json\` (RFC-compliant) for autolinking.
|
|
|
4704
5120
|
console.log(` cd ${extName}`);
|
|
4705
5121
|
console.log(" npm install");
|
|
4706
5122
|
if (includeModule) console.log(" npm run codegen");
|
|
4707
|
-
|
|
5123
|
+
rl.close();
|
|
4708
5124
|
}
|
|
4709
5125
|
var create_default3 = create3;
|
|
4710
5126
|
|
|
@@ -4775,14 +5191,16 @@ function extractLynxModules(files) {
|
|
|
4775
5191
|
}
|
|
4776
5192
|
var codegen_default = codegen;
|
|
4777
5193
|
|
|
4778
|
-
// src/common/devServer.
|
|
5194
|
+
// src/common/devServer.tsx
|
|
5195
|
+
import { useState as useState5, useEffect as useEffect3, useRef, useCallback as useCallback4 } from "react";
|
|
4779
5196
|
import { spawn } from "child_process";
|
|
4780
5197
|
import fs20 from "fs";
|
|
4781
5198
|
import http from "http";
|
|
4782
5199
|
import os4 from "os";
|
|
4783
5200
|
import path21 from "path";
|
|
4784
|
-
import
|
|
5201
|
+
import { render as render2, useInput, useApp } from "ink";
|
|
4785
5202
|
import { WebSocketServer } from "ws";
|
|
5203
|
+
import { jsx as jsx10 } from "react/jsx-runtime";
|
|
4786
5204
|
var DEFAULT_PORT = 3e3;
|
|
4787
5205
|
var STATIC_MIME = {
|
|
4788
5206
|
".png": "image/png",
|
|
@@ -4837,319 +5255,431 @@ function getLanIp() {
|
|
|
4837
5255
|
}
|
|
4838
5256
|
return "localhost";
|
|
4839
5257
|
}
|
|
4840
|
-
|
|
4841
|
-
const
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
const
|
|
4872
|
-
const
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
if (fs20.existsSync(iosIcon)) iconFilePath = iosIcon;
|
|
4890
|
-
}
|
|
4891
|
-
const iconExt = iconFilePath ? path21.extname(iconFilePath) || ".png" : "";
|
|
4892
|
-
const httpServer = http.createServer((req, res) => {
|
|
4893
|
-
let reqPath = (req.url || "/").split("?")[0];
|
|
4894
|
-
if (reqPath === `${basePath}/status`) {
|
|
4895
|
-
res.setHeader("Content-Type", "text/plain");
|
|
4896
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
4897
|
-
res.end("packager-status:running");
|
|
4898
|
-
return;
|
|
4899
|
-
}
|
|
4900
|
-
if (reqPath === `${basePath}/meta.json`) {
|
|
4901
|
-
const lanIp = getLanIp();
|
|
4902
|
-
const nativeModules = discoverNativeExtensions(projectRoot);
|
|
4903
|
-
const androidPackageName = config.android?.packageName?.trim();
|
|
4904
|
-
const iosBundleId = config.ios?.bundleId?.trim();
|
|
4905
|
-
const idParts = [androidPackageName?.toLowerCase(), iosBundleId?.toLowerCase()].filter(
|
|
4906
|
-
(x) => Boolean(x)
|
|
4907
|
-
);
|
|
4908
|
-
const meta = {
|
|
4909
|
-
name: projectName,
|
|
4910
|
-
slug: projectName,
|
|
4911
|
-
bundleUrl: `http://${lanIp}:${port}${basePath}/${lynxBundleFile}`,
|
|
4912
|
-
bundleFile: lynxBundleFile,
|
|
4913
|
-
hostUri: `http://${lanIp}:${port}${basePath}`,
|
|
4914
|
-
debuggerHost: `${lanIp}:${port}`,
|
|
4915
|
-
developer: { tool: "tamer4lynx" },
|
|
4916
|
-
packagerStatus: "running",
|
|
4917
|
-
nativeModules: nativeModules.map((m) => ({ packageName: m.packageName, moduleClassName: m.moduleClassName }))
|
|
4918
|
-
};
|
|
4919
|
-
if (androidPackageName) meta.androidPackageName = androidPackageName;
|
|
4920
|
-
if (iosBundleId) meta.iosBundleId = iosBundleId;
|
|
4921
|
-
if (idParts.length > 0) meta.tamerAppKey = idParts.join("|");
|
|
4922
|
-
const rawIcon = config.icon;
|
|
4923
|
-
if (rawIcon && typeof rawIcon === "object" && "source" in rawIcon && typeof rawIcon.source === "string") {
|
|
4924
|
-
meta.iconSource = rawIcon.source;
|
|
4925
|
-
} else if (typeof rawIcon === "string") {
|
|
4926
|
-
meta.iconSource = rawIcon;
|
|
4927
|
-
}
|
|
4928
|
-
if (iconFilePath) {
|
|
4929
|
-
meta.icon = `http://${lanIp}:${port}${basePath}/icon${iconExt}`;
|
|
4930
|
-
}
|
|
4931
|
-
res.setHeader("Content-Type", "application/json");
|
|
4932
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
4933
|
-
res.end(JSON.stringify(meta, null, 2));
|
|
4934
|
-
return;
|
|
4935
|
-
}
|
|
4936
|
-
if (iconFilePath && (reqPath === `${basePath}/icon` || reqPath === `${basePath}/icon${iconExt}`)) {
|
|
4937
|
-
fs20.readFile(iconFilePath, (err, data) => {
|
|
4938
|
-
if (err) {
|
|
4939
|
-
res.writeHead(404);
|
|
4940
|
-
res.end();
|
|
4941
|
-
return;
|
|
4942
|
-
}
|
|
4943
|
-
res.setHeader("Content-Type", STATIC_MIME[iconExt] ?? "image/png");
|
|
4944
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
4945
|
-
res.end(data);
|
|
4946
|
-
});
|
|
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();
|
|
4947
5307
|
return;
|
|
4948
5308
|
}
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
{ prefix: `${basePath}/assets/`, rootSub: "assets" }
|
|
4952
|
-
];
|
|
4953
|
-
for (const { prefix, rootSub } of lynxStaticMounts) {
|
|
4954
|
-
if (!reqPath.startsWith(prefix)) continue;
|
|
4955
|
-
let rel = reqPath.slice(prefix.length);
|
|
4956
|
-
try {
|
|
4957
|
-
rel = decodeURIComponent(rel);
|
|
4958
|
-
} catch {
|
|
4959
|
-
res.writeHead(400);
|
|
4960
|
-
res.end();
|
|
4961
|
-
return;
|
|
4962
|
-
}
|
|
4963
|
-
const safe = path21.normalize(rel).replace(/^(\.\.(\/|\\|$))+/, "");
|
|
4964
|
-
if (path21.isAbsolute(safe) || safe.startsWith("..")) {
|
|
4965
|
-
res.writeHead(403);
|
|
4966
|
-
res.end();
|
|
4967
|
-
return;
|
|
4968
|
-
}
|
|
4969
|
-
const allowedRoot = path21.resolve(lynxProjectDir, rootSub);
|
|
4970
|
-
const abs = path21.resolve(allowedRoot, safe);
|
|
4971
|
-
if (!abs.startsWith(allowedRoot + path21.sep) && abs !== allowedRoot) {
|
|
4972
|
-
res.writeHead(403);
|
|
4973
|
-
res.end();
|
|
4974
|
-
return;
|
|
4975
|
-
}
|
|
4976
|
-
if (!fs20.existsSync(abs) || !fs20.statSync(abs).isFile()) {
|
|
4977
|
-
res.writeHead(404);
|
|
4978
|
-
res.end("Not found");
|
|
4979
|
-
return;
|
|
4980
|
-
}
|
|
4981
|
-
sendFileFromDisk(res, abs);
|
|
5309
|
+
if (input === "q") {
|
|
5310
|
+
handleQuit();
|
|
4982
5311
|
return;
|
|
4983
5312
|
}
|
|
4984
|
-
if (
|
|
4985
|
-
|
|
4986
|
-
} else if (!reqPath.startsWith(basePath)) {
|
|
4987
|
-
reqPath = basePath + (reqPath.startsWith("/") ? reqPath : "/" + reqPath);
|
|
4988
|
-
}
|
|
4989
|
-
const relPath = reqPath.replace(basePath, "").replace(/^\//, "") || lynxBundleFile;
|
|
4990
|
-
const filePath = path21.resolve(distDir, relPath);
|
|
4991
|
-
const distResolved = path21.resolve(distDir);
|
|
4992
|
-
if (!filePath.startsWith(distResolved + path21.sep) && filePath !== distResolved) {
|
|
4993
|
-
res.writeHead(403);
|
|
4994
|
-
res.end();
|
|
5313
|
+
if (input === "r") {
|
|
5314
|
+
void rebuildRef.current();
|
|
4995
5315
|
return;
|
|
4996
5316
|
}
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
res.writeHead(404);
|
|
5000
|
-
res.end("Not found");
|
|
5001
|
-
return;
|
|
5002
|
-
}
|
|
5003
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
5004
|
-
res.setHeader("Content-Type", reqPath.endsWith(".bundle") ? "application/octet-stream" : "application/javascript");
|
|
5005
|
-
res.end(data);
|
|
5006
|
-
});
|
|
5007
|
-
});
|
|
5008
|
-
const wss = new WebSocketServer({ noServer: true });
|
|
5009
|
-
httpServer.on("upgrade", (request, socket, head) => {
|
|
5010
|
-
const reqPath = (request.url || "").split("?")[0];
|
|
5011
|
-
if (reqPath === `${basePath}/__hmr` || reqPath === "/__hmr" || reqPath.endsWith("/__hmr")) {
|
|
5012
|
-
wss.handleUpgrade(request, socket, head, (ws) => wss.emit("connection", ws, request));
|
|
5013
|
-
} else {
|
|
5014
|
-
socket.destroy();
|
|
5317
|
+
if (input === "l") {
|
|
5318
|
+
setUi((s) => ({ ...s, showLogs: !s.showLogs }));
|
|
5015
5319
|
}
|
|
5016
5320
|
});
|
|
5017
|
-
|
|
5018
|
-
const
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
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 () => {
|
|
5025
5338
|
try {
|
|
5026
|
-
const
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
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}`);
|
|
5034
5349
|
}
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
} catch {
|
|
5048
|
-
}
|
|
5049
|
-
if (chokidar) {
|
|
5050
|
-
const watchPaths = [
|
|
5051
|
-
path21.join(lynxProjectDir, "src"),
|
|
5052
|
-
path21.join(lynxProjectDir, "lynx.config.ts"),
|
|
5053
|
-
path21.join(lynxProjectDir, "lynx.config.js")
|
|
5054
|
-
].filter((p) => fs20.existsSync(p));
|
|
5055
|
-
if (watchPaths.length > 0) {
|
|
5056
|
-
const watcher = chokidar.watch(watchPaths, { ignoreInitial: true });
|
|
5057
|
-
watcher.on("change", async () => {
|
|
5058
|
-
try {
|
|
5059
|
-
await runBuild();
|
|
5060
|
-
broadcastReload();
|
|
5061
|
-
console.log("\u{1F504} Rebuilt, clients notified");
|
|
5062
|
-
} catch (e) {
|
|
5063
|
-
console.error("Build failed:", e.message);
|
|
5064
|
-
}
|
|
5065
|
-
});
|
|
5066
|
-
}
|
|
5067
|
-
}
|
|
5068
|
-
try {
|
|
5069
|
-
await runBuild();
|
|
5070
|
-
} catch (e) {
|
|
5071
|
-
console.error("\u274C Initial build failed:", e.message);
|
|
5072
|
-
process.exit(1);
|
|
5073
|
-
}
|
|
5074
|
-
let stopBonjour;
|
|
5075
|
-
httpServer.listen(port, "0.0.0.0", () => {
|
|
5076
|
-
void import("dnssd-advertise").then(({ advertise }) => {
|
|
5077
|
-
stopBonjour = advertise({
|
|
5078
|
-
name: projectName,
|
|
5079
|
-
type: "tamer",
|
|
5080
|
-
protocol: "tcp",
|
|
5081
|
-
port,
|
|
5082
|
-
txt: {
|
|
5083
|
-
name: projectName.slice(0, 255),
|
|
5084
|
-
path: basePath.slice(0, 255)
|
|
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;
|
|
5085
5362
|
}
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
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 {
|
|
5118
5567
|
}
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
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
|
+
}
|
|
5134
5588
|
}
|
|
5135
|
-
|
|
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
|
|
5136
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
|
|
5137
5681
|
});
|
|
5138
|
-
|
|
5139
|
-
buildProcess?.kill();
|
|
5140
|
-
await stopBonjour?.();
|
|
5141
|
-
httpServer.close();
|
|
5142
|
-
wss.close();
|
|
5143
|
-
process.exit(0);
|
|
5144
|
-
};
|
|
5145
|
-
process.on("SIGINT", () => {
|
|
5146
|
-
void cleanup();
|
|
5147
|
-
});
|
|
5148
|
-
process.on("SIGTERM", () => {
|
|
5149
|
-
void cleanup();
|
|
5150
|
-
});
|
|
5151
|
-
await new Promise(() => {
|
|
5152
|
-
});
|
|
5682
|
+
await waitUntilExit();
|
|
5153
5683
|
}
|
|
5154
5684
|
var devServer_default = startDevServer;
|
|
5155
5685
|
|
|
@@ -5659,7 +6189,10 @@ ${podDeps.map((d) => `pod '${d.podName}', :path => '${d.absPath}'`).join("\n")}
|
|
|
5659
6189
|
// src/common/add.ts
|
|
5660
6190
|
import fs23 from "fs";
|
|
5661
6191
|
import path24 from "path";
|
|
5662
|
-
import { execSync as execSync10 } from "child_process";
|
|
6192
|
+
import { execFile, execSync as execSync10 } from "child_process";
|
|
6193
|
+
import { promisify } from "util";
|
|
6194
|
+
import semver from "semver";
|
|
6195
|
+
var execFileAsync = promisify(execFile);
|
|
5663
6196
|
var CORE_PACKAGES = [
|
|
5664
6197
|
"@tamer4lynx/tamer-app-shell",
|
|
5665
6198
|
"@tamer4lynx/tamer-screen",
|
|
@@ -5669,8 +6202,49 @@ var CORE_PACKAGES = [
|
|
|
5669
6202
|
"@tamer4lynx/tamer-system-ui",
|
|
5670
6203
|
"@tamer4lynx/tamer-icons"
|
|
5671
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
|
+
];
|
|
5672
6222
|
var PACKAGE_ALIASES = {};
|
|
5673
|
-
function
|
|
6223
|
+
async function getHighestPublishedVersion(fullName) {
|
|
6224
|
+
try {
|
|
6225
|
+
const { stdout } = await execFileAsync("npm", ["view", fullName, "versions", "--json"], {
|
|
6226
|
+
maxBuffer: 10 * 1024 * 1024
|
|
6227
|
+
});
|
|
6228
|
+
const parsed = JSON.parse(stdout.trim());
|
|
6229
|
+
const list = Array.isArray(parsed) ? parsed : [parsed];
|
|
6230
|
+
const valid = list.filter((v) => typeof v === "string" && !!semver.valid(v));
|
|
6231
|
+
if (valid.length === 0) return null;
|
|
6232
|
+
return semver.rsort(valid)[0] ?? null;
|
|
6233
|
+
} catch {
|
|
6234
|
+
return null;
|
|
6235
|
+
}
|
|
6236
|
+
}
|
|
6237
|
+
async function normalizeTamerInstallSpec(pkg) {
|
|
6238
|
+
if (!pkg.startsWith("@tamer4lynx/")) return pkg;
|
|
6239
|
+
if (/^@[^/]+\/[^@]+@/.test(pkg)) return pkg;
|
|
6240
|
+
const highest = await getHighestPublishedVersion(pkg);
|
|
6241
|
+
if (highest) {
|
|
6242
|
+
return `${pkg}@${highest}`;
|
|
6243
|
+
}
|
|
6244
|
+
console.warn(`\u26A0\uFE0F Could not resolve published versions for ${pkg}; using @prerelease`);
|
|
6245
|
+
return `${pkg}@prerelease`;
|
|
6246
|
+
}
|
|
6247
|
+
function detectPackageManager2(cwd) {
|
|
5674
6248
|
const dir = path24.resolve(cwd);
|
|
5675
6249
|
if (fs23.existsSync(path24.join(dir, "pnpm-lock.yaml"))) return "pnpm";
|
|
5676
6250
|
if (fs23.existsSync(path24.join(dir, "bun.lockb"))) return "bun";
|
|
@@ -5681,14 +6255,25 @@ function runInstall(cwd, packages, pm) {
|
|
|
5681
6255
|
const cmd = pm === "npm" ? "npm" : pm === "pnpm" ? "pnpm" : "bun";
|
|
5682
6256
|
execSync10(`${cmd} ${args.join(" ")}`, { stdio: "inherit", cwd });
|
|
5683
6257
|
}
|
|
5684
|
-
function addCore() {
|
|
6258
|
+
async function addCore() {
|
|
5685
6259
|
const { lynxProjectDir } = resolveHostPaths();
|
|
5686
|
-
const pm =
|
|
5687
|
-
console.log(`
|
|
5688
|
-
|
|
6260
|
+
const pm = detectPackageManager2(lynxProjectDir);
|
|
6261
|
+
console.log(`Resolving latest published versions (npm)\u2026`);
|
|
6262
|
+
const resolved = await Promise.all(CORE_PACKAGES.map(normalizeTamerInstallSpec));
|
|
6263
|
+
console.log(`Adding core packages to ${lynxProjectDir} (using ${pm})\u2026`);
|
|
6264
|
+
runInstall(lynxProjectDir, resolved, pm);
|
|
5689
6265
|
console.log("\u2705 Core packages installed. Run `t4l link` to link native modules.");
|
|
5690
6266
|
}
|
|
5691
|
-
function
|
|
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
|
+
}
|
|
6276
|
+
async function add(packages = []) {
|
|
5692
6277
|
const list = Array.isArray(packages) ? packages : [];
|
|
5693
6278
|
if (list.length === 0) {
|
|
5694
6279
|
console.log("Usage: t4l add <package> [package...]");
|
|
@@ -5698,28 +6283,695 @@ function add(packages = []) {
|
|
|
5698
6283
|
return;
|
|
5699
6284
|
}
|
|
5700
6285
|
const { lynxProjectDir } = resolveHostPaths();
|
|
5701
|
-
const pm =
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
6286
|
+
const pm = detectPackageManager2(lynxProjectDir);
|
|
6287
|
+
console.log(`Resolving latest published versions (npm)\u2026`);
|
|
6288
|
+
const normalized = await Promise.all(
|
|
6289
|
+
list.map(async (p) => {
|
|
6290
|
+
const spec = p.startsWith("@") ? p : PACKAGE_ALIASES[p] ?? `@tamer4lynx/${p}`;
|
|
6291
|
+
return normalizeTamerInstallSpec(spec);
|
|
6292
|
+
})
|
|
6293
|
+
);
|
|
6294
|
+
console.log(`Adding ${normalized.join(", ")} to ${lynxProjectDir} (using ${pm})\u2026`);
|
|
5707
6295
|
runInstall(lynxProjectDir, normalized, pm);
|
|
5708
6296
|
console.log("\u2705 Packages installed. Run `t4l link` to link native modules.");
|
|
5709
6297
|
}
|
|
5710
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
|
+
|
|
5711
6962
|
// index.ts
|
|
5712
6963
|
function readCliVersion() {
|
|
5713
|
-
const root =
|
|
5714
|
-
const here =
|
|
5715
|
-
const parent =
|
|
5716
|
-
const pkgPath =
|
|
5717
|
-
return JSON.parse(
|
|
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;
|
|
5718
6969
|
}
|
|
5719
6970
|
var version = readCliVersion();
|
|
5720
|
-
function
|
|
5721
|
-
|
|
5722
|
-
|
|
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.");
|
|
5723
6975
|
process.exit(1);
|
|
5724
6976
|
}
|
|
5725
6977
|
}
|
|
@@ -5731,7 +6983,7 @@ function parsePlatform(value) {
|
|
|
5731
6983
|
}
|
|
5732
6984
|
program.version(version).description("Tamer4Lynx CLI - A tool for managing Lynx projects");
|
|
5733
6985
|
program.command("init").description("Initialize tamer.config.json interactively").action(() => {
|
|
5734
|
-
|
|
6986
|
+
init();
|
|
5735
6987
|
});
|
|
5736
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) => {
|
|
5737
6989
|
const t = target.toLowerCase();
|
|
@@ -5754,22 +7006,26 @@ program.command("create <target>").description("Create a project or extension. T
|
|
|
5754
7006
|
console.error(`Invalid create target: ${target}. Use ios | android | module | element | service | combo`);
|
|
5755
7007
|
process.exit(1);
|
|
5756
7008
|
});
|
|
5757
|
-
program.command("build [platform]").description("Build app. Platform: ios | android (default: both)").option("-e, --embeddable", "Output embeddable bundle + code for existing apps. Use with --release.").option("-d, --debug", "Debug build with dev client embedded (default)").option("-r, --release", "Release build without dev client").option("-i, --install", "Install after building").action(async (platform, opts) => {
|
|
5758
|
-
|
|
5759
|
-
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;
|
|
5760
7013
|
if (opts.embeddable) {
|
|
5761
7014
|
await buildEmbeddable({ release: true });
|
|
5762
7015
|
return;
|
|
5763
7016
|
}
|
|
5764
7017
|
const p = parsePlatform(platform ?? "all") ?? "all";
|
|
7018
|
+
if (production) {
|
|
7019
|
+
assertProductionSigningReady(p);
|
|
7020
|
+
}
|
|
5765
7021
|
if (p === "android" || p === "all") {
|
|
5766
|
-
await build_default({ install: opts.install, release });
|
|
7022
|
+
await build_default({ install: opts.install, release, production });
|
|
5767
7023
|
}
|
|
5768
7024
|
if (p === "ios" || p === "all") {
|
|
5769
|
-
await build_default2({ install: opts.install, release });
|
|
7025
|
+
await build_default2({ install: opts.install, release, production });
|
|
5770
7026
|
}
|
|
5771
7027
|
});
|
|
5772
|
-
program.command("link [platform]").description("Link native modules. Platform: ios | android | both (default: both)").option("-s, --silent", "Run
|
|
7028
|
+
program.command("link [platform]").description("Link native modules. Platform: ios | android | both (default: both)").option("-s, --silent", "Run without output").action((platform, opts) => {
|
|
5773
7029
|
if (opts.silent) {
|
|
5774
7030
|
console.log = () => {
|
|
5775
7031
|
};
|
|
@@ -5790,14 +7046,15 @@ program.command("link [platform]").description("Link native modules. Platform: i
|
|
|
5790
7046
|
autolink_default2();
|
|
5791
7047
|
autolink_default();
|
|
5792
7048
|
});
|
|
5793
|
-
program.command("bundle [platform]").description("Build Lynx bundle and copy to native project. Platform: ios | android (default: both)").option("-d, --debug", "Debug bundle with dev client embedded (default)").option("-r, --release", "Release bundle without dev client").action(async (platform, opts) => {
|
|
5794
|
-
|
|
5795
|
-
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;
|
|
5796
7053
|
const p = parsePlatform(platform ?? "both") ?? "both";
|
|
5797
|
-
if (p === "android" || p === "all") await bundle_default({ release });
|
|
5798
|
-
if (p === "ios" || p === "all") bundle_default2({ release });
|
|
7054
|
+
if (p === "android" || p === "all") await bundle_default({ release, production });
|
|
7055
|
+
if (p === "ios" || p === "all") bundle_default2({ release, production });
|
|
5799
7056
|
});
|
|
5800
|
-
program.command("inject <platform>").description("Inject
|
|
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) => {
|
|
5801
7058
|
const p = platform?.toLowerCase();
|
|
5802
7059
|
if (p === "ios") {
|
|
5803
7060
|
await injectHostIos({ force: opts.force });
|
|
@@ -5810,7 +7067,7 @@ program.command("inject <platform>").description("Inject tamer-host templates in
|
|
|
5810
7067
|
console.error(`Invalid inject platform: ${platform}. Use ios | android`);
|
|
5811
7068
|
process.exit(1);
|
|
5812
7069
|
});
|
|
5813
|
-
program.command("sync [platform]").description("Sync dev client
|
|
7070
|
+
program.command("sync [platform]").description("Sync dev client. Platform: android (default)").action(async (platform) => {
|
|
5814
7071
|
const p = (platform ?? "android").toLowerCase();
|
|
5815
7072
|
if (p !== "android") {
|
|
5816
7073
|
console.error("sync only supports android.");
|
|
@@ -5831,12 +7088,27 @@ program.command("build-dev-app").option("-p, --platform <platform>", "Platform:
|
|
|
5831
7088
|
await build_default2({ install: opts.install, release: false });
|
|
5832
7089
|
}
|
|
5833
7090
|
});
|
|
5834
|
-
program.command("add [packages...]").description("Add @tamer4lynx packages to the Lynx project
|
|
5835
|
-
|
|
7091
|
+
program.command("add [packages...]").description("Add @tamer4lynx packages to the Lynx project").action(async (packages) => {
|
|
7092
|
+
await add(packages);
|
|
7093
|
+
});
|
|
7094
|
+
program.command("add-core").description("Add core packages").action(async () => {
|
|
7095
|
+
await addCore();
|
|
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
|
+
});
|
|
5836
7108
|
program.command("codegen").description("Generate code from @lynxmodule declarations").action(() => {
|
|
5837
7109
|
codegen_default();
|
|
5838
7110
|
});
|
|
5839
|
-
program.command("android <subcommand>").description("(Legacy) Use: t4l <command> android. e.g. t4l create android").option("-d, --debug", "Create: host project. Bundle/build: debug with dev client.").option("-r, --release", "Create: dev-app project. Bundle/build: release without dev client.").option("-i, --install", "Install after build").option("-e, --embeddable", "Build embeddable").option("-f, --force", "Force (inject)").action(async (subcommand, opts) => {
|
|
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) => {
|
|
5840
7112
|
const sub = subcommand?.toLowerCase();
|
|
5841
7113
|
if (sub === "create") {
|
|
5842
7114
|
if (opts.debug && opts.release) {
|
|
@@ -5851,14 +7123,19 @@ program.command("android <subcommand>").description("(Legacy) Use: t4l <command>
|
|
|
5851
7123
|
return;
|
|
5852
7124
|
}
|
|
5853
7125
|
if (sub === "bundle") {
|
|
5854
|
-
|
|
5855
|
-
|
|
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 });
|
|
5856
7129
|
return;
|
|
5857
7130
|
}
|
|
5858
7131
|
if (sub === "build") {
|
|
5859
|
-
|
|
7132
|
+
validateBuildMode(opts.debug, opts.release, opts.production);
|
|
7133
|
+
const release = opts.release === true || opts.production === true;
|
|
5860
7134
|
if (opts.embeddable) await buildEmbeddable({ release: true });
|
|
5861
|
-
else
|
|
7135
|
+
else {
|
|
7136
|
+
if (opts.production === true) assertProductionSigningReady("android");
|
|
7137
|
+
await build_default({ install: opts.install, release, production: opts.production === true });
|
|
7138
|
+
}
|
|
5862
7139
|
return;
|
|
5863
7140
|
}
|
|
5864
7141
|
if (sub === "sync") {
|
|
@@ -5872,7 +7149,7 @@ program.command("android <subcommand>").description("(Legacy) Use: t4l <command>
|
|
|
5872
7149
|
console.error(`Unknown android subcommand: ${subcommand}. Use: create | link | bundle | build | sync | inject`);
|
|
5873
7150
|
process.exit(1);
|
|
5874
7151
|
});
|
|
5875
|
-
program.command("ios <subcommand>").description("(Legacy) Use: t4l <command> ios. e.g. t4l create ios").option("-d, --debug", "Debug (bundle/build)").option("-r, --release", "Release (bundle/build)").option("-i, --install", "Install after build").option("-e, --embeddable", "Build embeddable").option("-f, --force", "Force (inject)").action(async (subcommand, opts) => {
|
|
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) => {
|
|
5876
7153
|
const sub = subcommand?.toLowerCase();
|
|
5877
7154
|
if (sub === "create") {
|
|
5878
7155
|
create_default2();
|
|
@@ -5883,14 +7160,19 @@ program.command("ios <subcommand>").description("(Legacy) Use: t4l <command> ios
|
|
|
5883
7160
|
return;
|
|
5884
7161
|
}
|
|
5885
7162
|
if (sub === "bundle") {
|
|
5886
|
-
|
|
5887
|
-
|
|
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 });
|
|
5888
7166
|
return;
|
|
5889
7167
|
}
|
|
5890
7168
|
if (sub === "build") {
|
|
5891
|
-
|
|
7169
|
+
validateBuildMode(opts.debug, opts.release, opts.production);
|
|
7170
|
+
const release = opts.release === true || opts.production === true;
|
|
5892
7171
|
if (opts.embeddable) await buildEmbeddable({ release: true });
|
|
5893
|
-
else
|
|
7172
|
+
else {
|
|
7173
|
+
if (opts.production === true) assertProductionSigningReady("ios");
|
|
7174
|
+
await build_default2({ install: opts.install, release, production: opts.production === true });
|
|
7175
|
+
}
|
|
5894
7176
|
return;
|
|
5895
7177
|
}
|
|
5896
7178
|
if (sub === "inject") {
|
|
@@ -5901,10 +7183,10 @@ program.command("ios <subcommand>").description("(Legacy) Use: t4l <command> ios
|
|
|
5901
7183
|
process.exit(1);
|
|
5902
7184
|
});
|
|
5903
7185
|
program.command("autolink-toggle").alias("autolink").description("Toggle autolink on/off in tamer.config.json (controls postinstall linking)").action(async () => {
|
|
5904
|
-
const configPath =
|
|
7186
|
+
const configPath = path29.join(process.cwd(), "tamer.config.json");
|
|
5905
7187
|
let config = {};
|
|
5906
|
-
if (
|
|
5907
|
-
config = JSON.parse(
|
|
7188
|
+
if (fs28.existsSync(configPath)) {
|
|
7189
|
+
config = JSON.parse(fs28.readFileSync(configPath, "utf8"));
|
|
5908
7190
|
}
|
|
5909
7191
|
if (config.autolink) {
|
|
5910
7192
|
delete config.autolink;
|
|
@@ -5913,11 +7195,11 @@ program.command("autolink-toggle").alias("autolink").description("Toggle autolin
|
|
|
5913
7195
|
config.autolink = true;
|
|
5914
7196
|
console.log("Autolink enabled in tamer.config.json");
|
|
5915
7197
|
}
|
|
5916
|
-
|
|
7198
|
+
fs28.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
5917
7199
|
console.log(`Updated ${configPath}`);
|
|
5918
7200
|
});
|
|
5919
7201
|
if (process.argv.length <= 2 || process.argv.length === 3 && process.argv[2] === "init") {
|
|
5920
|
-
Promise.resolve(
|
|
7202
|
+
Promise.resolve(init()).then(() => process.exit(0));
|
|
5921
7203
|
} else {
|
|
5922
7204
|
program.parseAsync().then(() => process.exit(0)).catch(() => process.exit(1));
|
|
5923
7205
|
}
|