@montytools/cli 0.1.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.
@@ -0,0 +1,75 @@
1
+ // The curated component catalog: one blessed, pre-vetted implementation per
2
+ // capability. Every entry is MIT-licensed, Radix-based, and styles itself
3
+ // through the app's semantic tokens — so it comes out in the Monty theme
4
+ // with no extra work. Bare names not listed here fall through to the core
5
+ // shadcn registry (dialog, tabs, popover, ...).
6
+
7
+ export const REGISTRIES = {
8
+ "@kibo-ui": "https://www.kibo-ui.com/r/{name}.json",
9
+ "@diceui": "https://diceui.com/r/{name}.json",
10
+ };
11
+
12
+ export const CATALOG = {
13
+ "data-table": {
14
+ item: "@diceui/data-table",
15
+ // upstream registryDependencies omit skeleton (data-table-skeleton.tsx needs it)
16
+ also: ["skeleton"],
17
+ use: "sortable/filterable/paginated table — default for any list of records",
18
+ },
19
+ kanban: {
20
+ item: "@kibo-ui/kanban",
21
+ use: "drag-and-drop board grouped by a status/enum field",
22
+ },
23
+ calendar: {
24
+ item: "@kibo-ui/calendar",
25
+ use: "month grid showing records that have a date field",
26
+ },
27
+ gantt: {
28
+ item: "@kibo-ui/gantt",
29
+ use: "timeline view for records with start and end dates",
30
+ },
31
+ combobox: {
32
+ item: "@kibo-ui/combobox",
33
+ use: "searchable select — use instead of <Select> when options exceed ~10",
34
+ },
35
+ "file-upload": {
36
+ item: "@kibo-ui/dropzone",
37
+ use: "drag-and-drop file upload area",
38
+ },
39
+ editor: {
40
+ item: "@kibo-ui/editor",
41
+ use: "rich text editor (notes, descriptions, long-form fields)",
42
+ },
43
+ tags: {
44
+ item: "@kibo-ui/tags",
45
+ use: "multi-value tag input (labels, categories)",
46
+ },
47
+ status: {
48
+ item: "@kibo-ui/status",
49
+ use: "colored status dot + label for enum states",
50
+ },
51
+ "avatar-stack": {
52
+ item: "@kibo-ui/avatar-stack",
53
+ use: "overlapping member avatars (pairs with useMembers)",
54
+ },
55
+ rating: {
56
+ item: "@kibo-ui/rating",
57
+ use: "star rating input/display",
58
+ },
59
+ chart: {
60
+ item: "chart",
61
+ use: "themed charts (bar/line/area/pie) for dashboards and KPIs",
62
+ },
63
+ sidebar: {
64
+ item: "sidebar",
65
+ use: "collapsible app sidebar — use when the app grows past one page",
66
+ },
67
+ command: {
68
+ item: "command",
69
+ use: "command palette / cmd-K search",
70
+ },
71
+ sonner: {
72
+ item: "sonner",
73
+ use: "toast notifications for action feedback",
74
+ },
75
+ };
package/bin/monty.mjs ADDED
@@ -0,0 +1,397 @@
1
+ #!/usr/bin/env node
2
+ // monty — the Monty platform CLI. Output is agent-shaped: structured
3
+ // single-line events, no spinners, instruction-shaped errors, and a
4
+ // deterministic final line (`deployed: …` / `error: …`).
5
+
6
+ import { spawn, spawnSync } from "node:child_process";
7
+ import { cpSync, mkdirSync, readFileSync, rmSync, writeFileSync, readdirSync, statSync, existsSync } from "node:fs";
8
+ import { homedir } from "node:os";
9
+ import { basename, dirname, join, relative } from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ import { createInterface } from "node:readline/promises";
12
+ import { CATALOG, REGISTRIES } from "./catalog.mjs";
13
+
14
+ const CONFIG_DIR = join(homedir(), ".monty");
15
+ const CONFIG_PATH = join(CONFIG_DIR, "config.json");
16
+ const DEFAULT_HOST = "https://usemonty.dev";
17
+
18
+ const [, , command, ...rest] = process.argv;
19
+
20
+ function flag(name) {
21
+ const i = rest.indexOf(`--${name}`);
22
+ return i >= 0 ? rest[i + 1] : undefined;
23
+ }
24
+
25
+ function fail(code, fix) {
26
+ console.error(`error: [MontyError ${code}] Fix: ${fix}`);
27
+ process.exit(1);
28
+ }
29
+
30
+ function loadConfig() {
31
+ try {
32
+ return JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ // ── monty login ────────────────────────────────────────────────────────────
39
+ async function login() {
40
+ const host = flag("host") ?? DEFAULT_HOST;
41
+ let key = flag("key");
42
+ if (!key) {
43
+ console.log(`open: ${host}/cli-auth (sign in, create a CLI key)`);
44
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
45
+ key = (await rl.question("paste key: ")).trim();
46
+ rl.close();
47
+ }
48
+ if (!/^mk_[0-9a-f]{48}$/.test(key)) {
49
+ fail("INVALID_CLI_KEY", `That does not look like a Monty CLI key (mk_ + 48 hex chars). Create one at ${host}/cli-auth.`);
50
+ }
51
+ mkdirSync(CONFIG_DIR, { recursive: true });
52
+ writeFileSync(CONFIG_PATH, JSON.stringify({ host, key }, null, 2) + "\n");
53
+ console.log(`logged-in: ${host} (key saved to ~/.monty/config.json)`);
54
+ }
55
+
56
+ // ── monty create ───────────────────────────────────────────────────────────
57
+ async function create() {
58
+ const slug = rest.find((a) => !a.startsWith("--"));
59
+ if (!slug || !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(slug)) {
60
+ fail("INVALID_SLUG", 'Usage: monty create <slug> — lowercase letters/digits with single hyphens (e.g. "standup-notes").');
61
+ }
62
+ const name =
63
+ flag("name") ??
64
+ slug.split("-").map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
65
+ const icon = flag("icon") ?? "layout-grid";
66
+ const target = join(process.cwd(), flag("dir") ?? slug);
67
+ if (existsSync(target)) {
68
+ fail("DIR_EXISTS", `${target} already exists. Pick another slug or remove the directory.`);
69
+ }
70
+
71
+ // Template is bundled into the published package (../template). Fall back to
72
+ // the monorepo path when running the CLI in-place during development.
73
+ const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
74
+ const templateDir = [
75
+ join(pkgRoot, "template"),
76
+ join(pkgRoot, "..", "template"),
77
+ ].find((d) => existsSync(join(d, "monty.config.ts")));
78
+ if (!templateDir) {
79
+ fail("TEMPLATE_MISSING", "The Monty app template is missing from this CLI install. Reinstall the monty CLI.");
80
+ }
81
+
82
+ console.log(`create: ${slug} -> ${target}`);
83
+ cpSync(templateDir, target, {
84
+ recursive: true,
85
+ filter: (src) => {
86
+ const base = basename(src);
87
+ return !["node_modules", "dist", ".monty", ".env.local", "routeTree.gen.ts"].includes(base);
88
+ },
89
+ });
90
+
91
+ // Stamp identity into the copied files.
92
+ const configPath = join(target, "monty.config.ts");
93
+ writeFileSync(
94
+ configPath,
95
+ readFileSync(configPath, "utf8")
96
+ .replace(/slug: "[^"]*"/, `slug: "${slug}"`)
97
+ .replace(/name: "[^"]*"/, `name: "${name}"`)
98
+ .replace(/icon: "[^"]*"/, `icon: "${icon}"`),
99
+ );
100
+ const pkgPath = join(target, "package.json");
101
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
102
+ pkg.name = slug;
103
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
104
+ const htmlPath = join(target, "index.html");
105
+ writeFileSync(
106
+ htmlPath,
107
+ readFileSync(htmlPath, "utf8").replace(/<title>[^<]*<\/title>/, `<title>${name}</title>`),
108
+ );
109
+
110
+ // Client config comes from the platform — public values, no dashboard trip.
111
+ const host = loadConfig()?.host ?? DEFAULT_HOST;
112
+ try {
113
+ const cfg = await fetch(`${host}/api/config`).then((r) => r.json());
114
+ writeFileSync(
115
+ join(target, ".env.local"),
116
+ `VITE_CONVEX_URL=${cfg.convexUrl}\nVITE_CLERK_PUBLISHABLE_KEY=${cfg.clerkPublishableKey}\n`,
117
+ );
118
+ console.log(`config: .env.local written from ${host}/api/config`);
119
+ } catch {
120
+ console.log(`config: WARNING — could not reach ${host}/api/config; copy .env.example to .env.local manually`);
121
+ }
122
+
123
+ console.log(`created: ${target}`);
124
+ console.log(`next: cd ${relative(process.cwd(), target)} && pnpm install && monty dev`);
125
+ }
126
+
127
+ // ── monty dev ──────────────────────────────────────────────────────────────
128
+ async function dev() {
129
+ const appDir = process.cwd();
130
+ if (!existsSync(join(appDir, "monty.config.ts"))) {
131
+ fail("NOT_A_MONTY_APP", "No monty.config.ts here. Run `monty dev` from your app's root directory.");
132
+ }
133
+ const meta = await compileConfig(appDir);
134
+ const host = loadConfig()?.host ?? DEFAULT_HOST;
135
+ const port = Number(flag("port") ?? 5173);
136
+
137
+ console.log(`dev: starting vite on :${port} (app "${meta.slug}")`);
138
+ const child = spawn("npx", ["vite", "dev", "--port", String(port), "--strictPort"], {
139
+ cwd: appDir,
140
+ stdio: ["ignore", "pipe", "inherit"],
141
+ });
142
+ let announced = false;
143
+ child.stdout.on("data", (chunk) => {
144
+ const text = chunk.toString();
145
+ process.stdout.write(text);
146
+ if (!announced && /localhost:\d+/.test(text)) {
147
+ announced = true;
148
+ console.log(`open: ${host}/apps/${meta.slug}?dev=http://localhost:${port}`);
149
+ console.log(`data: sandboxed to "${meta.slug}#dev" (live records untouched)`);
150
+ console.log(`ready: http://localhost:${port}`);
151
+ }
152
+ });
153
+ child.on("exit", (code) => process.exit(code ?? 0));
154
+ }
155
+
156
+ // ── monty add / components / docs ──────────────────────────────────────────
157
+ // Wraps the shadcn CLI behind the curated catalog: agents ask for a
158
+ // capability by plain name ("kanban") and get the blessed, theme-compatible
159
+ // implementation. Only catalog registries and core shadcn resolve.
160
+
161
+ function requireAppDir(cmd) {
162
+ const appDir = process.cwd();
163
+ if (!existsSync(join(appDir, "monty.config.ts"))) {
164
+ fail("NOT_A_MONTY_APP", `No monty.config.ts here. Run \`monty ${cmd}\` from your app's root directory.`);
165
+ }
166
+ return appDir;
167
+ }
168
+
169
+ // "kanban" -> "@kibo-ui/kanban"; bare unlisted names pass through to core
170
+ // shadcn; explicit @namespace items must belong to a supported registry.
171
+ function resolveComponent(name) {
172
+ if (CATALOG[name]) return CATALOG[name].item;
173
+ if (name.startsWith("@")) {
174
+ const ns = name.split("/")[0];
175
+ if (ns !== "@shadcn" && !REGISTRIES[ns]) {
176
+ fail("UNSUPPORTED_REGISTRY", `${ns} is not a supported registry. Run \`monty components\` for the curated catalog; core shadcn components install by bare name.`);
177
+ }
178
+ }
179
+ return name;
180
+ }
181
+
182
+ async function add() {
183
+ const appDir = requireAppDir("add");
184
+ const names = rest.filter((a) => !a.startsWith("--"));
185
+ if (names.length === 0) {
186
+ fail("NO_COMPONENT", "Usage: monty add <name...> — run `monty components` to see what's available.");
187
+ }
188
+ const items = names.flatMap((n) => [resolveComponent(n), ...(CATALOG[n]?.also ?? [])]);
189
+
190
+ // --overwrite so shadcn never halts on a per-file prompt (an aborted prompt
191
+ // cancels ALL remaining writes). Existing files win instead: snapshot them
192
+ // and restore any the install replaced — the app's themed primitives stay,
193
+ // new files land alongside.
194
+ const snapshot = new Map();
195
+ for (const dir of ["src/components", "src/hooks", "src/lib"]) {
196
+ const abs = join(appDir, dir);
197
+ if (existsSync(abs)) for (const f of walk(abs)) snapshot.set(f, readFileSync(f));
198
+ }
199
+ run(appDir, "add", ["pnpm", "dlx", "shadcn@latest", "add", "--yes", "--overwrite", ...items],
200
+ `Could not add ${items.join(", ")}. If the name was a guess, run \`monty components\` for the curated catalog — core shadcn components (dialog, tabs, dropdown-menu, ...) install by bare name.`);
201
+
202
+ // The shadcn CLI rewrites imports and mangles paths outside its known
203
+ // aliases (e.g. @/config/*, @/types/*). The template's layout matches
204
+ // registry sources exactly, so for third-party items restore each newly
205
+ // created file verbatim from the registry JSON.
206
+ for (const item of items) {
207
+ const [ns, name] = item.split("/");
208
+ const urlTemplate = REGISTRIES[ns];
209
+ if (!urlTemplate) continue;
210
+ const spec = await fetch(urlTemplate.replace("{name}", name)).then((r) => r.json()).catch(() => null);
211
+ for (const file of spec?.files ?? []) {
212
+ if (typeof file.content !== "string") continue;
213
+ const dest = join(appDir, (file.target || file.path).replace(/^~\//, ""));
214
+ if (existsSync(dest) && !snapshot.has(dest)) writeFileSync(dest, file.content);
215
+ }
216
+ }
217
+
218
+ let kept = 0;
219
+ for (const [file, buf] of snapshot) {
220
+ if (existsSync(file) && !readFileSync(file).equals(buf)) {
221
+ writeFileSync(file, buf);
222
+ kept++;
223
+ }
224
+ }
225
+ if (kept > 0) console.log(`kept: ${kept} existing file(s) unchanged (your themed versions win)`);
226
+ console.log(`added: ${items.join(", ")} -> src/components (already themed; import and compose)`);
227
+ }
228
+
229
+ function components() {
230
+ const query = rest.filter((a) => !a.startsWith("--")).join(" ").toLowerCase();
231
+ const entries = Object.entries(CATALOG).filter(
232
+ ([name, { item, use }]) => !query || `${name} ${item} ${use}`.toLowerCase().includes(query),
233
+ );
234
+ const width = Math.max(...Object.keys(CATALOG).map((n) => n.length));
235
+ const itemWidth = Math.max(...Object.values(CATALOG).map((c) => c.item.length));
236
+ console.log(`components: ${entries.length} curated (install with \`monty add <name>\`)`);
237
+ for (const [name, { item, use }] of entries) {
238
+ console.log(` ${name.padEnd(width)} ${item.padEnd(itemWidth)} ${use}`);
239
+ }
240
+ console.log("core: any shadcn component installs by bare name (dialog, tabs, dropdown-menu, popover, tooltip, sheet, checkbox, textarea, ...)");
241
+ console.log("docs: `monty docs <name>` shows a component's source before installing");
242
+ }
243
+
244
+ async function docs() {
245
+ const appDir = requireAppDir("docs");
246
+ const name = rest.find((a) => !a.startsWith("--"));
247
+ if (!name) {
248
+ fail("NO_COMPONENT", "Usage: monty docs <name> — run `monty components` to see what's available.");
249
+ }
250
+ run(appDir, "docs", ["pnpm", "dlx", "shadcn@latest", "view", resolveComponent(name)],
251
+ `Could not view ${name}. Run \`monty components\` to see the curated catalog.`);
252
+ }
253
+
254
+ // ── monty deploy ───────────────────────────────────────────────────────────
255
+ async function deploy() {
256
+ const appDir = process.cwd();
257
+ if (!existsSync(join(appDir, "monty.config.ts"))) {
258
+ fail("NOT_A_MONTY_APP", "No monty.config.ts here. Run `monty deploy` from your app's root directory.");
259
+ }
260
+ const config = loadConfig();
261
+ if (!config?.key) {
262
+ fail("NOT_LOGGED_IN", "Run `monty login` first (create a key at /cli-auth in the Monty host).");
263
+ }
264
+
265
+ // 1) Compile monty.config.ts → { slug, name, icon, schemaJson } using the
266
+ // app's OWN zod/sdk instances (esbuild-bundled, run in a subprocess).
267
+ console.log("compile: monty.config.ts");
268
+ const meta = await compileConfig(appDir);
269
+ console.log(`compile: ok (app "${meta.slug}", ${Object.keys(meta.schemaJson.tables).length} tables)`);
270
+
271
+ // 2) Fail fast locally before any upload. Build FIRST — it also generates
272
+ // src/routeTree.gen.ts, without which tsc fails on a fresh checkout.
273
+ run(appDir, "build", ["npx", "vite", "build"],
274
+ "The production build failed. Read the vite error above; it names the file to fix.");
275
+ run(appDir, "typecheck", ["npx", "tsc", "--noEmit"],
276
+ "TypeScript errors above. Fix them in the listed files; `monty deploy` never uploads code that does not compile.");
277
+
278
+ // 3) Multipart POST to the host.
279
+ const dist = join(appDir, "dist");
280
+ const files = walk(dist);
281
+ const form = new FormData();
282
+ form.set("monty", JSON.stringify(meta));
283
+ let total = 0;
284
+ for (const file of files) {
285
+ const rel = relative(dist, file).split("\\").join("/");
286
+ const buf = readFileSync(file);
287
+ total += buf.byteLength;
288
+ form.set(rel, new Blob([buf]), rel);
289
+ }
290
+ console.log(`upload: ${files.length} files, ${(total / 1024).toFixed(0)} KB -> ${config.host}/api/deploy`);
291
+ const res = await fetch(`${config.host}/api/deploy`, {
292
+ method: "POST",
293
+ headers: { authorization: `Bearer ${config.key}` },
294
+ body: form,
295
+ });
296
+ const body = await res.json().catch(() => null);
297
+ if (!res.ok || !body?.ok) {
298
+ fail(body?.code ?? `HTTP_${res.status}`, body?.fix ?? "Unexpected server response — is the Monty host reachable?");
299
+ }
300
+ console.log(`origin: ${body.origin}`);
301
+ console.log(`deployed: ${body.url} (version ${body.version})`);
302
+ }
303
+
304
+ async function compileConfig(appDir) {
305
+ const { build } = await import("esbuild");
306
+ const tmpDir = join(appDir, ".monty");
307
+ mkdirSync(tmpDir, { recursive: true });
308
+ const entry = join(tmpDir, "compile-entry.mjs");
309
+ const out = join(tmpDir, "compile-out.mjs");
310
+ writeFileSync(entry, [
311
+ `import { app } from "../monty.config";`,
312
+ `import { compileApp } from "@montytools/sdk/compile";`,
313
+ `process.stdout.write(JSON.stringify(compileApp(app)));`,
314
+ ].join("\n"));
315
+ try {
316
+ await build({
317
+ entryPoints: [entry],
318
+ outfile: out,
319
+ bundle: true,
320
+ platform: "node",
321
+ format: "esm",
322
+ target: "node22",
323
+ absWorkingDir: appDir,
324
+ logLevel: "silent",
325
+ });
326
+ const result = spawnSync(process.execPath, [out], { encoding: "utf8" });
327
+ if (result.status !== 0) {
328
+ fail("CONFIG_COMPILE_FAILED", `monty.config.ts threw while loading:\n${result.stderr}\nFix the config (it must only call defineApp with zod tables).`);
329
+ }
330
+ return JSON.parse(result.stdout);
331
+ } catch (e) {
332
+ if (e?.errors) {
333
+ fail("CONFIG_COMPILE_FAILED", `esbuild could not bundle monty.config.ts: ${e.errors[0]?.text ?? e.message}`);
334
+ }
335
+ throw e;
336
+ } finally {
337
+ rmSync(entry, { force: true });
338
+ rmSync(out, { force: true });
339
+ }
340
+ }
341
+
342
+ function run(cwd, label, argv, fixOnFail) {
343
+ console.log(`${label}: ${argv.join(" ")}`);
344
+ // stdin ignored: interactive prompts (e.g. shadcn's per-file overwrite
345
+ // confirmation) take their safe default instead of blocking an agent.
346
+ const result = spawnSync(argv[0], argv.slice(1), { cwd, stdio: ["ignore", "inherit", "inherit"] });
347
+ if (result.status !== 0) {
348
+ fail(label.toUpperCase() + "_FAILED", fixOnFail);
349
+ }
350
+ console.log(`${label}: ok`);
351
+ }
352
+
353
+ function walk(dir) {
354
+ const out = [];
355
+ for (const name of readdirSync(dir)) {
356
+ const p = join(dir, name);
357
+ if (statSync(p).isDirectory()) out.push(...walk(p));
358
+ else out.push(p);
359
+ }
360
+ return out;
361
+ }
362
+
363
+ // ── dispatch ───────────────────────────────────────────────────────────────
364
+ switch (command) {
365
+ case "login":
366
+ await login();
367
+ break;
368
+ case "create":
369
+ await create();
370
+ break;
371
+ case "dev":
372
+ await dev();
373
+ break;
374
+ case "add":
375
+ await add();
376
+ break;
377
+ case "components":
378
+ case "search":
379
+ components();
380
+ break;
381
+ case "docs":
382
+ await docs();
383
+ break;
384
+ case "deploy":
385
+ await deploy();
386
+ break;
387
+ default:
388
+ console.log("usage: monty <login|create|dev|add|components|docs|deploy>");
389
+ console.log(" login --host <url> --key <mk_...> save CLI credentials");
390
+ console.log(" create <slug> [--name N] [--icon I] stamp a new app from the template");
391
+ console.log(" dev [--port 5173] run the app locally (sandboxed data)");
392
+ console.log(" add <name...> install curated UI components (see `monty components`)");
393
+ console.log(" components [query] list the curated component catalog");
394
+ console.log(" docs <name> view a component's source before installing");
395
+ console.log(" deploy build + upload this app");
396
+ process.exit(command ? 1 : 0);
397
+ }
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@montytools/cli",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "monty": "./bin/monty.mjs"
7
+ },
8
+ "files": [
9
+ "bin",
10
+ "template"
11
+ ],
12
+ "engines": {
13
+ "node": ">=22"
14
+ },
15
+ "scripts": {
16
+ "prepack": "node scripts/bundle-template.mjs",
17
+ "typecheck": "node --check bin/monty.mjs"
18
+ },
19
+ "dependencies": {
20
+ "esbuild": "^0.28.1"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ }
25
+ }
@@ -0,0 +1,3 @@
1
+ # Copy to .env.local (gitignored). `monty dev` will manage this for you later.
2
+ VITE_CONVEX_URL=https://YOUR-DEPLOYMENT.convex.cloud
3
+ VITE_CLERK_PUBLISHABLE_KEY=pk_test_REPLACE_ME
@@ -0,0 +1,133 @@
1
+ # You are building a Monty app
2
+
3
+ Monty is a work OS: your app runs inside a team's workspace, on shared reactive
4
+ data, with auth and deployment handled by the platform. **You only write product
5
+ logic.** Everything below is the complete contract.
6
+
7
+ ## The three files that matter
8
+
9
+ | File | What it is |
10
+ |---|---|
11
+ | `monty.config.ts` | Your data schema — plain zod. The ONLY place data shapes are defined. Also `name` + `icon` (any [lucide](https://lucide.dev/icons) icon name, e.g. `"receipt"`, `"users"`) — Monty renders your app's logo tile from it. |
12
+ | `src/routes/` | Your UI — TanStack Router file routes (`index.tsx` = `/`). |
13
+ | `src/main.tsx` | Wiring. Do not edit. |
14
+
15
+ ## Rules (violations break the app)
16
+
17
+ 1. **Never import `convex`, `@clerk/*`, or talk to a database or API directly.**
18
+ All data goes through `@montytools/sdk/react` hooks. Auth, tenancy, and realtime
19
+ are handled below you — never write them.
20
+ 2. **Never ask for or pass a workspace/tenant id.** The platform injects it
21
+ from the verified session; it is not part of any API you can see.
22
+ 3. **UI is shadcn/ui, preconfigured — never build components from scratch.**
23
+ Before building any UI piece, run `monty components`. If the capability is
24
+ listed (data tables, kanban, calendar, combobox, file upload, rich text,
25
+ charts, …) install the curated implementation with `monty add <name>`;
26
+ core shadcn components install by bare name (`monty add dialog tabs`).
27
+ Everything lands in `src/components/ui/*` already carrying the Monty
28
+ theme. `monty docs <name>` shows a component's source before installing.
29
+ Icons from `lucide-react`. Don't install other component libraries or
30
+ write raw-color CSS — use semantic tokens (`bg-background`,
31
+ `text-muted-foreground`, …). Don't edit `src/index.css` theme tokens.
32
+ 4. **Schema changes = edit `monty.config.ts` and save.** Types update
33
+ immediately. Prefer additive changes; give new fields `.optional()` or
34
+ `.default(...)` so existing records stay readable.
35
+
36
+ ## Data: define, then use
37
+
38
+ ```ts
39
+ // monty.config.ts
40
+ export const app = defineApp({
41
+ slug: "expenses",
42
+ tables: {
43
+ expenses: z.object({
44
+ title: z.string().min(1),
45
+ amount: z.number().positive(),
46
+ status: z.enum(["draft", "submitted", "approved"]).default("draft"),
47
+ assigneeId: z.string().optional(), // userId from useMembers()
48
+ }),
49
+ },
50
+ });
51
+ ```
52
+
53
+ ```tsx
54
+ import { useList, useRecord, useInsert, useUpdate, useRemove, useMembers } from "@montytools/sdk/react";
55
+ import { app } from "../../monty.config";
56
+
57
+ const { data, status, loadMore } = useList(app, "expenses", {
58
+ filter: { status: "submitted" }, // equality on schema fields only
59
+ order: "desc", // by creation time
60
+ limit: 50,
61
+ });
62
+ // data: rows typed from your zod schema + { _id, _creationTime, updatedAt, createdBy }
63
+ // status: "LoadingFirstPage" | "CanLoadMore" | "LoadingMore" | "Exhausted"
64
+ // LIVE: any teammate's write re-renders this. Never poll, refetch, or cache.
65
+
66
+ const insert = useInsert(app, "expenses");
67
+ await insert({ title: "Lunch", amount: 12 }); // defaults applied, validated
68
+
69
+ const update = useUpdate(app, "expenses");
70
+ await update(row._id, { status: "approved" }); // shallow-merge patch
71
+ await update(row._id, { assigneeId: null }); // null CLEARS an .optional() field
72
+ // Counters: read-modify-write (`update(id, { votes: row.votes + 1 })`) is the
73
+ // blessed pattern. It is last-write-wins — two simultaneous clicks can lose
74
+ // one. Fine for votes/likes; if exact counts matter, model events as ROWS in
75
+ // their own table and count client-side.
76
+
77
+ const remove = useRemove(app, "expenses");
78
+ await remove(row._id);
79
+
80
+ const one = useRecord(app, "expenses", idOrNull); // row | null(missing or no selection) | undefined(loading)
81
+ const members = useMembers(); // [{ userId, name, email, imageUrl, role }]
82
+ ```
83
+
84
+ ## Errors are instructions
85
+
86
+ Every platform error is one line shaped like:
87
+ `[MontyError VALIDATION] at expenses.status: expected "draft"|"submitted"|"approved", got "done". Fix: ...`
88
+
89
+ **Do exactly what the `Fix:` says.** Codes you may see:
90
+
91
+ | Code | Meaning |
92
+ |---|---|
93
+ | `VALIDATION` | Payload doesn't match your zod schema (unknown fields are also an error) — fix the payload or the schema. |
94
+ | `UNKNOWN_TABLE` | Table name not in `monty.config.ts` `tables`. |
95
+ | `NOT_FOUND` | Stale, foreign, or WRONG-TABLE record id — ids come from `useList`/`useRecord` on the same table; never hard-code or mix them. |
96
+ | `INVALID_SCHEMA` | A table field uses a reserved name (`_*`, `updatedAt`, `createdBy`) — rename it. |
97
+ | `SCHEMA_DRIFT` (warning) | Stored rows predate your latest schema change; nothing crashes, but make changed fields `.optional()`/`.default(...)`. |
98
+ | `UNAUTHENTICATED` / `NO_ACTIVE_WORKSPACE` | App isn't running through the Monty host/dev shell. |
99
+ | `MISSING_ENV` / `NO_PROVIDER` | `.env.local` or the `<MontyProvider>` in `main.tsx` was removed. |
100
+
101
+ ## Dev loop
102
+
103
+ ```
104
+ pnpm dev # Vite + HMR on :5173; sign in with your workspace account
105
+ ```
106
+
107
+ Headless? Verify with `pnpm exec vite build` then `pnpm exec tsc --noEmit` — in
108
+ that order: the first build generates `src/routeTree.gen.ts`, without which
109
+ typecheck fails on a fresh app. `monty deploy` runs both itself (it never
110
+ uploads code that doesn't compile), so deploy is self-verifying.
111
+
112
+ **Driving your app in a browser (agents):** while `monty dev` runs, opening
113
+ `http://localhost:5173` is ALREADY AUTHENTICATED — no sign-in screen (the dev
114
+ server mints short-lived workspace tokens from the CLI login). Point
115
+ Playwright or any browser automation at it, click through your app against
116
+ live sandboxed data, and read your errors in the `monty dev` terminal
117
+ (`[browser:error] …` lines). Edit → HMR → look → fix: verify your own work.
118
+
119
+ Dev writes go to a sandboxed `#dev` namespace inside your real workspace —
120
+ iterate freely, live app data is untouched. The "DEV · … · sandbox data" badge
121
+ confirms it.
122
+
123
+ ## Modeling tips
124
+
125
+ - Rows, not embedded arrays: a comments thread is a `comments` table with a
126
+ `parentId` field, not an array inside a record (records cap at 1 MiB).
127
+ - People are `z.string()` userIds from `useMembers()`; render names/avatars
128
+ from the member list.
129
+ - Timestamps are `z.number()` epochs or `z.iso.datetime()` strings — never
130
+ `z.date()`.
131
+ - Field names `_*`, `updatedAt`, `createdBy` are reserved for system fields.
132
+ - Cross-field `.refine()` rules run on **insert only** — updates validate
133
+ field-by-field. Keep invariants per-field where possible.
@@ -0,0 +1,28 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "radix-lyra",
4
+ "rsc": false,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "src/index.css",
9
+ "baseColor": "olive",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "iconLibrary": "lucide",
14
+ "rtl": false,
15
+ "menuColor": "default",
16
+ "menuAccent": "subtle",
17
+ "aliases": {
18
+ "components": "@/components",
19
+ "utils": "@/lib/utils",
20
+ "ui": "@/components/ui",
21
+ "lib": "@/lib",
22
+ "hooks": "@/hooks"
23
+ },
24
+ "registries": {
25
+ "@kibo-ui": "https://www.kibo-ui.com/r/{name}.json",
26
+ "@diceui": "https://diceui.com/r/{name}.json"
27
+ }
28
+ }
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Expenses</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>