@montytools/cli 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/monty.mjs +65 -8
  2. package/package.json +1 -1
package/bin/monty.mjs CHANGED
@@ -376,6 +376,7 @@ function installDeps() {
376
376
  const pm = spawnSync("pnpm", ["--version"], { stdio: "ignore" }).status === 0 ? "pnpm" : "npm";
377
377
  run(appDir, "install", [pm, "install"],
378
378
  "Dependency install failed. Read the package manager error above; usually network or a bad package.json edit.");
379
+ ensureSdk(appDir);
379
380
  }
380
381
 
381
382
  function buildApp() {
@@ -420,6 +421,45 @@ async function freePort(start) {
420
421
  fail("NO_FREE_PORT", `No free port between ${start} and ${start + 49}. Pass --port <n>.`);
421
422
  }
422
423
 
424
+
425
+ // Apps pin @montytools/sdk at scaffold time and go stale — the CLI knows the
426
+ // minimum SDK its workflows need (e.g. tunnel-host allowlisting lives in the
427
+ // SDK's vite plugin) and upgrades the app automatically before dev/deploy.
428
+ const MIN_SDK = "0.1.1";
429
+
430
+ function installedSdkVersion(appDir) {
431
+ try {
432
+ return JSON.parse(
433
+ readFileSync(join(appDir, "node_modules", "@montytools", "sdk", "package.json"), "utf8"),
434
+ ).version;
435
+ } catch {
436
+ return null;
437
+ }
438
+ }
439
+
440
+ function semverLt(a, b) {
441
+ const pa = a.split(".").map(Number);
442
+ const pb = b.split(".").map(Number);
443
+ for (let i = 0; i < 3; i++) {
444
+ if ((pa[i] || 0) < (pb[i] || 0)) return true;
445
+ if ((pa[i] || 0) > (pb[i] || 0)) return false;
446
+ }
447
+ return false;
448
+ }
449
+
450
+ function ensureSdk(appDir) {
451
+ const v = installedSdkVersion(appDir);
452
+ if (v && !semverLt(v, MIN_SDK)) return;
453
+ console.log(
454
+ v
455
+ ? `sdk: installed ${v} < required ${MIN_SDK} — updating @montytools/sdk`
456
+ : "sdk: @montytools/sdk missing — installing",
457
+ );
458
+ const pm = spawnSync("pnpm", ["--version"], { stdio: "ignore" }).status === 0 ? "pnpm" : "npm";
459
+ run(appDir, "sdk-update", [pm, "install", "@montytools/sdk@latest"],
460
+ "Could not update @montytools/sdk. Run the install manually in the app folder, then retry.");
461
+ }
462
+
423
463
  // ── monty dev ──────────────────────────────────────────────────────────────
424
464
  // Development mode: vite locally + a Cloudflare quick tunnel registered as
425
465
  // the app's DEV channel, so workspace admins see the app live (HMR included)
@@ -428,6 +468,7 @@ async function freePort(start) {
428
468
  // clicks Publish in the workspace, this process builds + uploads for real.
429
469
  async function dev() {
430
470
  const appDir = requireAppDir("dev");
471
+ ensureSdk(appDir);
431
472
  const meta = await compileConfig(appDir);
432
473
  const cfg = loadConfig();
433
474
  const host = cfg?.host ?? DEFAULT_HOST;
@@ -555,16 +596,31 @@ async function dev() {
555
596
  // the origin before it resolves would make admins' browsers cache NXDOMAIN
556
597
  // (macOS negative cache ≈ 30 min of a broken iframe) — so gate on DNS via
557
598
  // DoH, which never touches the local resolver.
599
+ async function resolvesVia(dohBase, hostname) {
600
+ try {
601
+ const r = await fetch(`${dohBase}?name=${hostname}&type=A`, {
602
+ headers: { accept: "application/dns-json" },
603
+ });
604
+ const d = await r.json();
605
+ return Array.isArray(d.Answer) && d.Answer.some((a) => a.type === 1);
606
+ } catch {
607
+ return false;
608
+ }
609
+ }
610
+
558
611
  async function waitForDns(hostname) {
612
+ // Confirm on TWO independent public resolvers, then hold a grace period so
613
+ // slower home/ISP resolver chains catch up — an admin's browser asking one
614
+ // beat too early caches NXDOMAIN for a long time (macOS ≈ 30 min).
559
615
  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 */ }
616
+ const [cf, goog] = await Promise.all([
617
+ resolvesVia("https://cloudflare-dns.com/dns-query", hostname),
618
+ resolvesVia("https://dns.google/resolve", hostname),
619
+ ]);
620
+ if (cf && goog) {
621
+ await new Promise((res) => setTimeout(res, 10_000));
622
+ return true;
623
+ }
568
624
  await new Promise((res) => setTimeout(res, 5000));
569
625
  }
570
626
  return false;
@@ -711,6 +767,7 @@ async function docs() {
711
767
  // ── monty deploy ───────────────────────────────────────────────────────────
712
768
  async function deploy() {
713
769
  const appDir = requireAppDir("deploy");
770
+ ensureSdk(appDir);
714
771
  if (!rest.includes("--from-dev")) {
715
772
  console.log("note: direct deploy skips workspace review — the usual flow is `monty dev` + the Publish button in the workspace.");
716
773
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@montytools/cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "monty": "./bin/monty.mjs"