nomadexapp 0.2.1 → 0.2.3
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/bin/nomadex.mjs +121 -19
- package/package.json +1 -1
package/bin/nomadex.mjs
CHANGED
|
@@ -162,6 +162,9 @@ const parseVersion = (value) =>
|
|
|
162
162
|
.split(".")
|
|
163
163
|
.map((part) => Number.parseInt(part.replace(/[^0-9].*$/u, ""), 10) || 0);
|
|
164
164
|
|
|
165
|
+
const isInteractivePromptAvailable = () =>
|
|
166
|
+
Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
167
|
+
|
|
165
168
|
const compareVersions = (left, right) => {
|
|
166
169
|
const maxLength = Math.max(left.length, right.length);
|
|
167
170
|
for (let index = 0; index < maxLength; index += 1) {
|
|
@@ -345,7 +348,7 @@ const renderLoginPage = ({ errorMessage = "", nextPath = "/threads" } = {}) => {
|
|
|
345
348
|
};
|
|
346
349
|
|
|
347
350
|
const promptForUpdate = async () => {
|
|
348
|
-
if (!options.updateCheck || !
|
|
351
|
+
if (!options.updateCheck || !isInteractivePromptAvailable()) {
|
|
349
352
|
return false;
|
|
350
353
|
}
|
|
351
354
|
|
|
@@ -424,16 +427,18 @@ const promptForUpdate = async () => {
|
|
|
424
427
|
};
|
|
425
428
|
|
|
426
429
|
const wsUrl = new URL(options.wsUrl);
|
|
427
|
-
const
|
|
428
|
-
const
|
|
429
|
-
const
|
|
430
|
-
|
|
430
|
+
const formatWsBaseUrl = () => `${wsUrl.protocol}//${wsUrl.host}`;
|
|
431
|
+
const getWsHost = () => wsUrl.hostname;
|
|
432
|
+
const getWsPort = () =>
|
|
433
|
+
Number(wsUrl.port || (wsUrl.protocol === "wss:" ? 443 : 80));
|
|
434
|
+
const getReadyzUrl = () => {
|
|
435
|
+
const target = new URL(wsUrl);
|
|
431
436
|
target.protocol = target.protocol === "wss:" ? "https:" : "http:";
|
|
432
437
|
target.pathname = "/readyz";
|
|
433
438
|
target.search = "";
|
|
434
439
|
target.hash = "";
|
|
435
440
|
return target;
|
|
436
|
-
}
|
|
441
|
+
};
|
|
437
442
|
const authRelayTarget = options.authRelayTarget;
|
|
438
443
|
const isTermuxEnvironment = () => {
|
|
439
444
|
const prefix = process.env.PREFIX ?? "";
|
|
@@ -443,6 +448,22 @@ const isTermuxEnvironment = () => {
|
|
|
443
448
|
);
|
|
444
449
|
};
|
|
445
450
|
|
|
451
|
+
const isLocalHost = (host) =>
|
|
452
|
+
["127.0.0.1", "0.0.0.0", "localhost", "::1", "[::1]"].includes(
|
|
453
|
+
host.toLowerCase(),
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
const getPortProbeHost = (host) => {
|
|
457
|
+
const normalized = host.toLowerCase();
|
|
458
|
+
if (normalized === "localhost" || normalized === "0.0.0.0") {
|
|
459
|
+
return "127.0.0.1";
|
|
460
|
+
}
|
|
461
|
+
if (normalized === "[::1]") {
|
|
462
|
+
return "::1";
|
|
463
|
+
}
|
|
464
|
+
return host;
|
|
465
|
+
};
|
|
466
|
+
|
|
446
467
|
const isTransientNpxCodexPath = (candidate) => {
|
|
447
468
|
const normalized = candidate.replaceAll("\\", "/");
|
|
448
469
|
return (
|
|
@@ -618,9 +639,37 @@ const isPortOpen = (targetHost, targetPort) =>
|
|
|
618
639
|
socket.connect(targetPort, targetHost);
|
|
619
640
|
});
|
|
620
641
|
|
|
642
|
+
const findNextFreePort = async (targetHost, startPort, attempts = 20) => {
|
|
643
|
+
for (let port = startPort + 1; port < startPort + 1 + attempts; port += 1) {
|
|
644
|
+
if (!(await isPortOpen(targetHost, port))) {
|
|
645
|
+
return port;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return null;
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
const confirmAlternatePort = async (message) => {
|
|
652
|
+
if (!isInteractivePromptAvailable()) {
|
|
653
|
+
return true;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const rl = createInterface({
|
|
657
|
+
input: process.stdin,
|
|
658
|
+
output: process.stdout,
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
try {
|
|
662
|
+
const answer = await rl.question(`${message} [Y/n] `);
|
|
663
|
+
const normalized = answer.trim().toLowerCase();
|
|
664
|
+
return normalized === "" || !["n", "no"].includes(normalized);
|
|
665
|
+
} finally {
|
|
666
|
+
rl.close();
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
|
|
621
670
|
const isCodexAppServerReady = async () => {
|
|
622
671
|
try {
|
|
623
|
-
const response = await fetch(
|
|
672
|
+
const response = await fetch(getReadyzUrl(), {
|
|
624
673
|
signal: AbortSignal.timeout(500),
|
|
625
674
|
});
|
|
626
675
|
return response.ok;
|
|
@@ -634,9 +683,26 @@ const ensureUiPortAvailable = async () => {
|
|
|
634
683
|
return;
|
|
635
684
|
}
|
|
636
685
|
|
|
637
|
-
|
|
638
|
-
|
|
686
|
+
const nextPort = await findNextFreePort("127.0.0.1", options.uiPort);
|
|
687
|
+
if (nextPort === null) {
|
|
688
|
+
throw new Error(
|
|
689
|
+
`UI port ${options.uiPort} is already in use and no nearby free port was found. Open the existing UI at http://127.0.0.1:${options.uiPort} or choose another port with --port.`,
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const useAlternatePort = await confirmAlternatePort(
|
|
694
|
+
`[nomadexapp] UI port ${options.uiPort} is already in use. Use ${nextPort} instead?`,
|
|
639
695
|
);
|
|
696
|
+
if (!useAlternatePort) {
|
|
697
|
+
throw new Error(
|
|
698
|
+
`UI port ${options.uiPort} is already in use. Open the existing UI at http://127.0.0.1:${options.uiPort} or choose another port with --port.`,
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
console.log(
|
|
703
|
+
`[nomadexapp] UI port ${options.uiPort} is busy. Using ${nextPort} instead.`,
|
|
704
|
+
);
|
|
705
|
+
options.uiPort = nextPort;
|
|
640
706
|
};
|
|
641
707
|
|
|
642
708
|
const formatSpawnError = (error) => {
|
|
@@ -685,22 +751,54 @@ const ensureDistBuilt = () => {
|
|
|
685
751
|
};
|
|
686
752
|
|
|
687
753
|
const ensureAppServer = async () => {
|
|
688
|
-
|
|
754
|
+
const wsHost = getWsHost();
|
|
755
|
+
const wsPort = getWsPort();
|
|
756
|
+
const probeHost = getPortProbeHost(wsHost);
|
|
757
|
+
|
|
758
|
+
if (await isPortOpen(probeHost, wsPort)) {
|
|
689
759
|
if (await isCodexAppServerReady()) {
|
|
690
|
-
console.log(
|
|
760
|
+
console.log(
|
|
761
|
+
`[nomadexapp] Reusing Codex app-server at ${formatWsBaseUrl()}`,
|
|
762
|
+
);
|
|
691
763
|
return;
|
|
692
764
|
}
|
|
693
765
|
|
|
694
|
-
|
|
695
|
-
|
|
766
|
+
if (!isLocalHost(wsHost)) {
|
|
767
|
+
throw new Error(
|
|
768
|
+
`Port ${wsPort} on ${wsHost} is already in use, but it is not responding like a Codex app-server.`,
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const nextPort = await findNextFreePort(probeHost, wsPort);
|
|
773
|
+
if (nextPort === null) {
|
|
774
|
+
throw new Error(
|
|
775
|
+
`Port ${wsPort} on ${wsHost} is already in use, and no nearby free local app-server port was found.`,
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const useAlternatePort = await confirmAlternatePort(
|
|
780
|
+
`[nomadexapp] App-server port ${wsPort} is in use by another process. Use ${nextPort} instead?`,
|
|
781
|
+
);
|
|
782
|
+
if (!useAlternatePort) {
|
|
783
|
+
throw new Error(
|
|
784
|
+
`Port ${wsPort} on ${wsHost} is already in use, but it is not responding like a Codex app-server.`,
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
wsUrl.port = String(nextPort);
|
|
789
|
+
options.wsUrl = formatWsBaseUrl();
|
|
790
|
+
console.log(
|
|
791
|
+
`[nomadexapp] App-server port ${wsPort} is busy. Using ${formatWsBaseUrl()} instead.`,
|
|
696
792
|
);
|
|
697
793
|
}
|
|
698
794
|
|
|
699
795
|
const codexLaunch = getCodexLaunch();
|
|
700
|
-
console.log(
|
|
796
|
+
console.log(
|
|
797
|
+
`[nomadexapp] Starting Codex app-server at ${formatWsBaseUrl()}`,
|
|
798
|
+
);
|
|
701
799
|
const appServer = spawn(
|
|
702
800
|
codexLaunch.command,
|
|
703
|
-
[...codexLaunch.args, "app-server", "--listen",
|
|
801
|
+
[...codexLaunch.args, "app-server", "--listen", formatWsBaseUrl()],
|
|
704
802
|
{
|
|
705
803
|
cwd: launchCwd,
|
|
706
804
|
stdio: "inherit",
|
|
@@ -725,7 +823,8 @@ const ensureAppServer = async () => {
|
|
|
725
823
|
}
|
|
726
824
|
if (appServer.exitCode !== null) {
|
|
727
825
|
const termuxHint =
|
|
728
|
-
isTermuxEnvironment() &&
|
|
826
|
+
isTermuxEnvironment() &&
|
|
827
|
+
!["PATH codex", "Termux codex"].includes(codexLaunch.source)
|
|
729
828
|
? " Termux detected. Install `@mmmbuto/codex-cli-termux@latest`, ensure `codex` is on PATH, or launch Nomadex with `--codex-cmd codex`."
|
|
730
829
|
: "";
|
|
731
830
|
throw new Error(
|
|
@@ -735,7 +834,9 @@ const ensureAppServer = async () => {
|
|
|
735
834
|
await sleep(200);
|
|
736
835
|
}
|
|
737
836
|
|
|
738
|
-
throw new Error(
|
|
837
|
+
throw new Error(
|
|
838
|
+
`Timed out waiting for Codex app-server at ${formatWsBaseUrl()}`,
|
|
839
|
+
);
|
|
739
840
|
};
|
|
740
841
|
|
|
741
842
|
const sendText = (res, statusCode, message) => {
|
|
@@ -903,7 +1004,6 @@ const getPreferredIp = () => {
|
|
|
903
1004
|
};
|
|
904
1005
|
|
|
905
1006
|
const wsProxy = httpProxy.createProxyServer({
|
|
906
|
-
target: options.wsUrl,
|
|
907
1007
|
ws: true,
|
|
908
1008
|
changeOrigin: true,
|
|
909
1009
|
});
|
|
@@ -1129,7 +1229,9 @@ server.on("upgrade", (req, socket, head) => {
|
|
|
1129
1229
|
return;
|
|
1130
1230
|
}
|
|
1131
1231
|
req.url = "/";
|
|
1132
|
-
wsProxy.ws(req, socket, head
|
|
1232
|
+
wsProxy.ws(req, socket, head, {
|
|
1233
|
+
target: wsUrl.toString(),
|
|
1234
|
+
});
|
|
1133
1235
|
return;
|
|
1134
1236
|
}
|
|
1135
1237
|
} catch {
|