@montytools/cli 0.4.2 → 0.5.1
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 +893 -209
- package/lib/compile.mjs +2 -2
- package/lib/schemaCodegen.mjs +222 -0
- package/lib/schemaPull.mjs +83 -0
- package/package.json +4 -3
- package/skills/monty-build/SKILL.md +47 -28
- package/skills/monty-design/SKILL.md +123 -0
- package/skills/monty-operate/SKILL.md +7 -9
- package/template/AGENTS.md +114 -16
- package/template/package.json +1 -1
- package/template/src/index.css +35 -31
package/template/AGENTS.md
CHANGED
|
@@ -31,9 +31,16 @@ Folders are managed for you: this app lives in `~/Monty/<slug>`. `monty current`
|
|
|
31
31
|
Icons from `lucide-react`. Don't install other component libraries or
|
|
32
32
|
write raw-color CSS — use semantic tokens (`bg-background`,
|
|
33
33
|
`text-muted-foreground`, …). Don't edit `src/index.css` theme tokens.
|
|
34
|
+
The look is Lyra: surfaces are stock components — borderless,
|
|
35
|
+
sharp-cornered — never hand-styled divs; chart marks are sharp
|
|
36
|
+
rectangles on real axes. The `monty-design` skill is the full spec.
|
|
34
37
|
4. **Schema changes = edit `monty.config.ts` and save.** Types update
|
|
35
38
|
immediately. Prefer additive changes; give new fields `.optional()` or
|
|
36
39
|
`.default(...)` so existing records stay readable.
|
|
40
|
+
5. **You are not done until the work is saved.** After every meaningful
|
|
41
|
+
change verified in dev, run `monty save "<what changed>"` — it builds,
|
|
42
|
+
typechecks, and pushes the working copy to the cloud copy, like
|
|
43
|
+
`git push main`. Unsaved work exists only on this machine.
|
|
37
44
|
|
|
38
45
|
## Data: define, then use
|
|
39
46
|
|
|
@@ -92,6 +99,98 @@ await update(row._id, { receipt }); // store descriptor, not
|
|
|
92
99
|
const { url } = useFileUrl(app, row.receipt); // authenticated blob: URL for previews/downloads
|
|
93
100
|
```
|
|
94
101
|
|
|
102
|
+
## Derived fields, metrics, pages (V2 — the platform renders these)
|
|
103
|
+
|
|
104
|
+
Your config can carry a whole database app the platform shell renders for
|
|
105
|
+
you — system table views, record drawers — no React needed.
|
|
106
|
+
This SPA scaffold exists for the pages that ARE bespoke (`pages` entries
|
|
107
|
+
with `kind: "custom"` mount your routes inside that shell).
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { defineApp, formula, lookup, montyDate, montyMoney, montyRef, rollup, self } from "@montytools/sdk";
|
|
111
|
+
import { z } from "zod";
|
|
112
|
+
|
|
113
|
+
export const app = defineApp({
|
|
114
|
+
slug: "pipeline",
|
|
115
|
+
tables: {
|
|
116
|
+
people: z.object({
|
|
117
|
+
name: z.string().min(1),
|
|
118
|
+
companyId: montyRef("companies"), // typed relation
|
|
119
|
+
commissionRate: z.number().default(0.1),
|
|
120
|
+
// Derived fields live IN the table, like spreadsheet columns:
|
|
121
|
+
companyName: lookup(z.string(), { ref: "companyId", field: "name" }),
|
|
122
|
+
monthlySales: rollup(montyMoney(), { // live aggregate (SUMIFS)
|
|
123
|
+
from: "sales",
|
|
124
|
+
where: { status: "won", salespersonId: self("_id") },
|
|
125
|
+
sum: "amount", over: "closedAt", range: "currentMonth",
|
|
126
|
+
}),
|
|
127
|
+
commission: formula(montyMoney(), "monthlySales * commissionRate"),
|
|
128
|
+
}),
|
|
129
|
+
companies: z.object({ name: z.string().min(1) }),
|
|
130
|
+
sales: z.object({
|
|
131
|
+
amount: montyMoney(),
|
|
132
|
+
status: z.enum(["open", "won", "lost"]).default("open"),
|
|
133
|
+
salespersonId: montyRef("people"),
|
|
134
|
+
closedAt: montyDate().optional(),
|
|
135
|
+
}),
|
|
136
|
+
},
|
|
137
|
+
metrics: { // app-level named numbers formulas reach as `metrics.wonThisMonth`
|
|
138
|
+
wonThisMonth: rollup(montyMoney(), { from: "sales", where: { status: "won" }, sum: "amount", over: "closedAt", range: "currentMonth" }),
|
|
139
|
+
},
|
|
140
|
+
pages: {
|
|
141
|
+
people: { kind: "view", table: "people", summaries: { monthlySales: "sum" } },
|
|
142
|
+
reports: { kind: "custom", path: "/reports" }, // ← your src/routes/reports.tsx
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Rules that matter:
|
|
148
|
+
|
|
149
|
+
- **Custom pages wear the platform chrome from `@montytools/sdk/ui`** — the
|
|
150
|
+
SAME components the shell renders its own pages with, so your page is
|
|
151
|
+
indistinguishable from a system table view. Every custom page opens with
|
|
152
|
+
`PageHeader`; without it the page looks foreign beside record pages:
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
import { PageHeader, PageHeaderButton } from "@montytools/sdk/ui";
|
|
156
|
+
|
|
157
|
+
<div className="flex h-full min-h-dvh flex-col">
|
|
158
|
+
<PageHeader icon={ChartColumn} title="Reports" meta="42 rows">
|
|
159
|
+
<PageHeaderButton onClick={exportCsv}>Export</PageHeaderButton>
|
|
160
|
+
<PageHeaderButton primary onClick={openNew}>
|
|
161
|
+
<Plus className="size-3.5" /> New
|
|
162
|
+
</PageHeaderButton>
|
|
163
|
+
</PageHeader>
|
|
164
|
+
<main className="min-h-0 flex-1 overflow-auto p-6">…</main>
|
|
165
|
+
</div>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`primary` marks the page's one main action; other actions wear the quiet
|
|
169
|
+
control style; `icon` is any lucide icon (defaults to the custom-page
|
|
170
|
+
puzzle mark the sidebar uses). Also there: `FloatingBar` +
|
|
171
|
+
`FloatingBarButton` (the bottom-center bar for bulk-selection actions and
|
|
172
|
+
mode strips) and the Lyra table classes `SURFACE`/`THEAD`/`TH`/`ROW`/`CHIP`
|
|
173
|
+
for record-like tables (`<div className={SURFACE}><table>…`), identical to
|
|
174
|
+
the app's Configuration surfaces.
|
|
175
|
+
- **Formulas are strings**, not functions — the Monty expression grammar
|
|
176
|
+
(`+ - * / %`, comparisons, `&& || !`, `IF/ROUND/ABS/MIN/MAX`,
|
|
177
|
+
`metrics.<name>`). A formula sees only fields declared ABOVE it. Type
|
|
178
|
+
errors and unknown identifiers fail the compile with the fix inline.
|
|
179
|
+
- **Derived fields are read-only** — writing one through `useInsert`/
|
|
180
|
+
`useUpdate` is a `VALIDATION` error naming the field and its kind.
|
|
181
|
+
- Rollup `where` values are equality literals or `self("ownField")` ("this
|
|
182
|
+
row's value"). `sum:` names a numeric field; `count: true` counts.
|
|
183
|
+
- In custom pages, read metrics/rollups with `useMetric(app, "wonThisMonth")`
|
|
184
|
+
and `useAggregate(app, "people", "monthlySales", rowIds)`.
|
|
185
|
+
- **Datasets** (computed row sets — hidden data, joins): declare the row
|
|
186
|
+
shape in `datasets: { leaderboard: z.object({...}) }`, implement it as a
|
|
187
|
+
`defineDataset` export in `server/index.ts` (≤1,000 rows — aggregate or
|
|
188
|
+
filter server-side), consume with `useDataset(app, "leaderboard")` —
|
|
189
|
+
polled (focus + optional interval + your own writes), NOT live.
|
|
190
|
+
- `monty schema pull` regenerates this file from the platform if another
|
|
191
|
+
agent edited the schema remotely (the terminal will tell you when —
|
|
192
|
+
`MANIFEST_DRIFT`).
|
|
193
|
+
|
|
95
194
|
## Errors are instructions
|
|
96
195
|
|
|
97
196
|
Every platform error is one line shaped like:
|
|
@@ -116,7 +215,7 @@ When logic must not run in the browser (private tables, third-party APIs with
|
|
|
116
215
|
secret keys, webhooks, clocks), create `server/index.ts` with named async
|
|
117
216
|
exports. Web-standard APIs only (`fetch`, `crypto`, `URL`, …) — no `node:`
|
|
118
217
|
imports; it runs on Cloudflare Workers when Live and inside `monty dev` in
|
|
119
|
-
|
|
218
|
+
a session. Every export gets `ctx`: `ctx.records` (full CRUD on all your
|
|
120
219
|
tables, including ones the UI never exposes), `ctx.secrets` (see below),
|
|
121
220
|
`ctx.viewer` (who called: member session/visitor/schedule/none), and
|
|
122
221
|
`ctx.track()` (emit an event).
|
|
@@ -160,10 +259,10 @@ Public functions are NOT callable through `useServerFn` — only at their
|
|
|
160
259
|
public).
|
|
161
260
|
|
|
162
261
|
**Secrets:** `monty secret set STRIPE_KEY` stores a key for Live (write-only,
|
|
163
|
-
never in code or config). In
|
|
164
|
-
`.monty/secrets.json`. Both arrive as `ctx.secrets.STRIPE_KEY`.
|
|
262
|
+
never in code or config). In a dev session, put the same names in the
|
|
263
|
+
gitignored `.monty/secrets.json`. Both arrive as `ctx.secrets.STRIPE_KEY`.
|
|
165
264
|
|
|
166
|
-
**
|
|
265
|
+
**Session behavior:** `monty dev` runs your schedules for real (a `cron:` line
|
|
167
266
|
prints per firing, UTC) and serves public functions at
|
|
168
267
|
`http://localhost:<port>/__monty/public/<name>` — curl them to test.
|
|
169
268
|
|
|
@@ -180,25 +279,24 @@ monty dev # Vite + HMR, auto-picks a free port and prints it
|
|
|
180
279
|
Headless? Verify with `monty build` then `monty typecheck` (typecheck builds
|
|
181
280
|
first when needed — the build generates `src/routeTree.gen.ts`).
|
|
182
281
|
|
|
183
|
-
**
|
|
184
|
-
|
|
185
|
-
(tunneled
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
`monty deploy` directly if the user explicitly asks.
|
|
282
|
+
**One live app:** there is ONE copy of the app — the one the team uses.
|
|
283
|
+
While `monty dev` runs, workspace admins see your running session's
|
|
284
|
+
version automatically (tunneled); everyone else
|
|
285
|
+
keeps the saved copy. After every meaningful change verified in dev, run
|
|
286
|
+
`monty save "<what changed>"` — it pushes the work to the cloud copy,
|
|
287
|
+
like `git push main` (build + typecheck gate it, so nothing uncompilable
|
|
288
|
+
ever ships). Leave `monty dev` running while you work.
|
|
191
289
|
|
|
192
290
|
**Driving your app in a browser (agents):** while `monty dev` runs, opening
|
|
193
291
|
`http://localhost:5173` is ALREADY AUTHENTICATED — no sign-in screen (the dev
|
|
194
292
|
server mints short-lived workspace tokens from the CLI login). Point
|
|
195
293
|
Playwright or any browser automation at it, click through your app against
|
|
196
|
-
reactive
|
|
294
|
+
reactive live data, and read your errors in the `monty dev` terminal
|
|
197
295
|
(`[browser:error] …` lines). Edit → HMR → look → fix: verify your own work.
|
|
198
296
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
badge
|
|
297
|
+
The session reads and writes the app's REAL records — there is one set
|
|
298
|
+
of data, and every write journals into the app's Activity. Treat edits as
|
|
299
|
+
real edits; the "SESSION · … · live data" badge reminds you.
|
|
202
300
|
|
|
203
301
|
## Modeling tips
|
|
204
302
|
|
package/template/package.json
CHANGED
package/template/src/index.css
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
@import "shadcn/tailwind.css";
|
|
4
4
|
@import "@fontsource-variable/roboto";
|
|
5
5
|
|
|
6
|
+
/* Shared platform chrome (@montytools/sdk/ui) ships Tailwind classes in its
|
|
7
|
+
dist — scan it so they compile. */
|
|
8
|
+
@source "../node_modules/@montytools/sdk/dist/ui.js";
|
|
9
|
+
|
|
6
10
|
|
|
7
11
|
@custom-variant dark (&:is(.dark *));
|
|
8
12
|
|
|
@@ -85,37 +89,37 @@
|
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
.dark {
|
|
88
|
-
--background:
|
|
89
|
-
--foreground:
|
|
90
|
-
--card:
|
|
91
|
-
--card-foreground:
|
|
92
|
-
--popover:
|
|
93
|
-
--popover-foreground:
|
|
94
|
-
--primary:
|
|
95
|
-
--primary-foreground:
|
|
96
|
-
--secondary:
|
|
97
|
-
--secondary-foreground:
|
|
98
|
-
--muted:
|
|
99
|
-
--muted-foreground:
|
|
100
|
-
--accent:
|
|
101
|
-
--accent-foreground:
|
|
102
|
-
--destructive: oklch(0.
|
|
103
|
-
--border:
|
|
104
|
-
--input:
|
|
105
|
-
--ring:
|
|
106
|
-
--chart-1:
|
|
107
|
-
--chart-2:
|
|
108
|
-
--chart-3:
|
|
109
|
-
--chart-4:
|
|
110
|
-
--chart-5:
|
|
111
|
-
--sidebar:
|
|
112
|
-
--sidebar-foreground:
|
|
113
|
-
--sidebar-primary:
|
|
114
|
-
--sidebar-primary-foreground:
|
|
115
|
-
--sidebar-accent:
|
|
116
|
-
--sidebar-accent-foreground:
|
|
117
|
-
--sidebar-border:
|
|
118
|
-
--sidebar-ring:
|
|
92
|
+
--background: #101112;
|
|
93
|
+
--foreground: #F1F2F3;
|
|
94
|
+
--card: #17181A;
|
|
95
|
+
--card-foreground: #F1F2F3;
|
|
96
|
+
--popover: #17181A;
|
|
97
|
+
--popover-foreground: #F1F2F3;
|
|
98
|
+
--primary: #266DF0;
|
|
99
|
+
--primary-foreground: #FFFFFF;
|
|
100
|
+
--secondary: rgb(255 255 255 / 0.05);
|
|
101
|
+
--secondary-foreground: #F1F2F3;
|
|
102
|
+
--muted: rgb(255 255 255 / 0.05);
|
|
103
|
+
--muted-foreground: rgb(255 255 255 / 0.6);
|
|
104
|
+
--accent: rgb(255 255 255 / 0.05);
|
|
105
|
+
--accent-foreground: #F1F2F3;
|
|
106
|
+
--destructive: oklch(0.68 0.19 25);
|
|
107
|
+
--border: #1B1C1F;
|
|
108
|
+
--input: #212224;
|
|
109
|
+
--ring: #266DF0;
|
|
110
|
+
--chart-1: #266DF0;
|
|
111
|
+
--chart-2: #4E8CFC;
|
|
112
|
+
--chart-3: #9B69FF;
|
|
113
|
+
--chart-4: #6EA5F7;
|
|
114
|
+
--chart-5: #2D3A55;
|
|
115
|
+
--sidebar: #101112;
|
|
116
|
+
--sidebar-foreground: #F1F2F3;
|
|
117
|
+
--sidebar-primary: #266DF0;
|
|
118
|
+
--sidebar-primary-foreground: #FFFFFF;
|
|
119
|
+
--sidebar-accent: rgb(255 255 255 / 0.05);
|
|
120
|
+
--sidebar-accent-foreground: #F1F2F3;
|
|
121
|
+
--sidebar-border: #1B1C1F;
|
|
122
|
+
--sidebar-ring: #266DF0;
|
|
119
123
|
}
|
|
120
124
|
|
|
121
125
|
@layer base {
|