@montytools/cli 0.2.0 → 0.2.1

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/monty.mjs CHANGED
@@ -7,6 +7,7 @@ import { spawn, spawnSync } from "node:child_process";
7
7
  import { randomBytes } from "node:crypto";
8
8
  import { cpSync, mkdirSync, readFileSync, rmSync, writeFileSync, readdirSync, statSync, existsSync } from "node:fs";
9
9
  import { createServer } from "node:http";
10
+ import { connect as netConnect } from "node:net";
10
11
  import { homedir } from "node:os";
11
12
  import { basename, dirname, join, relative } from "node:path";
12
13
  import { fileURLToPath } from "node:url";
@@ -396,14 +397,25 @@ function typecheckApp() {
396
397
  }
397
398
 
398
399
 
400
+ // Probe by CONNECTING (not binding): SO_REUSEADDR lets a wildcard bind
401
+ // "succeed" on macOS even when a specific loopback address holds the port,
402
+ // so bind-probes lie. A successful connect = someone is listening.
403
+ function portTaken(host, port) {
404
+ return new Promise((resolve) => {
405
+ const sock = netConnect({ host, port });
406
+ const done = (taken) => {
407
+ sock.destroy();
408
+ resolve(taken);
409
+ };
410
+ sock.once("connect", () => done(true));
411
+ sock.once("error", () => done(false));
412
+ sock.setTimeout(400, () => done(false));
413
+ });
414
+ }
415
+
399
416
  async function freePort(start) {
400
417
  for (let p = start; p < start + 50; p++) {
401
- const ok = await new Promise((resolve) => {
402
- const probe = createServer();
403
- probe.once("error", () => resolve(false));
404
- probe.listen(p, "127.0.0.1", () => probe.close(() => resolve(true)));
405
- });
406
- if (ok) return p;
418
+ if (!(await portTaken("127.0.0.1", p)) && !(await portTaken("::1", p))) return p;
407
419
  }
408
420
  fail("NO_FREE_PORT", `No free port between ${start} and ${start + 49}. Pass --port <n>.`);
409
421
  }
@@ -499,8 +511,14 @@ async function dev() {
499
511
  const t = await startTunnel(port);
500
512
  tunnelChild = t.child;
501
513
  if (t.url) {
502
- originUrl = t.url;
503
514
  console.log(`tunnel: ${t.url}`);
515
+ console.log("tunnel: waiting for DNS to go live (prevents cached failures in your browser)…");
516
+ if (await waitForDns(t.url.replace("https://", ""))) {
517
+ originUrl = t.url;
518
+ console.log("tunnel: DNS live");
519
+ } else {
520
+ console.log("tunnel: DNS never propagated — dev mode registered on localhost (visible on this machine's browser only)");
521
+ }
504
522
  } else {
505
523
  console.log("tunnel: unavailable — dev mode registered on localhost (visible on this machine's browser only)");
506
524
  }
@@ -532,6 +550,26 @@ async function dev() {
532
550
  });
533
551
  }
534
552
 
553
+
554
+ // trycloudflare DNS takes up to a couple of minutes to propagate. Registering
555
+ // the origin before it resolves would make admins' browsers cache NXDOMAIN
556
+ // (macOS negative cache ≈ 30 min of a broken iframe) — so gate on DNS via
557
+ // DoH, which never touches the local resolver.
558
+ async function waitForDns(hostname) {
559
+ for (let i = 0; i < 36; i++) {
560
+ try {
561
+ const r = await fetch(
562
+ `https://cloudflare-dns.com/dns-query?name=${hostname}&type=A`,
563
+ { headers: { accept: "application/dns-json" } },
564
+ );
565
+ const d = await r.json();
566
+ if (Array.isArray(d.Answer) && d.Answer.some((a) => a.type === 1)) return true;
567
+ } catch { /* transient — retry */ }
568
+ await new Promise((res) => setTimeout(res, 5000));
569
+ }
570
+ return false;
571
+ }
572
+
535
573
  // Cloudflare quick tunnel via the cloudflared npm wrapper (downloads the
536
574
  // binary on first use). Resolves with the public URL, or null on failure —
537
575
  // dev mode then falls back to localhost-only registration.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@montytools/cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "monty": "./bin/monty.mjs"
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "@fontsource-variable/roboto": "^5.2.10",
13
- "@montytools/sdk": "^0.1.0",
13
+ "@montytools/sdk": "^0.1.1",
14
14
  "@tanstack/react-router": "1.170.17",
15
15
  "class-variance-authority": "^0.7.1",
16
16
  "clsx": "^2.1.1",