nomadexapp 0.2.1 → 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.
Files changed (2) hide show
  1. package/bin/nomadex.mjs +114 -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,17 @@ 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 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,22 @@ 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
+
446
466
  const isTransientNpxCodexPath = (candidate) => {
447
467
  const normalized = candidate.replaceAll("\\", "/");
448
468
  return (
@@ -618,9 +638,37 @@ const isPortOpen = (targetHost, targetPort) =>
618
638
  socket.connect(targetPort, targetHost);
619
639
  });
620
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
+
621
669
  const isCodexAppServerReady = async () => {
622
670
  try {
623
- const response = await fetch(readyzUrl, {
671
+ const response = await fetch(getReadyzUrl(), {
624
672
  signal: AbortSignal.timeout(500),
625
673
  });
626
674
  return response.ok;
@@ -634,9 +682,26 @@ const ensureUiPortAvailable = async () => {
634
682
  return;
635
683
  }
636
684
 
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.`,
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.`,
639
703
  );
704
+ options.uiPort = nextPort;
640
705
  };
641
706
 
642
707
  const formatSpawnError = (error) => {
@@ -685,22 +750,50 @@ const ensureDistBuilt = () => {
685
750
  };
686
751
 
687
752
  const ensureAppServer = async () => {
688
- if (await isPortOpen(wsHost, wsPort)) {
753
+ const wsHost = getWsHost();
754
+ const wsPort = getWsPort();
755
+ const probeHost = getPortProbeHost(wsHost);
756
+
757
+ if (await isPortOpen(probeHost, wsPort)) {
689
758
  if (await isCodexAppServerReady()) {
690
- console.log(`[nomadexapp] Reusing Codex app-server at ${options.wsUrl}`);
759
+ console.log(`[nomadexapp] Reusing Codex app-server at ${wsUrl}`);
691
760
  return;
692
761
  }
693
762
 
694
- throw new Error(
695
- `Port ${wsPort} on ${wsHost} is already in use, but it is not responding like a Codex app-server.`,
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.`,
696
789
  );
697
790
  }
698
791
 
699
792
  const codexLaunch = getCodexLaunch();
700
- console.log(`[nomadexapp] Starting Codex app-server at ${options.wsUrl}`);
793
+ console.log(`[nomadexapp] Starting Codex app-server at ${wsUrl}`);
701
794
  const appServer = spawn(
702
795
  codexLaunch.command,
703
- [...codexLaunch.args, "app-server", "--listen", options.wsUrl],
796
+ [...codexLaunch.args, "app-server", "--listen", wsUrl.toString()],
704
797
  {
705
798
  cwd: launchCwd,
706
799
  stdio: "inherit",
@@ -725,7 +818,8 @@ const ensureAppServer = async () => {
725
818
  }
726
819
  if (appServer.exitCode !== null) {
727
820
  const termuxHint =
728
- isTermuxEnvironment() && codexLaunch.source !== "PATH codex"
821
+ isTermuxEnvironment() &&
822
+ !["PATH codex", "Termux codex"].includes(codexLaunch.source)
729
823
  ? " Termux detected. Install `@mmmbuto/codex-cli-termux@latest`, ensure `codex` is on PATH, or launch Nomadex with `--codex-cmd codex`."
730
824
  : "";
731
825
  throw new Error(
@@ -735,7 +829,7 @@ const ensureAppServer = async () => {
735
829
  await sleep(200);
736
830
  }
737
831
 
738
- throw new Error(`Timed out waiting for Codex app-server at ${options.wsUrl}`);
832
+ throw new Error(`Timed out waiting for Codex app-server at ${wsUrl}`);
739
833
  };
740
834
 
741
835
  const sendText = (res, statusCode, message) => {
@@ -903,7 +997,6 @@ const getPreferredIp = () => {
903
997
  };
904
998
 
905
999
  const wsProxy = httpProxy.createProxyServer({
906
- target: options.wsUrl,
907
1000
  ws: true,
908
1001
  changeOrigin: true,
909
1002
  });
@@ -1129,7 +1222,9 @@ server.on("upgrade", (req, socket, head) => {
1129
1222
  return;
1130
1223
  }
1131
1224
  req.url = "/";
1132
- wsProxy.ws(req, socket, head);
1225
+ wsProxy.ws(req, socket, head, {
1226
+ target: wsUrl.toString(),
1227
+ });
1133
1228
  return;
1134
1229
  }
1135
1230
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nomadexapp",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Remote browser workspace for local coding agents",
5
5
  "license": "MIT",
6
6
  "type": "module",