@montytools/cli 0.1.5 → 0.2.0
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 +207 -17
- package/package.json +1 -1
- package/skills/monty-build/SKILL.md +18 -9
- package/template/AGENTS.md +15 -5
package/bin/monty.mjs
CHANGED
|
@@ -362,36 +362,214 @@ async function create() {
|
|
|
362
362
|
|
|
363
363
|
installSkills({ appDir: target });
|
|
364
364
|
console.log(`created: ${target}`);
|
|
365
|
-
console.log(`next: cd ${target} &&
|
|
365
|
+
console.log(`next: cd ${target} && monty install && monty dev`);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
// ── monty install / build / typecheck ───────────────────────────────────────
|
|
370
|
+
// The full app lifecycle goes through the CLI — agents never invoke pnpm,
|
|
371
|
+
// vite, or tsc directly. Same underlying tools, agent-shaped output, and the
|
|
372
|
+
// build-before-typecheck ordering handled for you.
|
|
373
|
+
function installDeps() {
|
|
374
|
+
const appDir = requireAppDir("install");
|
|
375
|
+
const pm = spawnSync("pnpm", ["--version"], { stdio: "ignore" }).status === 0 ? "pnpm" : "npm";
|
|
376
|
+
run(appDir, "install", [pm, "install"],
|
|
377
|
+
"Dependency install failed. Read the package manager error above; usually network or a bad package.json edit.");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function buildApp() {
|
|
381
|
+
const appDir = requireAppDir("build");
|
|
382
|
+
run(appDir, "build", ["npx", "vite", "build"],
|
|
383
|
+
"The production build failed. Read the vite error above; it names the file to fix.");
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function typecheckApp() {
|
|
387
|
+
const appDir = requireAppDir("typecheck");
|
|
388
|
+
// routeTree.gen.ts is generated by the build — without it tsc fails on a
|
|
389
|
+
// fresh checkout, so build first when it's missing.
|
|
390
|
+
if (!existsSync(join(appDir, "src", "routeTree.gen.ts"))) {
|
|
391
|
+
run(appDir, "build", ["npx", "vite", "build"],
|
|
392
|
+
"The production build failed. Read the vite error above; it names the file to fix.");
|
|
393
|
+
}
|
|
394
|
+
run(appDir, "typecheck", ["npx", "tsc", "--noEmit"],
|
|
395
|
+
"TypeScript errors above. Fix them in the listed files.");
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
async function freePort(start) {
|
|
400
|
+
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;
|
|
407
|
+
}
|
|
408
|
+
fail("NO_FREE_PORT", `No free port between ${start} and ${start + 49}. Pass --port <n>.`);
|
|
366
409
|
}
|
|
367
410
|
|
|
368
411
|
// ── monty dev ──────────────────────────────────────────────────────────────
|
|
412
|
+
// Development mode: vite locally + a Cloudflare quick tunnel registered as
|
|
413
|
+
// the app's DEV channel, so workspace admins see the app live (HMR included)
|
|
414
|
+
// at usemonty.dev while it runs. Data is #dev-sandboxed automatically (vite
|
|
415
|
+
// dev build). The heartbeat doubles as the publish poll: when an owner
|
|
416
|
+
// clicks Publish in the workspace, this process builds + uploads for real.
|
|
369
417
|
async function dev() {
|
|
370
|
-
const appDir =
|
|
371
|
-
if (!existsSync(join(appDir, "monty.config.ts"))) {
|
|
372
|
-
fail("NOT_A_MONTY_APP", "No monty.config.ts here. Run `monty dev` from your app's root directory.");
|
|
373
|
-
}
|
|
418
|
+
const appDir = requireAppDir("dev");
|
|
374
419
|
const meta = await compileConfig(appDir);
|
|
375
|
-
const
|
|
376
|
-
const
|
|
420
|
+
const cfg = loadConfig();
|
|
421
|
+
const host = cfg?.host ?? DEFAULT_HOST;
|
|
422
|
+
// Auto-pick a free port (agents run several apps side by side); an
|
|
423
|
+
// explicit --port is honored strictly.
|
|
424
|
+
const requested = flag("port");
|
|
425
|
+
const port = requested ? Number(requested) : await freePort(5173);
|
|
377
426
|
|
|
378
427
|
console.log(`dev: starting vite on :${port} (app "${meta.slug}")`);
|
|
379
428
|
const child = spawn("npx", ["vite", "dev", "--port", String(port), "--strictPort"], {
|
|
380
429
|
cwd: appDir,
|
|
381
430
|
stdio: ["ignore", "pipe", "inherit"],
|
|
382
431
|
});
|
|
432
|
+
|
|
433
|
+
let tunnelChild = null;
|
|
434
|
+
let hbTimer = null;
|
|
435
|
+
let publishing = false;
|
|
436
|
+
let ended = false;
|
|
437
|
+
const buildFile = join(appDir, ".monty", "build");
|
|
438
|
+
const buildId = existsSync(buildFile) ? readFileSync(buildFile, "utf8").trim() : undefined;
|
|
439
|
+
|
|
440
|
+
async function endSession() {
|
|
441
|
+
if (ended) return;
|
|
442
|
+
ended = true;
|
|
443
|
+
if (hbTimer) clearInterval(hbTimer);
|
|
444
|
+
try { tunnelChild?.kill(); } catch { /* already gone */ }
|
|
445
|
+
if (cfg?.key) {
|
|
446
|
+
try {
|
|
447
|
+
await fetch(`${host}/api/dev-session`, {
|
|
448
|
+
method: "POST",
|
|
449
|
+
headers: { authorization: `Bearer ${cfg.key}`, "content-type": "application/json" },
|
|
450
|
+
body: JSON.stringify({ slug: meta.slug, end: true }),
|
|
451
|
+
signal: AbortSignal.timeout(2000),
|
|
452
|
+
});
|
|
453
|
+
} catch { /* best effort */ }
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async function heartbeat(originUrl) {
|
|
458
|
+
try {
|
|
459
|
+
const r = await fetch(`${host}/api/dev-session`, {
|
|
460
|
+
method: "POST",
|
|
461
|
+
headers: { authorization: `Bearer ${cfg.key}`, "content-type": "application/json" },
|
|
462
|
+
body: JSON.stringify({ slug: meta.slug, tunnelUrl: originUrl, name: meta.name, icon: meta.icon, buildId }),
|
|
463
|
+
});
|
|
464
|
+
const data = await r.json().catch(() => null);
|
|
465
|
+
if (!r.ok) {
|
|
466
|
+
console.log(`dev-session: ${data?.code ?? r.status}${data?.fix ? ` — ${data.fix}` : ""}`);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (data?.publishRequested && !publishing) {
|
|
470
|
+
publishing = true;
|
|
471
|
+
console.log("publish: requested from the workspace — building & uploading…");
|
|
472
|
+
await new Promise((resolve) => {
|
|
473
|
+
const pub = spawn(process.execPath, [fileURLToPath(import.meta.url), "deploy", "--from-dev"], {
|
|
474
|
+
cwd: appDir,
|
|
475
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
476
|
+
});
|
|
477
|
+
pub.on("exit", (code) => {
|
|
478
|
+
console.log(
|
|
479
|
+
code === 0
|
|
480
|
+
? "publish: done — the workspace now serves the new version (dev session continues)"
|
|
481
|
+
: "publish: FAILED — fix the errors above, then click Publish again",
|
|
482
|
+
);
|
|
483
|
+
resolve(undefined);
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
publishing = false;
|
|
487
|
+
}
|
|
488
|
+
} catch { /* transient network hiccup — next beat retries */ }
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async function startDevSession() {
|
|
492
|
+
if (!cfg?.key) {
|
|
493
|
+
console.log("dev: not logged in — workspace dev mode disabled (run `monty login`)");
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
let originUrl = `http://localhost:${port}`;
|
|
497
|
+
if (!rest.includes("--no-tunnel")) {
|
|
498
|
+
console.log("tunnel: starting (cloudflared quick tunnel)…");
|
|
499
|
+
const t = await startTunnel(port);
|
|
500
|
+
tunnelChild = t.child;
|
|
501
|
+
if (t.url) {
|
|
502
|
+
originUrl = t.url;
|
|
503
|
+
console.log(`tunnel: ${t.url}`);
|
|
504
|
+
} else {
|
|
505
|
+
console.log("tunnel: unavailable — dev mode registered on localhost (visible on this machine's browser only)");
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
await heartbeat(originUrl);
|
|
509
|
+
console.log(`workspace: ${host}/apps/${meta.slug} — DEV mode for admins while this runs; click Publish there to ship`);
|
|
510
|
+
hbTimer = setInterval(() => void heartbeat(originUrl), 30_000);
|
|
511
|
+
}
|
|
512
|
+
|
|
383
513
|
let announced = false;
|
|
384
514
|
child.stdout.on("data", (chunk) => {
|
|
385
515
|
const text = chunk.toString();
|
|
386
516
|
process.stdout.write(text);
|
|
387
517
|
if (!announced && /localhost:\d+/.test(text)) {
|
|
388
518
|
announced = true;
|
|
389
|
-
console.log(`open: ${host}/apps/${meta.slug}?dev=http://localhost:${port}`);
|
|
390
519
|
console.log(`data: sandboxed to "${meta.slug}#dev" (live records untouched)`);
|
|
391
520
|
console.log(`ready: http://localhost:${port}`);
|
|
521
|
+
void startDevSession();
|
|
392
522
|
}
|
|
393
523
|
});
|
|
394
|
-
child.on("exit", (code) =>
|
|
524
|
+
child.on("exit", (code) => {
|
|
525
|
+
void endSession().then(() => process.exit(code ?? 0));
|
|
526
|
+
});
|
|
527
|
+
process.on("SIGINT", () => {
|
|
528
|
+
void endSession().then(() => process.exit(130));
|
|
529
|
+
});
|
|
530
|
+
process.on("SIGTERM", () => {
|
|
531
|
+
void endSession().then(() => process.exit(143));
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Cloudflare quick tunnel via the cloudflared npm wrapper (downloads the
|
|
536
|
+
// binary on first use). Resolves with the public URL, or null on failure —
|
|
537
|
+
// dev mode then falls back to localhost-only registration.
|
|
538
|
+
function startTunnel(port) {
|
|
539
|
+
return new Promise((resolve) => {
|
|
540
|
+
let child;
|
|
541
|
+
try {
|
|
542
|
+
child = spawn("npx", ["-y", "cloudflared", "tunnel", "--url", `http://localhost:${port}`], {
|
|
543
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
544
|
+
});
|
|
545
|
+
} catch {
|
|
546
|
+
return resolve({ child: null, url: null });
|
|
547
|
+
}
|
|
548
|
+
let settled = false;
|
|
549
|
+
const timer = setTimeout(() => {
|
|
550
|
+
if (!settled) {
|
|
551
|
+
settled = true;
|
|
552
|
+
resolve({ child, url: null });
|
|
553
|
+
}
|
|
554
|
+
}, 45_000);
|
|
555
|
+
const scan = (chunk) => {
|
|
556
|
+
const m = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/.exec(String(chunk));
|
|
557
|
+
if (m && !settled) {
|
|
558
|
+
settled = true;
|
|
559
|
+
clearTimeout(timer);
|
|
560
|
+
resolve({ child, url: m[0] });
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
child.stdout.on("data", scan);
|
|
564
|
+
child.stderr.on("data", scan);
|
|
565
|
+
child.on("exit", () => {
|
|
566
|
+
if (!settled) {
|
|
567
|
+
settled = true;
|
|
568
|
+
clearTimeout(timer);
|
|
569
|
+
resolve({ child: null, url: null });
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
});
|
|
395
573
|
}
|
|
396
574
|
|
|
397
575
|
// ── monty add / components / docs ──────────────────────────────────────────
|
|
@@ -400,9 +578,9 @@ async function dev() {
|
|
|
400
578
|
// implementation. Only catalog registries and core shadcn resolve.
|
|
401
579
|
|
|
402
580
|
function requireAppDir(cmd) {
|
|
403
|
-
const appDir = process.cwd();
|
|
404
|
-
if (!
|
|
405
|
-
fail("NOT_A_MONTY_APP", `
|
|
581
|
+
const appDir = findAppRoot(process.cwd());
|
|
582
|
+
if (!appDir) {
|
|
583
|
+
fail("NOT_A_MONTY_APP", `Not inside a Monty app. \`monty apps\` lists local apps; cd "$(monty select <slug>)" jumps to one — then run \`monty ${cmd}\`.`);
|
|
406
584
|
}
|
|
407
585
|
return appDir;
|
|
408
586
|
}
|
|
@@ -494,9 +672,9 @@ async function docs() {
|
|
|
494
672
|
|
|
495
673
|
// ── monty deploy ───────────────────────────────────────────────────────────
|
|
496
674
|
async function deploy() {
|
|
497
|
-
const appDir =
|
|
498
|
-
if (!
|
|
499
|
-
|
|
675
|
+
const appDir = requireAppDir("deploy");
|
|
676
|
+
if (!rest.includes("--from-dev")) {
|
|
677
|
+
console.log("note: direct deploy skips workspace review — the usual flow is `monty dev` + the Publish button in the workspace.");
|
|
500
678
|
}
|
|
501
679
|
const config = loadConfig();
|
|
502
680
|
if (!config?.key) {
|
|
@@ -642,20 +820,32 @@ switch (command) {
|
|
|
642
820
|
installSkills({ appDir: findAppRoot(process.cwd()), silent: false });
|
|
643
821
|
console.log("skills: up to date");
|
|
644
822
|
break;
|
|
823
|
+
case "install":
|
|
824
|
+
installDeps();
|
|
825
|
+
break;
|
|
826
|
+
case "build":
|
|
827
|
+
buildApp();
|
|
828
|
+
break;
|
|
829
|
+
case "typecheck":
|
|
830
|
+
typecheckApp();
|
|
831
|
+
break;
|
|
645
832
|
case "deploy":
|
|
646
833
|
await deploy();
|
|
647
834
|
break;
|
|
648
835
|
default:
|
|
649
|
-
console.log("usage: monty <login|create|current|select|apps|dev|add|components|docs|deploy|skills>");
|
|
836
|
+
console.log("usage: monty <login|create|current|select|apps|install|dev|build|typecheck|add|components|docs|deploy|skills>");
|
|
650
837
|
console.log(" login [--host <url>] [--key <mk_...>] sign in (opens your browser to authorize)");
|
|
651
838
|
console.log(" create <slug> [--name N] [--icon I] [--build ID] stamp a new app into ~/Monty/<slug>");
|
|
652
|
-
console.log(" dev [--port
|
|
839
|
+
console.log(" dev [--port N] run locally, auto-picks a free port (sandboxed data)");
|
|
653
840
|
console.log(" add <name...> install curated UI components (see `monty components`)");
|
|
654
841
|
console.log(" components [query] list the curated component catalog");
|
|
655
842
|
console.log(" docs <name> view a component's source before installing");
|
|
656
843
|
console.log(" current which app folder am I in?");
|
|
657
844
|
console.log(" select <slug> print an app's folder — cd \"$(monty select x)\"");
|
|
658
845
|
console.log(" apps list local apps in ~/Monty");
|
|
846
|
+
console.log(" install install app dependencies");
|
|
847
|
+
console.log(" build production build (vite, via monty)");
|
|
848
|
+
console.log(" typecheck typecheck (builds first if needed)");
|
|
659
849
|
console.log(" deploy build + upload this app");
|
|
660
850
|
console.log(" skills install/refresh the agent build skill");
|
|
661
851
|
process.exit(command ? 1 : 0);
|
package/package.json
CHANGED
|
@@ -19,21 +19,29 @@ the territory.
|
|
|
19
19
|
2. **The loop:** `monty create <slug> --name "Name" --icon <lucide-icon>` →
|
|
20
20
|
(if the prompt includes a `build id`, pass it: `--build <id>` — the
|
|
21
21
|
workspace's New app screen tracks your progress live) →
|
|
22
|
-
edit `monty.config.ts` (zod tables) + `src/routes/` →
|
|
23
|
-
`monty dev` (
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
`monty install` → edit `monty.config.ts` (zod tables) + `src/routes/` →
|
|
23
|
+
verify with `monty dev` (auto-port, already authenticated, sandboxed
|
|
24
|
+
data). **You are done when `monty dev` prints the workspace dev URL and
|
|
25
|
+
the app works — leave `monty dev` running.** Publishing to the whole
|
|
26
|
+
workspace is the OWNER'S click (Publish in the workspace menu bar);
|
|
27
|
+
**never run `monty deploy` yourself** unless the user explicitly asks
|
|
28
|
+
for a direct production deploy.
|
|
29
|
+
3. **Everything through the CLI.** `monty install`, `monty build`,
|
|
30
|
+
`monty typecheck`, `monty dev`, `monty deploy` — never run vite, tsc,
|
|
31
|
+
pnpm, or npm scripts directly. `monty dev` auto-picks a free port and
|
|
32
|
+
prints it; `monty typecheck` builds first when needed.
|
|
33
|
+
4. **One import surface:** `@montytools/sdk` (`defineApp`, zod) and
|
|
26
34
|
`@montytools/sdk/react` (hooks: `useList`, `useInsert`, …). Never import
|
|
27
35
|
Clerk or Convex directly; never fetch external APIs from app code — the
|
|
28
36
|
platform CSP blocks them.
|
|
29
|
-
|
|
37
|
+
5. **Schema is zod in `monty.config.ts`.** Field names `_*`, `updatedAt`,
|
|
30
38
|
`createdBy` are reserved. Push happens automatically on dev/deploy.
|
|
31
|
-
|
|
39
|
+
6. **UI is stock shadcn** (preset already wired). Add curated components with
|
|
32
40
|
`monty add <name>`; browse with `monty components` / `monty docs <name>`.
|
|
33
|
-
|
|
41
|
+
7. **Errors are instructions.** Every failure prints
|
|
34
42
|
`[MontyError CODE] Fix: …` — do exactly what the Fix says; don't guess.
|
|
35
43
|
Typecheck failures block deploy by design.
|
|
36
|
-
|
|
44
|
+
8. **Verify before deploy.** `monty dev` writes to a `#dev` sandbox — live
|
|
37
45
|
team records are never touched, so exercise the app for real.
|
|
38
46
|
|
|
39
47
|
## CLI reference
|
|
@@ -43,7 +51,8 @@ the territory.
|
|
|
43
51
|
| `monty login` | browser sign-in (loopback authorize), once per machine |
|
|
44
52
|
| `monty create <slug>` | stamp a new app into `~/Monty/<slug>` |
|
|
45
53
|
| `monty current` / `select` / `apps` | where am I / jump to app / list local |
|
|
46
|
-
| `monty
|
|
54
|
+
| `monty install` / `build` / `typecheck` | full lifecycle via the CLI — no raw pnpm/vite/tsc |
|
|
55
|
+
| `monty dev` | run locally (auto-picks a free port), sandboxed data, auto-auth |
|
|
47
56
|
| `monty add <name…>` | install curated shadcn components |
|
|
48
57
|
| `monty deploy` | build + typecheck + upload; app appears in the workspace |
|
|
49
58
|
| `monty skills` | (re)install this skill for your agent |
|
package/template/AGENTS.md
CHANGED
|
@@ -102,14 +102,24 @@ Every platform error is one line shaped like:
|
|
|
102
102
|
|
|
103
103
|
## Dev loop
|
|
104
104
|
|
|
105
|
+
Everything goes through the `monty` CLI — never run vite, tsc, pnpm, or npm
|
|
106
|
+
scripts directly:
|
|
107
|
+
|
|
105
108
|
```
|
|
106
|
-
|
|
109
|
+
monty install # dependencies
|
|
110
|
+
monty dev # Vite + HMR, auto-picks a free port and prints it
|
|
107
111
|
```
|
|
108
112
|
|
|
109
|
-
Headless? Verify with `
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
+
Headless? Verify with `monty build` then `monty typecheck` (typecheck builds
|
|
114
|
+
first when needed — the build generates `src/routeTree.gen.ts`).
|
|
115
|
+
|
|
116
|
+
**Development vs production:** while `monty dev` runs, the app is live in
|
|
117
|
+
the workspace in DEV mode (workspace admins only, tunneled, `#dev` sandboxed
|
|
118
|
+
data). Shipping to the whole team is the owner's **Publish** click in the
|
|
119
|
+
workspace menu bar — it signals your running `monty dev`, which builds,
|
|
120
|
+
typechecks, and uploads. You are done when the app works in dev mode;
|
|
121
|
+
leave `monty dev` running and let the owner publish. Only run
|
|
122
|
+
`monty deploy` directly if the user explicitly asks.
|
|
113
123
|
|
|
114
124
|
**Driving your app in a browser (agents):** while `monty dev` runs, opening
|
|
115
125
|
`http://localhost:5173` is ALREADY AUTHENTICATED — no sign-in screen (the dev
|