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.
Files changed (2) hide show
  1. package/bin/nomadex.mjs +121 -19
  2. 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 || !process.stdin.isTTY || !process.stdout.isTTY) {
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 wsHost = wsUrl.hostname;
428
- const wsPort = Number(wsUrl.port || (wsUrl.protocol === "wss:" ? 443 : 80));
429
- const readyzUrl = (() => {
430
- const target = new URL(options.wsUrl);
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(readyzUrl, {
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
- throw new Error(
638
- `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.`,
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
- if (await isPortOpen(wsHost, wsPort)) {
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(`[nomadexapp] Reusing Codex app-server at ${options.wsUrl}`);
760
+ console.log(
761
+ `[nomadexapp] Reusing Codex app-server at ${formatWsBaseUrl()}`,
762
+ );
691
763
  return;
692
764
  }
693
765
 
694
- throw new Error(
695
- `Port ${wsPort} on ${wsHost} is already in use, but it is not responding like a Codex app-server.`,
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(`[nomadexapp] Starting Codex app-server at ${options.wsUrl}`);
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", options.wsUrl],
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() && codexLaunch.source !== "PATH codex"
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(`Timed out waiting for Codex app-server at ${options.wsUrl}`);
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nomadexapp",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Remote browser workspace for local coding agents",
5
5
  "license": "MIT",
6
6
  "type": "module",