@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.
@@ -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
- Studio. Every export gets `ctx`: `ctx.records` (full CRUD on all your
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 Studio, put the same names in the gitignored
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
- **Studio behavior:** `monty dev` runs your schedules for real (a `cron:` line
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
- **Studio vs Live:** every Monty app has two states. While `monty dev` runs,
184
- the app is in **Studio** visible in the workspace to admins only
185
- (tunneled, `#dev` sandboxed data). **Live** is the published state the
186
- whole team sees. Going Live is the owner's **Publish** click in the
187
- workspace menu bar — it signals your running `monty dev`, which builds,
188
- typechecks, and uploads. You are done when the app works in Studio;
189
- leave `monty dev` running and let the owner publish. Only run
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 sandboxed data, and read your errors in the `monty dev` terminal
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
- Studio writes go to a sandboxed `#dev` namespace inside your real workspace
200
- iterate freely, Live app data is untouched. The "STUDIO · · sandbox data"
201
- badge confirms it.
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
 
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "@fontsource-variable/roboto": "^5.2.10",
13
- "@montytools/sdk": "^0.1.5",
13
+ "@montytools/sdk": "^0.2.1",
14
14
  "@tanstack/react-router": "1.170.17",
15
15
  "class-variance-authority": "^0.7.1",
16
16
  "clsx": "^2.1.1",
@@ -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: oklch(0.153 0.006 107.1);
89
- --foreground: oklch(0.988 0.003 106.5);
90
- --card: oklch(0.228 0.013 107.4);
91
- --card-foreground: oklch(0.988 0.003 106.5);
92
- --popover: oklch(0.228 0.013 107.4);
93
- --popover-foreground: oklch(0.988 0.003 106.5);
94
- --primary: oklch(0.437 0.078 188.216);
95
- --primary-foreground: oklch(0.984 0.014 180.72);
96
- --secondary: oklch(0.274 0.006 286.033);
97
- --secondary-foreground: oklch(0.985 0 0);
98
- --muted: oklch(0.286 0.016 107.4);
99
- --muted-foreground: oklch(0.737 0.021 106.9);
100
- --accent: oklch(0.286 0.016 107.4);
101
- --accent-foreground: oklch(0.988 0.003 106.5);
102
- --destructive: oklch(0.704 0.191 22.216);
103
- --border: oklch(1 0 0 / 10%);
104
- --input: oklch(1 0 0 / 15%);
105
- --ring: oklch(0.58 0.031 107.3);
106
- --chart-1: oklch(0.855 0.138 181.071);
107
- --chart-2: oklch(0.704 0.14 182.503);
108
- --chart-3: oklch(0.6 0.118 184.704);
109
- --chart-4: oklch(0.511 0.096 186.391);
110
- --chart-5: oklch(0.437 0.078 188.216);
111
- --sidebar: oklch(0.228 0.013 107.4);
112
- --sidebar-foreground: oklch(0.988 0.003 106.5);
113
- --sidebar-primary: oklch(0.704 0.14 182.503);
114
- --sidebar-primary-foreground: oklch(0.277 0.046 192.524);
115
- --sidebar-accent: oklch(0.286 0.016 107.4);
116
- --sidebar-accent-foreground: oklch(0.988 0.003 106.5);
117
- --sidebar-border: oklch(1 0 0 / 10%);
118
- --sidebar-ring: oklch(0.58 0.031 107.3);
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 {