@montytools/cli 0.5.0 → 0.5.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.
@@ -32,7 +32,8 @@ export function manifestHash(manifest) {
32
32
 
33
33
  const IMPORTABLE = [
34
34
  "defineApp", "formula", "lookup", "montyDate", "montyFileSchema",
35
- "montyMember", "montyMoney", "montyPercent", "montyRef", "rollup", "self",
35
+ "montyMember", "montyMoney", "montyMultiSelect", "montyPercent",
36
+ "montyPhone", "montyRating", "montyRef", "montySelect", "rollup", "self",
36
37
  ];
37
38
 
38
39
  function fieldsInOrder(section) {
@@ -56,7 +57,34 @@ function storedSource(spec, used) {
56
57
  src = `montyRef(${JSON.stringify(spec.table)})`;
57
58
  break;
58
59
  case "enum":
59
- src = `z.enum(${JSON.stringify(spec.values)})`;
60
+ if (spec.valueDescriptions) {
61
+ used.add("montySelect");
62
+ src = `montySelect(${JSON.stringify(spec.values)}, ${JSON.stringify(spec.valueDescriptions)})`;
63
+ } else {
64
+ src = `z.enum(${JSON.stringify(spec.values)})`;
65
+ }
66
+ break;
67
+ case "multiSelect":
68
+ if (spec.valueDescriptions) {
69
+ used.add("montyMultiSelect");
70
+ src = `montyMultiSelect(${JSON.stringify(spec.values)}, ${JSON.stringify(spec.valueDescriptions)})`;
71
+ } else {
72
+ src = `z.array(z.enum(${JSON.stringify(spec.values)}))`;
73
+ }
74
+ break;
75
+ case "email":
76
+ src = "z.email()";
77
+ break;
78
+ case "url":
79
+ src = "z.url()";
80
+ break;
81
+ case "phone":
82
+ used.add("montyPhone");
83
+ src = "montyPhone()";
84
+ break;
85
+ case "rating":
86
+ used.add("montyRating");
87
+ src = "montyRating()";
60
88
  break;
61
89
  default:
62
90
  // json: the manifest carries no deep shape (schemaJson owns storage
@@ -64,6 +92,7 @@ function storedSource(spec, used) {
64
92
  src = "z.any()";
65
93
  }
66
94
  if (spec.optional) src += ".optional()";
95
+ if (spec.description) src += `.describe(${JSON.stringify(spec.description)})`;
67
96
  return src;
68
97
  }
69
98
 
@@ -98,6 +127,7 @@ function rollupSource(spec, used, indent) {
98
127
  else lines.push("count: true,");
99
128
  if (spec.over !== undefined) lines.push(`over: ${JSON.stringify(spec.over)},`);
100
129
  if (spec.range !== undefined) lines.push(`range: ${JSON.stringify(spec.range)},`);
130
+ if (spec.description !== undefined) lines.push(`description: ${JSON.stringify(spec.description)},`);
101
131
  const body = lines.map((l) => `${pad} ${l}`).join("\n");
102
132
  return `rollup(${outputSource(spec.output, used)}, {\n${body}\n${pad}})`;
103
133
  }
@@ -108,10 +138,10 @@ function fieldSource(spec, used, indent) {
108
138
  return storedSource(spec, used);
109
139
  case "formula":
110
140
  used.add("formula");
111
- return `formula(${outputSource(spec.output, used)}, ${JSON.stringify(spec.expr)})`;
141
+ return `formula(${outputSource(spec.output, used)}, ${JSON.stringify(spec.expr)}${spec.description !== undefined ? `, ${JSON.stringify(spec.description)}` : ""})`;
112
142
  case "lookup":
113
143
  used.add("lookup");
114
- return `lookup(${outputSource(spec.output, used)}, { ref: ${JSON.stringify(spec.ref)}, field: ${JSON.stringify(spec.field)} })`;
144
+ return `lookup(${outputSource(spec.output, used)}, { ref: ${JSON.stringify(spec.ref)}, field: ${JSON.stringify(spec.field)}${spec.description !== undefined ? `, description: ${JSON.stringify(spec.description)}` : ""} })`;
115
145
  case "rollup":
116
146
  return rollupSource(spec, used, indent);
117
147
  default:
@@ -159,7 +189,16 @@ export function manifestToConfig(manifest, { name, icon } = {}) {
159
189
  }
160
190
 
161
191
  if (manifest.pages !== undefined) {
162
- out.push(` pages: ${JSON.stringify(manifest.pages, null, 2).replace(/\n/g, "\n ")},`);
192
+ // Emit in declared nav order — the config's declaration order becomes
193
+ // pagesOrder on the next compile, so emission order must match it.
194
+ const orderedPages = {};
195
+ for (const name of manifest.pagesOrder ?? Object.keys(manifest.pages)) {
196
+ if (name in manifest.pages) orderedPages[name] = manifest.pages[name];
197
+ }
198
+ for (const [name, page] of Object.entries(manifest.pages)) {
199
+ if (!(name in orderedPages)) orderedPages[name] = page;
200
+ }
201
+ out.push(` pages: ${JSON.stringify(orderedPages, null, 2).replace(/\n/g, "\n ")},`);
163
202
  }
164
203
 
165
204
  if (manifest.datasets !== undefined) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@montytools/cli",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/TomasMonty/monty-v2.git",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: monty-build
3
- description: Build, run, and deploy Monty workspace apps. Use whenever the task involves a Monty app, monty.config.ts, the monty CLI (create/dev/logs/deploy/add), the @montytools/sdk, or a prompt mentioning usemonty.dev. Covers folder discipline, the build loop, data/auth rules, and error handling.
3
+ description: Build, run, and save Monty workspace apps. Use whenever the task involves a Monty app, monty.config.ts, the monty CLI (create/dev/logs/save/add), the @montytools/sdk, or a prompt mentioning usemonty.dev. Covers folder discipline, the build loop, data/auth rules, and error handling.
4
4
  ---
5
5
 
6
6
  # Building Monty apps
@@ -11,10 +11,11 @@ platform's job. The complete contract lives in the app's own `AGENTS.md`
11
11
  (nearest-file-wins — read it before writing code). This skill is the map, not
12
12
  the territory.
13
13
 
14
- Every app has two states: **Live** (published what members and visitors
15
- see) and **Studio** (development — a running dev shell on `#dev`-sandboxed
16
- data, often already started for you by the Monty desktop). Those are the only
17
- names for them; "dev"/"prod" mean something else on this platform.
14
+ There is ONE copy of every app: **Live**, the cloud copy the team uses
15
+ (`monty save` updates it). While a dev **session** runs (a dev shell on
16
+ the app's LIVE records, often already started for you by the Monty desktop),
17
+ workspace admins see the session's version automatically no publish, no
18
+ channel switch. "dev"/"prod" mean platform environments, never app states.
18
19
 
19
20
  ## Rules
20
21
 
@@ -23,45 +24,64 @@ names for them; "dev"/"prod" mean something else on this platform.
23
24
  `monty login` first). `monty current` tells you where you are;
24
25
  `cd "$(monty select <slug>)"` jumps to an app; `monty apps` lists local
25
26
  ones. Never mkdir app folders by hand, and never edit the `id:` line in
26
- `monty.config.ts`.
27
+ `monty.config.ts`. The folder IS the app's source: every file in it
28
+ rides the source snapshot on `monty save`, so never leave scratch files
29
+ here (manifest edits, notes, one-off scripts). Work in the OS temp dir
30
+ instead, or pipe — `monty schema | <edit> | monty schema set -` needs
31
+ no file at all — and delete anything temporary before saving.
27
32
  2. **The loop:** `monty create <slug> --name "Name" --icon <tabler-icon>` →
28
33
  (if the prompt includes a `build id`, pass it: `--build <id>` — the
29
34
  workspace's New app screen tracks your progress live) →
30
- `monty install` → edit `monty.config.ts` (zod tables) + `src/routes/` →
31
- verify in Studio: run `monty dev` once if the dev shell is already
35
+ `monty install` → shape the schema through `monty schema` /
36
+ `monty schema set` (a brand-new app's very first session lands its
37
+ monty.config.ts once; after that the workspace owns the schema) + edit
38
+ `src/routes/` →
39
+ verify in the session: run `monty dev` once — if the dev shell is already
32
40
  running (the Monty desktop usually runs it for you) it prints the status,
33
- Studio URL, and recent log lines, then **exits immediately**; if nothing
41
+ app URL, and recent log lines, then **exits immediately**; if nothing
34
42
  is running it starts the shell (start it in the background and move on).
35
43
  Then iterate: edit code → vite hot-reloads → `monty logs -n 50` shows
36
- whether it compiled and any browser errors. **You are done when
37
- `monty dev` reports `state: online` (or prints the Studio URL on a fresh
38
- start) and the app works.** Re-running `monty dev` is always safe it
39
- attaches, prints status, and exits. Never try to run a second dev
40
- *server* for the same app (attach handles this for you) and never kill a
41
- dev shell you didn't start; `monty dev --takeover` is the only sanctioned
42
- restart when a session is wedged. Taking the app Live is the OWNER'S
43
- click (Publish in the workspace menu bar); **never run `monty deploy`
44
- yourself** unless the user explicitly asks for a direct Live deploy.
44
+ whether it compiled and any browser errors. Re-running `monty dev` is
45
+ always safe it attaches, prints status, and exits. Never try to run a
46
+ second dev *server* for the same app (attach handles this for you) and
47
+ never kill a dev shell you didn't start; `monty dev --takeover` is the
48
+ only sanctioned restart when a session is wedged. **You are not done
49
+ until you've saved: once the work is verified in the session, run
50
+ `monty save "<what changed>"`** it builds, typechecks, and pushes the
51
+ working copy to the cloud copy, like `git push main`. Save after every
52
+ meaningful change, not just at the end; unsaved work exists only on this
53
+ machine.
45
54
  3. **Everything through the CLI.** `monty install`, `monty build`,
46
- `monty typecheck`, `monty dev`, `monty deploy` — never run vite, tsc,
55
+ `monty typecheck`, `monty dev`, `monty save` — never run vite, tsc,
47
56
  pnpm, or npm scripts directly. `monty dev` auto-picks a free port and
48
57
  prints it; `monty typecheck` builds first when needed. `monty logs`
49
58
  (add `-f` to follow) is how you read the dev shell's output — vite build
50
- errors, browser errors, and publish results all land there.
59
+ errors, browser errors, and save results all land there.
51
60
  4. **One import surface:** `@montytools/sdk` (`defineApp`, zod) and
52
61
  `@montytools/sdk/react` (hooks: `useList`, `useInsert`, …). Never import
53
62
  Clerk or Convex directly; never fetch external APIs from app code — the
54
63
  platform CSP blocks them.
55
- 5. **Schema is zod in `monty.config.ts`.** Field names `_*`, `updatedAt`,
56
- `createdBy` are reserved. Push happens automatically on dev/deploy.
64
+ 5. **The data schema lives in the WORKSPACE, not in code.** Read it with
65
+ `monty schema` (JSON on stdout); change it by editing that JSON and
66
+ running `monty schema set <file>` — validated server-side, additive by
67
+ default. On workspace-owned apps, `monty.config.ts` edits do NOT change
68
+ the schema. Give every field a `description` and every enum/multiSelect
69
+ a `valueDescriptions` map saying WHEN each option applies — that's the
70
+ guidance later record-writing agents follow. Declare a page in the
71
+ manifest BEFORE shipping its route — a save with an undeclared route
72
+ refuses with the fix. Field names `_*`, `updatedAt`, `createdBy` are
73
+ reserved.
57
74
  6. **UI is stock shadcn** (preset already wired). Add curated components with
58
75
  `monty add <name>`; browse with `monty components` / `monty docs <name>`.
76
+ How pages should LOOK — Lyra surfaces, dark-only, the chart language — is
77
+ the `monty-design` skill; read it before styling any page.
59
78
  7. **Errors are instructions.** Every failure prints
60
79
  `[MontyError CODE] Fix: …` — do exactly what the Fix says; don't guess.
61
- Typecheck failures block deploy by design.
62
- 8. **Verify before publish.** The dev shell writes to the `#dev` Studio
63
- sandboxLive team records are never touched, so exercise the app for
64
- real.
80
+ Typecheck failures block the save by design.
81
+ 8. **Edits are real.** The dev shell reads and writes the app's LIVE
82
+ recordsthere is one set of data, and every write journals into the
83
+ app's Activity. Exercise the app for real; clean up test rows you
84
+ create; never seed junk into a team's working tables.
65
85
 
66
86
  ## CLI reference
67
87
 
@@ -71,8 +91,10 @@ names for them; "dev"/"prod" mean something else on this platform.
71
91
  | `monty create <slug>` | register the app in the workspace + stamp it into `~/.monty/apps/<id>` (needs login) |
72
92
  | `monty current` / `select` / `apps` | where am I / jump to app / list local |
73
93
  | `monty install` / `build` / `typecheck` | full lifecycle via the CLI — no raw pnpm/vite/tsc |
74
- | `monty dev` | run the app in Studio, or attach to an already-running session (auto-port, sandboxed data, auto-auth) |
94
+ | `monty dev` | run the app's session, or attach to an already-running one (auto-port, live data, auto-auth) |
75
95
  | `monty logs [-n N] [-f]` | read/follow the dev shell log — the debugging window after every edit |
76
96
  | `monty add <name…>` | install curated shadcn components |
77
- | `monty deploy` | build + typecheck + upload straight to Live (owner escape hatch) |
97
+ | `monty schema [slug]` | print the app's stored manifest (tables, pages, metrics) as JSON |
98
+ | `monty schema set <file\|->` | write an edited manifest back (validated, CAS, additive by default) |
99
+ | `monty save ["what changed"]` | push the working copy to the cloud copy, like `git push main` (build + typecheck gate it) |
78
100
  | `monty skills` | (re)install this skill for your agent |
@@ -0,0 +1,123 @@
1
+ ---
2
+ name: monty-design
3
+ description: Design Monty app pages — Lyra shadcn styling (borderless, sharp-cornered, stock components only), dark-only theming, and the Monty chart language (square marks on real axes). Use whenever building or restyling UI in a Monty app; pages, dashboards, charts, stat tiles, KPI rows, tables, or any prompt about how a Monty app should look.
4
+ ---
5
+
6
+ # Designing Monty pages
7
+
8
+ Monty is a B2B work OS. Apps render inside the platform shell, so a page is
9
+ "designed" when it looks native to Monty: quiet, rectilinear, data-forward.
10
+ The design system is already installed — your job is to NOT fight it.
11
+
12
+ ## The one law: stock Lyra, nothing invented
13
+
14
+ Every app ships shadcn preset `radix-lyra` (see `components.json`). Lyra
15
+ surfaces are **borderless and sharp-cornered**: `Card` is `rounded-none`,
16
+ no border, a `bg-card` fill with a hairline `ring-1 ring-foreground/10`.
17
+
18
+ - Use the components as they come: `Card`/`CardHeader`/`CardTitle`/
19
+ `CardDescription`/`CardContent`, `Table`, `Badge`, … Never rebuild a
20
+ surface as a styled `div` — a hand-rolled `rounded-xl border bg-…` card is
21
+ the canonical mistake.
22
+ - Never invent tokens or raw colors. Semantic tokens only (`bg-background`,
23
+ `text-muted-foreground`, `border-border`, `var(--chart-2)`, …).
24
+ - Sanctioned overrides are content-level only: e.g. `text-2xl tabular-nums`
25
+ on a stat value, a width on a label column. If an override styles a
26
+ SURFACE, you are off the system.
27
+
28
+ ## Dark-only, in the shell's palette
29
+
30
+ The Monty shell is dark-only; a light page inside it reads as broken. The
31
+ app theme's `.dark` tokens mirror the shell palette (#101112 ground,
32
+ #17181A cards, #266DF0 primary) so embedded pages are seamless — never
33
+ retheme or hand-pick your own dark colors. Until
34
+ the template ships dark by default: `class="dark"` on `<html>` in
35
+ `index.html`, and any boot-splash background set to the dark `--background`
36
+ value. Verify your page against the shell, not in isolation.
37
+
38
+ ## Page anatomy
39
+
40
+ Every page opens with the platform chrome from `@montytools/sdk/ui` — the
41
+ SAME components the shell renders system table views with, so a custom page
42
+ is indistinguishable from a record page. Never hand-roll the header bar.
43
+
44
+ ```tsx
45
+ import { PageHeader, PageHeaderButton } from "@montytools/sdk/ui";
46
+
47
+ <div className="flex h-full min-h-dvh flex-col bg-background">
48
+ <PageHeader icon={ChartColumn} title="Page title" meta="context (count, filter)">
49
+ <PageHeaderButton onClick={secondary}>Export</PageHeaderButton>
50
+ <PageHeaderButton primary onClick={main}><Plus className="size-3.5" /> New</PageHeaderButton>
51
+ </PageHeader>
52
+ <div className="min-h-0 flex-1 overflow-auto">
53
+ <div className="flex flex-col gap-3 p-6 pt-4">
54
+ {/* KPI row */}
55
+ <div className="grid grid-cols-[repeat(auto-fill,minmax(14rem,1fr))] gap-3">…</div>
56
+ {/* section cards */}
57
+ <div className="grid gap-3 lg:grid-cols-2">…</div>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ ```
62
+
63
+ Also in `@montytools/sdk/ui`: `FloatingBar` + `FloatingBarButton` (the
64
+ bottom-center bar for bulk-selection actions and mode strips) and the Lyra
65
+ table classes `SURFACE`/`THEAD`/`TH`/`ROW`/`CHIP` — record-like tables are
66
+ `<div className={SURFACE}><table>…` with those classes, identical to the
67
+ shell's Configuration surfaces.
68
+
69
+ A stat tile is a stock Card, nothing more:
70
+
71
+ ```tsx
72
+ <Card size="sm">
73
+ <CardHeader>
74
+ <CardDescription>Leads added</CardDescription>
75
+ <CardTitle className="text-2xl tabular-nums">{value}</CardTitle>
76
+ <div className="text-xs text-muted-foreground">{delta or context}</div>
77
+ </CardHeader>
78
+ </Card>
79
+ ```
80
+
81
+ ## Charts: sharp marks on real axes
82
+
83
+ Chart marks are **sharp rectangles — no rounded corners, ever** (matches the
84
+ Lyra rectilinear look). The three failure modes to avoid: pill "track+fill"
85
+ bars (read as progress bars), rounded caps (misstate where a value ends),
86
+ and floating bars with no axis (nothing anchors the eye).
87
+
88
+ - **Horizontal bars**: grow from a left hairline baseline
89
+ (`border-l border-foreground/25`) over quarter gridlines
90
+ (`absolute left-1/4|1/2|3/4 border-l border-foreground/10` —
91
+ foreground-alpha so hairlines read on any surface), bar `h-4`,
92
+ fill `var(--chart-2)`. Label left in `text-muted-foreground`
93
+ (fixed-width, truncate); count right in `font-medium tabular-nums`,
94
+ zero values muted with no fill.
95
+ - **Columns**: a value axis with a baseline (`border-foreground/25`) plus
96
+ hairline gridlines (`border-foreground/10`) and tiny tabular tick labels (10px, muted,
97
+ right-aligned in a left gutter). Round the axis top to a "nice" integer
98
+ (≤4 exact; else the next multiple of 5/10/50) so ticks stay honest.
99
+ Direct value labels above non-zero columns.
100
+ - **Color**: one accent ramp from the Monty palette — `var(--chart-2)` for
101
+ the emphasized series (today, the selection), `var(--chart-5)` for
102
+ context. The values are the platform's (Attio-blue family, matching the
103
+ shell); never restate them as hex, never a hue per category, and text
104
+ never wears the data color.
105
+ - **Numbers**: `tabular-nums` everywhere values align; money via
106
+ `toLocaleString(undefined, { style: "currency", currency: "USD" })`
107
+ (compact notation on tiles, full in tables).
108
+ - **Empty states**: keep the axis and gridlines rendered with an honest
109
+ muted line ("No activity logged on this day.") — structure stays, zeros
110
+ carry meaning.
111
+
112
+ Reference implementations: `ColumnChart` and `BarRow` in
113
+ `demos/crm/src/routes/stats.tsx` (day-selector columns, status bars) and
114
+ `demos/crm-v2/src/routes/stats.tsx` (money detail on bars, top-deals table).
115
+
116
+ ## Checklist before you call a page done
117
+
118
+ 1. No `border`/`rounded-*` on any surface you authored — surfaces are stock
119
+ Lyra components.
120
+ 2. No rounded corners on any chart mark; every bar/column sits on an axis.
121
+ 3. Dark: the page blends into the shell with no light seams.
122
+ 4. Values in tabular figures; labels in text tokens; one accent hue.
123
+ 5. Screenshot it inside the shell (`/apps/<slug>/…`), not just the dev port.
@@ -11,10 +11,8 @@ and write that data directly from the terminal: no browser session, no dev
11
11
  server. You do the work (browse, scrape, decide, transform); the app is
12
12
  where the results land, and open app tabs update live as you write.
13
13
 
14
- Two data channels per app: **Live** (the team's real records the default
15
- target) and the **Studio sandbox** (`--studio` development data, never
16
- seen by the team). Writing to Live is normal operating work; that is what
17
- this command is for.
14
+ One set of records per app the team's real data. Writing to it is
15
+ normal operating work; that is what this command is for.
18
16
 
19
17
  ## The loop
20
18
 
@@ -42,11 +40,20 @@ this command is for.
42
40
  values come from each row, and re-running never duplicates.
43
41
  2. **Schema first, rows second.** Field names come from
44
42
  `monty data schema`, exact spelling. The server stores what you send —
45
- a misspelled field is silently a new field, not an error.
46
- 3. **Live is the default and that's correct** — operating work targets the
47
- team's real data. Use `--studio` only when explicitly rehearsing.
48
- Destructive verbs (`remove`, bulk `update`) on Live deserve a
49
- confirmation with the user unless they clearly asked for the cleanup.
43
+ a misspelled field is silently a new field, not an error. The schema's
44
+ `description` and `enumDescriptions` annotations are the app's own
45
+ instructions for filling a field in: where a field has options (an
46
+ `enum`, alone or as array items), pick the option whose description
47
+ matches the situation never your own reading of the option's name,
48
+ and never a value outside the list. A field without options has no
49
+ fixed vocabulary, so its `description` plus the conventions visible in
50
+ existing rows (`list` first) are the contract — write explicit,
51
+ consistently formatted values, not free-hand variants of the same
52
+ thing.
53
+ 3. **Every write is real** — operating work targets the team's data, and
54
+ the app's Activity journals it. Destructive verbs (`remove`, bulk
55
+ `update`) deserve a confirmation with the user unless they clearly
56
+ asked for the cleanup.
50
57
  4. **Batch with arrays.** `--data` accepts a single object or an array;
51
58
  arrays write row by row and return all ids in order.
52
59
  5. **Paginate honestly.** `list` returns `"cursor": null` when complete; a
@@ -59,6 +66,13 @@ this command is for.
59
66
  7. **Rows come back flattened**: your fields at the top level plus system
60
67
  fields `_id`, `_creationTime`, `updatedAt`, `createdBy`. System fields
61
68
  are read-only — never send them in `--data`.
69
+ 8. **File fields hold descriptors, never bytes or paths.** A `file`-typed
70
+ field stores exactly the object `monty data upload` prints
71
+ (`{id, name, contentType, size, uploadedAt, uploadedBy}`). Upload
72
+ first, then reference: use the attach flags to set an existing row's
73
+ field in one command, or paste the printed descriptor into `--data`
74
+ when inserting. Never hand-write a descriptor — an id that no upload
75
+ produced 404s on download.
62
76
 
63
77
  ## Command reference
64
78
 
@@ -71,8 +85,10 @@ this command is for.
71
85
  | `monty data upsert <table> --key <field[,field]> --data '<json\|[json,…]>'` | find-or-create matched on the key fields — the idempotent write |
72
86
  | `monty data update <table> <id> --data '<json>' [--unset a,b]` | shallow-merge onto one row; `--unset` deletes fields |
73
87
  | `monty data remove <table> <id>` | delete one row |
88
+ | `monty data upload <path> [--name N] [--type mime] [--table <t> --record <id> --field <f>]` | store a file (10MB cap); prints the descriptor a `file` field holds — the attach flags set it on an existing row in the same command |
89
+ | `monty data download <file-id> [--out path]` | fetch a stored file's bytes to disk; the id is the `id` key of a row's file descriptor |
74
90
 
75
- All verbs take `--app <slug>` and `--studio`. Output is one JSON document on
91
+ All verbs take `--app <slug>`. Output is one JSON document on
76
92
  stdout.
77
93
 
78
94
  ## Example: import scraped leads into a CRM app
@@ -31,15 +31,25 @@ 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
- `.default(...)` so existing records stay readable.
39
+ `.default(...)` so existing records stay readable. Describe fields for
40
+ the next agent that fills them in: `.describe("what this holds")` on any
41
+ field, and `montySelect`/`montyMultiSelect` instead of bare `z.enum` so
42
+ every option says WHEN it applies, not what the word means.
43
+ 5. **You are not done until the work is saved.** After every meaningful
44
+ change verified in dev, run `monty save "<what changed>"` — it builds,
45
+ typechecks, and pushes the working copy to the cloud copy, like
46
+ `git push main`. Unsaved work exists only on this machine.
37
47
 
38
48
  ## Data: define, then use
39
49
 
40
50
  ```ts
41
51
  // monty.config.ts
42
- import { defineApp, montyFileSchema } from "@montytools/sdk";
52
+ import { defineApp, montyFileSchema, montySelect } from "@montytools/sdk";
43
53
  import { z } from "zod";
44
54
 
45
55
  export const app = defineApp({
@@ -47,8 +57,12 @@ export const app = defineApp({
47
57
  tables: {
48
58
  expenses: z.object({
49
59
  title: z.string().min(1),
50
- amount: z.number().positive(),
51
- status: z.enum(["draft", "submitted", "approved"]).default("draft"),
60
+ amount: z.number().positive().describe("Receipt total in USD, tax included."),
61
+ status: montySelect(["draft", "submitted", "approved"], {
62
+ draft: "Still being filled in, not yet sent for review.",
63
+ submitted: "Awaiting a manager's decision.",
64
+ approved: "Cleared for reimbursement.",
65
+ }).default("draft"),
52
66
  assigneeId: z.string().optional(), // userId from useMembers()
53
67
  receipt: montyFileSchema.optional(),
54
68
  }),
@@ -95,7 +109,7 @@ const { url } = useFileUrl(app, row.receipt); // authenticated blob:
95
109
  ## Derived fields, metrics, pages (V2 — the platform renders these)
96
110
 
97
111
  Your config can carry a whole database app the platform shell renders for
98
- you — system table views, record drawers, dashboards — no React needed.
112
+ you — system table views, record drawers — no React needed.
99
113
  This SPA scaffold exists for the pages that ARE bespoke (`pages` entries
100
114
  with `kind: "custom"` mount your routes inside that shell).
101
115
 
@@ -127,12 +141,11 @@ export const app = defineApp({
127
141
  closedAt: montyDate().optional(),
128
142
  }),
129
143
  },
130
- metrics: { // app-level named numbers for dashboards + formulas (`metrics.wonThisMonth`)
144
+ metrics: { // app-level named numbers formulas reach as `metrics.wonThisMonth`
131
145
  wonThisMonth: rollup(montyMoney(), { from: "sales", where: { status: "won" }, sum: "amount", over: "closedAt", range: "currentMonth" }),
132
146
  },
133
147
  pages: {
134
148
  people: { kind: "view", table: "people", summaries: { monthlySales: "sum" } },
135
- overview: { kind: "dashboard", metrics: ["wonThisMonth"] },
136
149
  reports: { kind: "custom", path: "/reports" }, // ← your src/routes/reports.tsx
137
150
  },
138
151
  });
@@ -140,6 +153,32 @@ export const app = defineApp({
140
153
 
141
154
  Rules that matter:
142
155
 
156
+ - **Custom pages wear the platform chrome from `@montytools/sdk/ui`** — the
157
+ SAME components the shell renders its own pages with, so your page is
158
+ indistinguishable from a system table view. Every custom page opens with
159
+ `PageHeader`; without it the page looks foreign beside record pages:
160
+
161
+ ```tsx
162
+ import { PageHeader, PageHeaderButton } from "@montytools/sdk/ui";
163
+
164
+ <div className="flex h-full min-h-dvh flex-col">
165
+ <PageHeader icon={ChartColumn} title="Reports" meta="42 rows">
166
+ <PageHeaderButton onClick={exportCsv}>Export</PageHeaderButton>
167
+ <PageHeaderButton primary onClick={openNew}>
168
+ <Plus className="size-3.5" /> New
169
+ </PageHeaderButton>
170
+ </PageHeader>
171
+ <main className="min-h-0 flex-1 overflow-auto p-6">…</main>
172
+ </div>
173
+ ```
174
+
175
+ `primary` marks the page's one main action; other actions wear the quiet
176
+ control style; `icon` is any lucide icon (defaults to the custom-page
177
+ puzzle mark the sidebar uses). Also there: `FloatingBar` +
178
+ `FloatingBarButton` (the bottom-center bar for bulk-selection actions and
179
+ mode strips) and the Lyra table classes `SURFACE`/`THEAD`/`TH`/`ROW`/`CHIP`
180
+ for record-like tables (`<div className={SURFACE}><table>…`), identical to
181
+ the app's Configuration surfaces.
143
182
  - **Formulas are strings**, not functions — the Monty expression grammar
144
183
  (`+ - * / %`, comparisons, `&& || !`, `IF/ROUND/ABS/MIN/MAX`,
145
184
  `metrics.<name>`). A formula sees only fields declared ABOVE it. Type
@@ -183,10 +222,13 @@ When logic must not run in the browser (private tables, third-party APIs with
183
222
  secret keys, webhooks, clocks), create `server/index.ts` with named async
184
223
  exports. Web-standard APIs only (`fetch`, `crypto`, `URL`, …) — no `node:`
185
224
  imports; it runs on Cloudflare Workers when Live and inside `monty dev` in
186
- Studio. Every export gets `ctx`: `ctx.records` (full CRUD on all your
187
- tables, including ones the UI never exposes), `ctx.secrets` (see below),
188
- `ctx.viewer` (who called: member session/visitor/schedule/none), and
189
- `ctx.track()` (emit an event).
225
+ a session. Every export gets `ctx`: `ctx.records` (full CRUD on all your
226
+ tables, including ones the UI never exposes), `ctx.files` (platform file
227
+ storage: `upload(data, {name?, contentType?}) → MontyFile`,
228
+ `download(fileOrId) → Blob`, `remove(fileOrId)` same descriptors as the
229
+ `useUploadFile` hook, 10MB cap), `ctx.secrets` (see below), `ctx.viewer`
230
+ (who called: member session/visitor/schedule/none), and `ctx.track()`
231
+ (emit an event).
190
232
 
191
233
  ```ts
192
234
  // server/index.ts
@@ -227,10 +269,10 @@ Public functions are NOT callable through `useServerFn` — only at their
227
269
  public).
228
270
 
229
271
  **Secrets:** `monty secret set STRIPE_KEY` stores a key for Live (write-only,
230
- never in code or config). In Studio, put the same names in the gitignored
231
- `.monty/secrets.json`. Both arrive as `ctx.secrets.STRIPE_KEY`.
272
+ never in code or config). In a dev session, put the same names in the
273
+ gitignored `.monty/secrets.json`. Both arrive as `ctx.secrets.STRIPE_KEY`.
232
274
 
233
- **Studio behavior:** `monty dev` runs your schedules for real (a `cron:` line
275
+ **Session behavior:** `monty dev` runs your schedules for real (a `cron:` line
234
276
  prints per firing, UTC) and serves public functions at
235
277
  `http://localhost:<port>/__monty/public/<name>` — curl them to test.
236
278
 
@@ -247,25 +289,24 @@ monty dev # Vite + HMR, auto-picks a free port and prints it
247
289
  Headless? Verify with `monty build` then `monty typecheck` (typecheck builds
248
290
  first when needed — the build generates `src/routeTree.gen.ts`).
249
291
 
250
- **Studio vs Live:** every Monty app has two states. While `monty dev` runs,
251
- the app is in **Studio** visible in the workspace to admins only
252
- (tunneled, `#dev` sandboxed data). **Live** is the published state the
253
- whole team sees. Going Live is the owner's **Publish** click in the
254
- workspace menu bar — it signals your running `monty dev`, which builds,
255
- typechecks, and uploads. You are done when the app works in Studio;
256
- leave `monty dev` running and let the owner publish. Only run
257
- `monty deploy` directly if the user explicitly asks.
292
+ **One live app:** there is ONE copy of the app the one the team uses.
293
+ While `monty dev` runs, workspace admins see your running session's
294
+ version automatically (tunneled); everyone else
295
+ keeps the saved copy. After every meaningful change verified in dev, run
296
+ `monty save "<what changed>"` — it pushes the work to the cloud copy,
297
+ like `git push main` (build + typecheck gate it, so nothing uncompilable
298
+ ever ships). Leave `monty dev` running while you work.
258
299
 
259
300
  **Driving your app in a browser (agents):** while `monty dev` runs, opening
260
301
  `http://localhost:5173` is ALREADY AUTHENTICATED — no sign-in screen (the dev
261
302
  server mints short-lived workspace tokens from the CLI login). Point
262
303
  Playwright or any browser automation at it, click through your app against
263
- reactive sandboxed data, and read your errors in the `monty dev` terminal
304
+ reactive live data, and read your errors in the `monty dev` terminal
264
305
  (`[browser:error] …` lines). Edit → HMR → look → fix: verify your own work.
265
306
 
266
- Studio writes go to a sandboxed `#dev` namespace inside your real workspace
267
- iterate freely, Live app data is untouched. The "STUDIO · · sandbox data"
268
- badge confirms it.
307
+ The session reads and writes the app's REAL records there is one set
308
+ of data, and every write journals into the app's Activity. Treat edits as
309
+ real edits; the "SESSION · … · live data" badge reminds you.
269
310
 
270
311
  ## Modeling tips
271
312
 
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "@fontsource-variable/roboto": "^5.2.10",
13
- "@montytools/sdk": "^0.2.0",
13
+ "@montytools/sdk": "^0.2.2",
14
14
  "@tanstack/react-router": "1.170.17",
15
15
  "class-variance-authority": "^0.7.1",
16
16
  "clsx": "^2.1.1",