@montytools/cli 0.2.0 → 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.
- package/bin/monty.mjs +87 -7
- package/package.json +1 -1
- package/template/package.json +1 -1
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";
|
|
@@ -375,6 +376,7 @@ function installDeps() {
|
|
|
375
376
|
const pm = spawnSync("pnpm", ["--version"], { stdio: "ignore" }).status === 0 ? "pnpm" : "npm";
|
|
376
377
|
run(appDir, "install", [pm, "install"],
|
|
377
378
|
"Dependency install failed. Read the package manager error above; usually network or a bad package.json edit.");
|
|
379
|
+
ensureSdk(appDir);
|
|
378
380
|
}
|
|
379
381
|
|
|
380
382
|
function buildApp() {
|
|
@@ -396,18 +398,68 @@ function typecheckApp() {
|
|
|
396
398
|
}
|
|
397
399
|
|
|
398
400
|
|
|
401
|
+
// Probe by CONNECTING (not binding): SO_REUSEADDR lets a wildcard bind
|
|
402
|
+
// "succeed" on macOS even when a specific loopback address holds the port,
|
|
403
|
+
// so bind-probes lie. A successful connect = someone is listening.
|
|
404
|
+
function portTaken(host, port) {
|
|
405
|
+
return new Promise((resolve) => {
|
|
406
|
+
const sock = netConnect({ host, port });
|
|
407
|
+
const done = (taken) => {
|
|
408
|
+
sock.destroy();
|
|
409
|
+
resolve(taken);
|
|
410
|
+
};
|
|
411
|
+
sock.once("connect", () => done(true));
|
|
412
|
+
sock.once("error", () => done(false));
|
|
413
|
+
sock.setTimeout(400, () => done(false));
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
399
417
|
async function freePort(start) {
|
|
400
418
|
for (let p = start; p < start + 50; p++) {
|
|
401
|
-
|
|
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;
|
|
419
|
+
if (!(await portTaken("127.0.0.1", p)) && !(await portTaken("::1", p))) return p;
|
|
407
420
|
}
|
|
408
421
|
fail("NO_FREE_PORT", `No free port between ${start} and ${start + 49}. Pass --port <n>.`);
|
|
409
422
|
}
|
|
410
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
|
+
|
|
411
463
|
// ── monty dev ──────────────────────────────────────────────────────────────
|
|
412
464
|
// Development mode: vite locally + a Cloudflare quick tunnel registered as
|
|
413
465
|
// the app's DEV channel, so workspace admins see the app live (HMR included)
|
|
@@ -416,6 +468,7 @@ async function freePort(start) {
|
|
|
416
468
|
// clicks Publish in the workspace, this process builds + uploads for real.
|
|
417
469
|
async function dev() {
|
|
418
470
|
const appDir = requireAppDir("dev");
|
|
471
|
+
ensureSdk(appDir);
|
|
419
472
|
const meta = await compileConfig(appDir);
|
|
420
473
|
const cfg = loadConfig();
|
|
421
474
|
const host = cfg?.host ?? DEFAULT_HOST;
|
|
@@ -499,8 +552,14 @@ async function dev() {
|
|
|
499
552
|
const t = await startTunnel(port);
|
|
500
553
|
tunnelChild = t.child;
|
|
501
554
|
if (t.url) {
|
|
502
|
-
originUrl = t.url;
|
|
503
555
|
console.log(`tunnel: ${t.url}`);
|
|
556
|
+
console.log("tunnel: waiting for DNS to go live (prevents cached failures in your browser)…");
|
|
557
|
+
if (await waitForDns(t.url.replace("https://", ""))) {
|
|
558
|
+
originUrl = t.url;
|
|
559
|
+
console.log("tunnel: DNS live");
|
|
560
|
+
} else {
|
|
561
|
+
console.log("tunnel: DNS never propagated — dev mode registered on localhost (visible on this machine's browser only)");
|
|
562
|
+
}
|
|
504
563
|
} else {
|
|
505
564
|
console.log("tunnel: unavailable — dev mode registered on localhost (visible on this machine's browser only)");
|
|
506
565
|
}
|
|
@@ -532,6 +591,26 @@ async function dev() {
|
|
|
532
591
|
});
|
|
533
592
|
}
|
|
534
593
|
|
|
594
|
+
|
|
595
|
+
// trycloudflare DNS takes up to a couple of minutes to propagate. Registering
|
|
596
|
+
// the origin before it resolves would make admins' browsers cache NXDOMAIN
|
|
597
|
+
// (macOS negative cache ≈ 30 min of a broken iframe) — so gate on DNS via
|
|
598
|
+
// DoH, which never touches the local resolver.
|
|
599
|
+
async function waitForDns(hostname) {
|
|
600
|
+
for (let i = 0; i < 36; i++) {
|
|
601
|
+
try {
|
|
602
|
+
const r = await fetch(
|
|
603
|
+
`https://cloudflare-dns.com/dns-query?name=${hostname}&type=A`,
|
|
604
|
+
{ headers: { accept: "application/dns-json" } },
|
|
605
|
+
);
|
|
606
|
+
const d = await r.json();
|
|
607
|
+
if (Array.isArray(d.Answer) && d.Answer.some((a) => a.type === 1)) return true;
|
|
608
|
+
} catch { /* transient — retry */ }
|
|
609
|
+
await new Promise((res) => setTimeout(res, 5000));
|
|
610
|
+
}
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
|
|
535
614
|
// Cloudflare quick tunnel via the cloudflared npm wrapper (downloads the
|
|
536
615
|
// binary on first use). Resolves with the public URL, or null on failure —
|
|
537
616
|
// dev mode then falls back to localhost-only registration.
|
|
@@ -673,6 +752,7 @@ async function docs() {
|
|
|
673
752
|
// ── monty deploy ───────────────────────────────────────────────────────────
|
|
674
753
|
async function deploy() {
|
|
675
754
|
const appDir = requireAppDir("deploy");
|
|
755
|
+
ensureSdk(appDir);
|
|
676
756
|
if (!rest.includes("--from-dev")) {
|
|
677
757
|
console.log("note: direct deploy skips workspace review — the usual flow is `monty dev` + the Publish button in the workspace.");
|
|
678
758
|
}
|
package/package.json
CHANGED
package/template/package.json
CHANGED