nomadexapp 0.2.0 → 0.2.2
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 +182 -23
- package/package.json +1 -1
package/bin/nomadex.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
|
-
import { statSync, readFileSync, existsSync } from "node:fs";
|
|
4
|
+
import { statSync, readFileSync, existsSync, realpathSync } from "node:fs";
|
|
5
5
|
import { readFile } from "node:fs/promises";
|
|
6
6
|
import net from "node:net";
|
|
7
7
|
import os from "node:os";
|
|
@@ -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,17 @@ const promptForUpdate = async () => {
|
|
|
424
427
|
};
|
|
425
428
|
|
|
426
429
|
const wsUrl = new URL(options.wsUrl);
|
|
427
|
-
const
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
430
|
+
const getWsHost = () => wsUrl.hostname;
|
|
431
|
+
const getWsPort = () =>
|
|
432
|
+
Number(wsUrl.port || (wsUrl.protocol === "wss:" ? 443 : 80));
|
|
433
|
+
const getReadyzUrl = () => {
|
|
434
|
+
const target = new URL(wsUrl);
|
|
431
435
|
target.protocol = target.protocol === "wss:" ? "https:" : "http:";
|
|
432
436
|
target.pathname = "/readyz";
|
|
433
437
|
target.search = "";
|
|
434
438
|
target.hash = "";
|
|
435
439
|
return target;
|
|
436
|
-
}
|
|
440
|
+
};
|
|
437
441
|
const authRelayTarget = options.authRelayTarget;
|
|
438
442
|
const isTermuxEnvironment = () => {
|
|
439
443
|
const prefix = process.env.PREFIX ?? "";
|
|
@@ -443,6 +447,31 @@ const isTermuxEnvironment = () => {
|
|
|
443
447
|
);
|
|
444
448
|
};
|
|
445
449
|
|
|
450
|
+
const isLocalHost = (host) =>
|
|
451
|
+
["127.0.0.1", "0.0.0.0", "localhost", "::1", "[::1]"].includes(
|
|
452
|
+
host.toLowerCase(),
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
const getPortProbeHost = (host) => {
|
|
456
|
+
const normalized = host.toLowerCase();
|
|
457
|
+
if (normalized === "localhost" || normalized === "0.0.0.0") {
|
|
458
|
+
return "127.0.0.1";
|
|
459
|
+
}
|
|
460
|
+
if (normalized === "[::1]") {
|
|
461
|
+
return "::1";
|
|
462
|
+
}
|
|
463
|
+
return host;
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
const isTransientNpxCodexPath = (candidate) => {
|
|
467
|
+
const normalized = candidate.replaceAll("\\", "/");
|
|
468
|
+
return (
|
|
469
|
+
normalized.includes("/.npm/_npx/") ||
|
|
470
|
+
normalized.includes("/node_modules/.bin/codex") ||
|
|
471
|
+
normalized.includes("/node_modules/@openai/codex/")
|
|
472
|
+
);
|
|
473
|
+
};
|
|
474
|
+
|
|
446
475
|
const resolveLocalNodePackageLaunch = (packageName, binName) => {
|
|
447
476
|
try {
|
|
448
477
|
const packageJsonPath = require.resolve(`${packageName}/package.json`);
|
|
@@ -467,14 +496,24 @@ const resolveLocalNodePackageLaunch = (packageName, binName) => {
|
|
|
467
496
|
}
|
|
468
497
|
};
|
|
469
498
|
|
|
470
|
-
const resolveExecutableOnPath = (commandNames) => {
|
|
499
|
+
const resolveExecutableOnPath = (commandNames, options = {}) => {
|
|
500
|
+
const { exclude } = options;
|
|
471
501
|
const pathDirs = (process.env.PATH ?? "").split(path.delimiter).filter(Boolean);
|
|
472
502
|
|
|
473
503
|
for (const dir of pathDirs) {
|
|
474
504
|
for (const commandName of commandNames) {
|
|
475
505
|
const candidate = path.join(dir, commandName);
|
|
476
506
|
try {
|
|
477
|
-
if (statSync(candidate).isFile()) {
|
|
507
|
+
if (!statSync(candidate).isFile()) {
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const resolved = realpathSync(candidate);
|
|
512
|
+
if (exclude?.(candidate, resolved)) {
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (resolved) {
|
|
478
517
|
return candidate;
|
|
479
518
|
}
|
|
480
519
|
} catch {
|
|
@@ -504,6 +543,49 @@ const resolvePathCodexLaunch = () => {
|
|
|
504
543
|
};
|
|
505
544
|
};
|
|
506
545
|
|
|
546
|
+
const resolveTermuxCodexLaunch = () => {
|
|
547
|
+
const prefix = process.env.PREFIX?.trim() || "/data/data/com.termux/files/usr";
|
|
548
|
+
const directCandidates = [
|
|
549
|
+
path.join(prefix, "bin", "codex"),
|
|
550
|
+
"/data/data/com.termux/files/usr/bin/codex",
|
|
551
|
+
];
|
|
552
|
+
|
|
553
|
+
for (const candidate of directCandidates) {
|
|
554
|
+
try {
|
|
555
|
+
if (statSync(candidate).isFile()) {
|
|
556
|
+
return {
|
|
557
|
+
command: candidate,
|
|
558
|
+
args: [],
|
|
559
|
+
shell: false,
|
|
560
|
+
source: "Termux codex",
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
} catch {
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const commandNames =
|
|
569
|
+
process.platform === "win32"
|
|
570
|
+
? ["codex.cmd", "codex.exe", "codex.bat", "codex"]
|
|
571
|
+
: ["codex"];
|
|
572
|
+
const command = resolveExecutableOnPath(commandNames, {
|
|
573
|
+
exclude: (candidate, resolved) =>
|
|
574
|
+
isTransientNpxCodexPath(candidate) || isTransientNpxCodexPath(resolved),
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
if (!command) {
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return {
|
|
582
|
+
command,
|
|
583
|
+
args: [],
|
|
584
|
+
shell: false,
|
|
585
|
+
source: "Termux codex",
|
|
586
|
+
};
|
|
587
|
+
};
|
|
588
|
+
|
|
507
589
|
const getCodexLaunch = () => {
|
|
508
590
|
const explicitCommand = options.codexCommand.trim();
|
|
509
591
|
if (explicitCommand) {
|
|
@@ -516,7 +598,9 @@ const getCodexLaunch = () => {
|
|
|
516
598
|
}
|
|
517
599
|
|
|
518
600
|
const dependencyLaunch = resolveLocalNodePackageLaunch("@openai/codex", "codex");
|
|
519
|
-
const pathLaunch =
|
|
601
|
+
const pathLaunch = isTermuxEnvironment()
|
|
602
|
+
? resolveTermuxCodexLaunch()
|
|
603
|
+
: resolvePathCodexLaunch();
|
|
520
604
|
|
|
521
605
|
if (isTermuxEnvironment()) {
|
|
522
606
|
return pathLaunch ?? dependencyLaunch ?? {
|
|
@@ -554,9 +638,37 @@ const isPortOpen = (targetHost, targetPort) =>
|
|
|
554
638
|
socket.connect(targetPort, targetHost);
|
|
555
639
|
});
|
|
556
640
|
|
|
641
|
+
const findNextFreePort = async (targetHost, startPort, attempts = 20) => {
|
|
642
|
+
for (let port = startPort + 1; port < startPort + 1 + attempts; port += 1) {
|
|
643
|
+
if (!(await isPortOpen(targetHost, port))) {
|
|
644
|
+
return port;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return null;
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
const confirmAlternatePort = async (message) => {
|
|
651
|
+
if (!isInteractivePromptAvailable()) {
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const rl = createInterface({
|
|
656
|
+
input: process.stdin,
|
|
657
|
+
output: process.stdout,
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
try {
|
|
661
|
+
const answer = await rl.question(`${message} [Y/n] `);
|
|
662
|
+
const normalized = answer.trim().toLowerCase();
|
|
663
|
+
return normalized === "" || !["n", "no"].includes(normalized);
|
|
664
|
+
} finally {
|
|
665
|
+
rl.close();
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
|
|
557
669
|
const isCodexAppServerReady = async () => {
|
|
558
670
|
try {
|
|
559
|
-
const response = await fetch(
|
|
671
|
+
const response = await fetch(getReadyzUrl(), {
|
|
560
672
|
signal: AbortSignal.timeout(500),
|
|
561
673
|
});
|
|
562
674
|
return response.ok;
|
|
@@ -570,9 +682,26 @@ const ensureUiPortAvailable = async () => {
|
|
|
570
682
|
return;
|
|
571
683
|
}
|
|
572
684
|
|
|
573
|
-
|
|
574
|
-
|
|
685
|
+
const nextPort = await findNextFreePort("127.0.0.1", options.uiPort);
|
|
686
|
+
if (nextPort === null) {
|
|
687
|
+
throw new Error(
|
|
688
|
+
`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.`,
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const useAlternatePort = await confirmAlternatePort(
|
|
693
|
+
`[nomadexapp] UI port ${options.uiPort} is already in use. Use ${nextPort} instead?`,
|
|
694
|
+
);
|
|
695
|
+
if (!useAlternatePort) {
|
|
696
|
+
throw new Error(
|
|
697
|
+
`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.`,
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
console.log(
|
|
702
|
+
`[nomadexapp] UI port ${options.uiPort} is busy. Using ${nextPort} instead.`,
|
|
575
703
|
);
|
|
704
|
+
options.uiPort = nextPort;
|
|
576
705
|
};
|
|
577
706
|
|
|
578
707
|
const formatSpawnError = (error) => {
|
|
@@ -621,22 +750,50 @@ const ensureDistBuilt = () => {
|
|
|
621
750
|
};
|
|
622
751
|
|
|
623
752
|
const ensureAppServer = async () => {
|
|
624
|
-
|
|
753
|
+
const wsHost = getWsHost();
|
|
754
|
+
const wsPort = getWsPort();
|
|
755
|
+
const probeHost = getPortProbeHost(wsHost);
|
|
756
|
+
|
|
757
|
+
if (await isPortOpen(probeHost, wsPort)) {
|
|
625
758
|
if (await isCodexAppServerReady()) {
|
|
626
|
-
console.log(`[nomadexapp] Reusing Codex app-server at ${
|
|
759
|
+
console.log(`[nomadexapp] Reusing Codex app-server at ${wsUrl}`);
|
|
627
760
|
return;
|
|
628
761
|
}
|
|
629
762
|
|
|
630
|
-
|
|
631
|
-
|
|
763
|
+
if (!isLocalHost(wsHost)) {
|
|
764
|
+
throw new Error(
|
|
765
|
+
`Port ${wsPort} on ${wsHost} is already in use, but it is not responding like a Codex app-server.`,
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const nextPort = await findNextFreePort(probeHost, wsPort);
|
|
770
|
+
if (nextPort === null) {
|
|
771
|
+
throw new Error(
|
|
772
|
+
`Port ${wsPort} on ${wsHost} is already in use, and no nearby free local app-server port was found.`,
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const useAlternatePort = await confirmAlternatePort(
|
|
777
|
+
`[nomadexapp] App-server port ${wsPort} is in use by another process. Use ${nextPort} instead?`,
|
|
778
|
+
);
|
|
779
|
+
if (!useAlternatePort) {
|
|
780
|
+
throw new Error(
|
|
781
|
+
`Port ${wsPort} on ${wsHost} is already in use, but it is not responding like a Codex app-server.`,
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
wsUrl.port = String(nextPort);
|
|
786
|
+
options.wsUrl = wsUrl.toString();
|
|
787
|
+
console.log(
|
|
788
|
+
`[nomadexapp] App-server port ${wsPort} is busy. Using ${wsUrl} instead.`,
|
|
632
789
|
);
|
|
633
790
|
}
|
|
634
791
|
|
|
635
792
|
const codexLaunch = getCodexLaunch();
|
|
636
|
-
console.log(`[nomadexapp] Starting Codex app-server at ${
|
|
793
|
+
console.log(`[nomadexapp] Starting Codex app-server at ${wsUrl}`);
|
|
637
794
|
const appServer = spawn(
|
|
638
795
|
codexLaunch.command,
|
|
639
|
-
[...codexLaunch.args, "app-server", "--listen",
|
|
796
|
+
[...codexLaunch.args, "app-server", "--listen", wsUrl.toString()],
|
|
640
797
|
{
|
|
641
798
|
cwd: launchCwd,
|
|
642
799
|
stdio: "inherit",
|
|
@@ -661,7 +818,8 @@ const ensureAppServer = async () => {
|
|
|
661
818
|
}
|
|
662
819
|
if (appServer.exitCode !== null) {
|
|
663
820
|
const termuxHint =
|
|
664
|
-
isTermuxEnvironment() &&
|
|
821
|
+
isTermuxEnvironment() &&
|
|
822
|
+
!["PATH codex", "Termux codex"].includes(codexLaunch.source)
|
|
665
823
|
? " Termux detected. Install `@mmmbuto/codex-cli-termux@latest`, ensure `codex` is on PATH, or launch Nomadex with `--codex-cmd codex`."
|
|
666
824
|
: "";
|
|
667
825
|
throw new Error(
|
|
@@ -671,7 +829,7 @@ const ensureAppServer = async () => {
|
|
|
671
829
|
await sleep(200);
|
|
672
830
|
}
|
|
673
831
|
|
|
674
|
-
throw new Error(`Timed out waiting for Codex app-server at ${
|
|
832
|
+
throw new Error(`Timed out waiting for Codex app-server at ${wsUrl}`);
|
|
675
833
|
};
|
|
676
834
|
|
|
677
835
|
const sendText = (res, statusCode, message) => {
|
|
@@ -839,7 +997,6 @@ const getPreferredIp = () => {
|
|
|
839
997
|
};
|
|
840
998
|
|
|
841
999
|
const wsProxy = httpProxy.createProxyServer({
|
|
842
|
-
target: options.wsUrl,
|
|
843
1000
|
ws: true,
|
|
844
1001
|
changeOrigin: true,
|
|
845
1002
|
});
|
|
@@ -1065,7 +1222,9 @@ server.on("upgrade", (req, socket, head) => {
|
|
|
1065
1222
|
return;
|
|
1066
1223
|
}
|
|
1067
1224
|
req.url = "/";
|
|
1068
|
-
wsProxy.ws(req, socket, head
|
|
1225
|
+
wsProxy.ws(req, socket, head, {
|
|
1226
|
+
target: wsUrl.toString(),
|
|
1227
|
+
});
|
|
1069
1228
|
return;
|
|
1070
1229
|
}
|
|
1071
1230
|
} catch {
|