@montytools/cli 0.2.5 → 0.2.6
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 +77 -25
- package/package.json +1 -1
package/bin/monty.mjs
CHANGED
|
@@ -17,6 +17,8 @@ import { CATALOG, REGISTRIES } from "./catalog.mjs";
|
|
|
17
17
|
const CONFIG_DIR = join(homedir(), ".monty");
|
|
18
18
|
const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
19
19
|
const DEFAULT_HOST = "https://usemonty.dev";
|
|
20
|
+
const DEV_SESSION_HEARTBEAT_MS = 30_000;
|
|
21
|
+
const DEV_SESSION_REQUEST_TIMEOUT_MS = 10_000;
|
|
20
22
|
// Every app's source lives in one predictable place. `monty create` stamps
|
|
21
23
|
// here by default (override with --dir) and `monty login` provisions it.
|
|
22
24
|
const MONTY_HOME = join(homedir(), "Monty");
|
|
@@ -511,23 +513,27 @@ async function dev() {
|
|
|
511
513
|
} catch { /* transient fs hiccup — next beat retries */ }
|
|
512
514
|
}
|
|
513
515
|
|
|
514
|
-
async function
|
|
515
|
-
if (ended) return;
|
|
516
|
-
ended = true;
|
|
517
|
-
if (hbTimer) clearInterval(hbTimer);
|
|
518
|
-
try { tunnelChild?.kill(); } catch { /* already gone */ }
|
|
516
|
+
async function clearDevSession(timeoutMs = 2000) {
|
|
519
517
|
if (cfg?.key) {
|
|
520
518
|
try {
|
|
521
519
|
await fetch(`${host}/api/dev-session`, {
|
|
522
520
|
method: "POST",
|
|
523
521
|
headers: { authorization: `Bearer ${cfg.key}`, "content-type": "application/json" },
|
|
524
522
|
body: JSON.stringify({ slug: meta.slug, end: true }),
|
|
525
|
-
signal: AbortSignal.timeout(
|
|
523
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
526
524
|
});
|
|
527
525
|
} catch { /* best effort */ }
|
|
528
526
|
}
|
|
529
527
|
}
|
|
530
528
|
|
|
529
|
+
async function endSession() {
|
|
530
|
+
if (ended) return;
|
|
531
|
+
ended = true;
|
|
532
|
+
if (hbTimer) clearInterval(hbTimer);
|
|
533
|
+
try { tunnelChild?.kill(); } catch { /* already gone */ }
|
|
534
|
+
await clearDevSession();
|
|
535
|
+
}
|
|
536
|
+
|
|
531
537
|
async function heartbeat(originUrl) {
|
|
532
538
|
await refreshSchemaIfChanged();
|
|
533
539
|
try {
|
|
@@ -542,11 +548,12 @@ async function dev() {
|
|
|
542
548
|
buildId,
|
|
543
549
|
schemaJson: currentMeta.schemaJson,
|
|
544
550
|
}),
|
|
551
|
+
signal: AbortSignal.timeout(DEV_SESSION_REQUEST_TIMEOUT_MS),
|
|
545
552
|
});
|
|
546
553
|
const data = await r.json().catch(() => null);
|
|
547
554
|
if (!r.ok) {
|
|
548
555
|
console.log(`dev-session: ${data?.code ?? r.status}${data?.fix ? ` — ${data.fix}` : ""}`);
|
|
549
|
-
return;
|
|
556
|
+
return false;
|
|
550
557
|
}
|
|
551
558
|
if (data?.publishRequested && !publishing) {
|
|
552
559
|
publishing = true;
|
|
@@ -567,7 +574,11 @@ async function dev() {
|
|
|
567
574
|
});
|
|
568
575
|
publishing = false;
|
|
569
576
|
}
|
|
570
|
-
|
|
577
|
+
return true;
|
|
578
|
+
} catch {
|
|
579
|
+
console.log("dev-session: heartbeat failed — retrying on the next beat");
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
571
582
|
}
|
|
572
583
|
|
|
573
584
|
async function startDevSession() {
|
|
@@ -575,27 +586,60 @@ async function dev() {
|
|
|
575
586
|
console.log("dev: not logged in — workspace dev mode disabled (run `monty login`)");
|
|
576
587
|
return;
|
|
577
588
|
}
|
|
589
|
+
await clearDevSession();
|
|
578
590
|
let originUrl = `http://localhost:${port}`;
|
|
591
|
+
let tunnelUpdate = Promise.resolve();
|
|
592
|
+
let tunnelVersion = 0;
|
|
593
|
+
async function activateTunnelUrl(url, { initial = false } = {}) {
|
|
594
|
+
const version = ++tunnelVersion;
|
|
595
|
+
if (!initial) {
|
|
596
|
+
console.log(`tunnel: changed to ${url}`);
|
|
597
|
+
await clearDevSession();
|
|
598
|
+
}
|
|
599
|
+
console.log("tunnel: waiting for DNS to go live (prevents cached failures in your browser)…");
|
|
600
|
+
const dnsLive = await waitForDns(url.replace("https://", ""));
|
|
601
|
+
if (ended || version !== tunnelVersion) return "superseded";
|
|
602
|
+
if (!dnsLive) {
|
|
603
|
+
if (initial) {
|
|
604
|
+
console.log("tunnel: DNS never propagated — dev mode registered on localhost (visible on this machine's browser only)");
|
|
605
|
+
} else {
|
|
606
|
+
console.log("tunnel: DNS never propagated for the new URL — keeping Studio offline until the next tunnel URL");
|
|
607
|
+
}
|
|
608
|
+
return "failed";
|
|
609
|
+
}
|
|
610
|
+
originUrl = url;
|
|
611
|
+
console.log(initial ? "tunnel: DNS live" : "tunnel: DNS live; Studio URL updated");
|
|
612
|
+
if (!initial && !(await heartbeat(originUrl))) {
|
|
613
|
+
console.log("dev-session: Studio still has no registered tunnel; the next heartbeat will retry");
|
|
614
|
+
}
|
|
615
|
+
return "activated";
|
|
616
|
+
}
|
|
617
|
+
const registerTunnelUrl = (url) => {
|
|
618
|
+
tunnelUpdate = tunnelUpdate.then(async () => {
|
|
619
|
+
if (ended || url === originUrl) return;
|
|
620
|
+
await activateTunnelUrl(url);
|
|
621
|
+
}).catch(() => {
|
|
622
|
+
console.log("tunnel: URL update failed — waiting for the next tunnel URL");
|
|
623
|
+
});
|
|
624
|
+
};
|
|
579
625
|
if (!rest.includes("--no-tunnel")) {
|
|
580
626
|
console.log("tunnel: starting (cloudflared quick tunnel)…");
|
|
581
|
-
const t = await startTunnel(port);
|
|
627
|
+
const t = await startTunnel(port, registerTunnelUrl);
|
|
582
628
|
tunnelChild = t.child;
|
|
583
629
|
if (t.url) {
|
|
584
630
|
console.log(`tunnel: ${t.url}`);
|
|
585
|
-
|
|
586
|
-
if (await waitForDns(t.url.replace("https://", ""))) {
|
|
587
|
-
originUrl = t.url;
|
|
588
|
-
console.log("tunnel: DNS live");
|
|
589
|
-
} else {
|
|
590
|
-
console.log("tunnel: DNS never propagated — dev mode registered on localhost (visible on this machine's browser only)");
|
|
591
|
-
}
|
|
631
|
+
await activateTunnelUrl(t.url, { initial: true });
|
|
592
632
|
} else {
|
|
593
633
|
console.log("tunnel: unavailable — dev mode registered on localhost (visible on this machine's browser only)");
|
|
594
634
|
}
|
|
595
635
|
}
|
|
596
|
-
await heartbeat(originUrl);
|
|
597
|
-
console.log(
|
|
598
|
-
|
|
636
|
+
const registered = await heartbeat(originUrl);
|
|
637
|
+
console.log(
|
|
638
|
+
registered
|
|
639
|
+
? `studio: ${host}/studio/${meta.slug} — your app is live there while this runs; click Publish to ship`
|
|
640
|
+
: `studio: waiting for ${host}/api/dev-session — leave this running; the next heartbeat will retry`,
|
|
641
|
+
);
|
|
642
|
+
hbTimer = setInterval(() => void heartbeat(originUrl), DEV_SESSION_HEARTBEAT_MS);
|
|
599
643
|
}
|
|
600
644
|
|
|
601
645
|
let announced = false;
|
|
@@ -629,6 +673,7 @@ async function resolvesVia(dohBase, hostname) {
|
|
|
629
673
|
try {
|
|
630
674
|
const r = await fetch(`${dohBase}?name=${hostname}&type=A`, {
|
|
631
675
|
headers: { accept: "application/dns-json" },
|
|
676
|
+
signal: AbortSignal.timeout(3000),
|
|
632
677
|
});
|
|
633
678
|
const d = await r.json();
|
|
634
679
|
return Array.isArray(d.Answer) && d.Answer.some((a) => a.type === 1);
|
|
@@ -658,7 +703,7 @@ async function waitForDns(hostname) {
|
|
|
658
703
|
// Cloudflare quick tunnel via the cloudflared npm wrapper (downloads the
|
|
659
704
|
// binary on first use). Resolves with the public URL, or null on failure —
|
|
660
705
|
// dev mode then falls back to localhost-only registration.
|
|
661
|
-
function startTunnel(port) {
|
|
706
|
+
function startTunnel(port, onUrlChange) {
|
|
662
707
|
return new Promise((resolve) => {
|
|
663
708
|
let child;
|
|
664
709
|
try {
|
|
@@ -675,12 +720,19 @@ function startTunnel(port) {
|
|
|
675
720
|
resolve({ child, url: null });
|
|
676
721
|
}
|
|
677
722
|
}, 45_000);
|
|
723
|
+
let currentUrl = null;
|
|
678
724
|
const scan = (chunk) => {
|
|
679
|
-
const
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
725
|
+
const urls = String(chunk).match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/g) ?? [];
|
|
726
|
+
for (const url of urls) {
|
|
727
|
+
if (url === currentUrl) continue;
|
|
728
|
+
currentUrl = url;
|
|
729
|
+
if (!settled) {
|
|
730
|
+
settled = true;
|
|
731
|
+
clearTimeout(timer);
|
|
732
|
+
resolve({ child, url });
|
|
733
|
+
} else {
|
|
734
|
+
onUrlChange?.(url);
|
|
735
|
+
}
|
|
684
736
|
}
|
|
685
737
|
};
|
|
686
738
|
child.stdout.on("data", scan);
|