@lunora/cli 0.0.0 → 1.0.0-alpha.10

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.
Files changed (75) hide show
  1. package/LICENSE.md +105 -0
  2. package/README.md +109 -9
  3. package/__assets__/package-og.svg +14 -0
  4. package/dist/bin.mjs +11 -0
  5. package/dist/index.d.mts +956 -0
  6. package/dist/index.d.ts +956 -0
  7. package/dist/index.mjs +19 -0
  8. package/dist/packem_chunks/handler.mjs +150 -0
  9. package/dist/packem_chunks/handler10.mjs +22 -0
  10. package/dist/packem_chunks/handler11.mjs +192 -0
  11. package/dist/packem_chunks/handler12.mjs +131 -0
  12. package/dist/packem_chunks/handler13.mjs +65 -0
  13. package/dist/packem_chunks/handler14.mjs +58 -0
  14. package/dist/packem_chunks/handler15.mjs +79 -0
  15. package/dist/packem_chunks/handler16.mjs +43 -0
  16. package/dist/packem_chunks/handler17.mjs +105 -0
  17. package/dist/packem_chunks/handler18.mjs +170 -0
  18. package/dist/packem_chunks/handler19.mjs +89 -0
  19. package/dist/packem_chunks/handler2.mjs +114 -0
  20. package/dist/packem_chunks/handler20.mjs +94 -0
  21. package/dist/packem_chunks/handler21.mjs +311 -0
  22. package/dist/packem_chunks/handler3.mjs +204 -0
  23. package/dist/packem_chunks/handler4.mjs +33 -0
  24. package/dist/packem_chunks/handler5.mjs +49 -0
  25. package/dist/packem_chunks/handler6.mjs +91 -0
  26. package/dist/packem_chunks/handler7.mjs +42 -0
  27. package/dist/packem_chunks/handler8.mjs +174 -0
  28. package/dist/packem_chunks/handler9.mjs +16 -0
  29. package/dist/packem_chunks/planDevCommand.mjs +500 -0
  30. package/dist/packem_chunks/runCodegenCommand.mjs +52 -0
  31. package/dist/packem_chunks/runDeployCommand.mjs +504 -0
  32. package/dist/packem_chunks/runInitCommand.mjs +1498 -0
  33. package/dist/packem_chunks/runMigrateGenerateCommand.mjs +397 -0
  34. package/dist/packem_chunks/runResetCommand.mjs +41 -0
  35. package/dist/packem_chunks/runRpcCommand.mjs +68 -0
  36. package/dist/packem_shared/COMMANDS-D3h9Iwvl.mjs +944 -0
  37. package/dist/packem_shared/DEFAULT_IMPORT_BATCH_SIZE-Ck-2bU08.mjs +244 -0
  38. package/dist/packem_shared/admin-url-4UzT-CI4.mjs +19 -0
  39. package/dist/packem_shared/api-spec-CtA6ilu4.mjs +13 -0
  40. package/dist/packem_shared/buildRegistryIndex-BcYe607_.mjs +38 -0
  41. package/dist/packem_shared/command-BC30oSBW.mjs +14 -0
  42. package/dist/packem_shared/commands-DPKWlqqX.mjs +812 -0
  43. package/dist/packem_shared/createLogger-B40gPzQo.mjs +78 -0
  44. package/dist/packem_shared/createRecordingSpawner-DxI3mebw.mjs +43 -0
  45. package/dist/packem_shared/detect-package-manager-DYp7n3mJ.mjs +61 -0
  46. package/dist/packem_shared/diffSnapshots-BeDvvNiF.mjs +161 -0
  47. package/dist/packem_shared/docker-hMQ97KSQ.mjs +21 -0
  48. package/dist/packem_shared/insertSchemaExtension-BuzF6-t2.mjs +59 -0
  49. package/dist/packem_shared/open-url-Dfq6fAyT.mjs +41 -0
  50. package/dist/packem_shared/output-format-wUvAN6AL.mjs +17 -0
  51. package/dist/packem_shared/parseArgs-YXFuKdEk.mjs +56 -0
  52. package/dist/packem_shared/parseManifest--vZf2FY1.mjs +94 -0
  53. package/dist/packem_shared/resolve-target-qbsJ_5sF.mjs +16 -0
  54. package/dist/packem_shared/runAddCommand-CTRA_JlL.mjs +4 -0
  55. package/dist/packem_shared/schema-drift-gate-BtBt0as0.mjs +79 -0
  56. package/dist/packem_shared/schemaIrToSnapshot-DdsljJT-.mjs +43 -0
  57. package/dist/packem_shared/storage-2RJBhUC4.mjs +84 -0
  58. package/dist/packem_shared/tui-prompts-DEiPCKV-.mjs +661 -0
  59. package/dist/packem_shared/wrangler-name-cy4yhm9j.mjs +12 -0
  60. package/package.json +62 -18
  61. package/skills/README.md +29 -0
  62. package/skills/lunora/SKILL.md +83 -0
  63. package/skills/lunora-create-package/SKILL.md +129 -0
  64. package/skills/lunora-deploy/SKILL.md +150 -0
  65. package/skills/lunora-functions/SKILL.md +182 -0
  66. package/skills/lunora-migration-helper/SKILL.md +194 -0
  67. package/skills/lunora-performance-audit/SKILL.md +143 -0
  68. package/skills/lunora-quickstart/SKILL.md +240 -0
  69. package/skills/lunora-realtime/SKILL.md +177 -0
  70. package/skills/lunora-setup-auth/SKILL.md +170 -0
  71. package/skills/lunora-setup-hyperdrive/SKILL.md +154 -0
  72. package/skills/lunora-setup-hyperdrive-global/SKILL.md +171 -0
  73. package/skills/lunora-setup-mail/SKILL.md +151 -0
  74. package/skills/lunora-setup-scheduler/SKILL.md +157 -0
  75. package/skills/lunora-setup-storage/SKILL.md +158 -0
@@ -0,0 +1,194 @@
1
+ ---
2
+ name: lunora-migration-helper
3
+ description: Plans Lunora schema and data migrations with widen-migrate-narrow. Use for
4
+ breaking schema changes, backfills, table reshaping, online data migrations
5
+ (`defineMigration` + `lunora migrate up`), the `.global()` D1 SQL flow, and the
6
+ pre-deploy schema-drift gate.
7
+ ---
8
+
9
+ # Lunora Migration Helper
10
+
11
+ Safely change a Lunora schema and migrate data when making breaking changes.
12
+
13
+ ## When to Use
14
+
15
+ - Adding required fields to existing tables.
16
+ - Changing field types or structure.
17
+ - Splitting/merging tables, renaming/removing fields.
18
+ - Reshaping `.global()` (D1-backed) tables.
19
+
20
+ ## When Not to Use
21
+
22
+ - Greenfield schema with no data at rest.
23
+ - Adding **optional** fields that need no backfill.
24
+ - Adding new tables or indexes with no correctness concern.
25
+
26
+ ## Two Storage Layers — Know Which You Are Migrating
27
+
28
+ Lunora tables live in one of two backends, and they migrate differently:
29
+
30
+ - **ShardDO SQLite (default `root`, and `.shardBy(key)` tables).** State lives in
31
+ the per-app / per-shard Durable Object. Data is reshaped with **online data
32
+ migrations** — `defineMigration` declarations run by `lunora migrate up`,
33
+ resumable per shard.
34
+ - **`.global()` tables (D1).** Replicated to D1 for cross-region reads. Their
35
+ structural DDL gets versioned **SQL migrations** via `lunora migrate generate`,
36
+ applied by `@lunora/d1`'s runner at deploy time.
37
+
38
+ A breaking change to a `.global()` table needs a generated SQL migration; a data
39
+ backfill (either layer) is an online `defineMigration`. Both follow the same
40
+ **widen → migrate → narrow** discipline.
41
+
42
+ ## Key Principle: Widen, Migrate, Narrow
43
+
44
+ The schema-drift gate (and D1 itself) will not let a breaking change deploy
45
+ without an accompanying migration. So every breaking change is staged:
46
+
47
+ 1. **Widen** — make the schema accept both old and new shapes (add the new field
48
+ as `v.optional`, keep the old one). Update reads to handle both; start writing
49
+ the new shape for new rows. Deploy.
50
+ 2. **Migrate** — backfill existing rows to the new shape (an online
51
+ `defineMigration` run with `lunora migrate up`; plus `lunora migrate generate`
52
+ for `.global()` structural DDL). Verify completeness with `lunora migrate
53
+ status`.
54
+ 3. **Narrow** — make the field required / drop the old field, remove the
55
+ both-shapes read code. Deploy.
56
+
57
+ ### Prefer new fields over changing types
58
+
59
+ When changing a field's shape, add a new field rather than mutating the existing
60
+ one — safer transition, easier rollback.
61
+
62
+ ### Don't delete data prematurely
63
+
64
+ Prefer deprecating: mark the old field `v.optional` with a `// deprecated:` code
65
+ comment explaining why it existed. Delete only once you are sure nothing reads
66
+ it.
67
+
68
+ ## Safe Changes (No Migration Needed)
69
+
70
+ ```ts
71
+ // Adding an optional field — safe.
72
+ users: defineTable({
73
+ name: v.string(),
74
+ bio: v.optional(v.string()),
75
+ });
76
+
77
+ // Adding a new table — safe.
78
+ posts: defineTable({ userId: v.id("users"), title: v.string() }).index("by_user", ["userId"]);
79
+
80
+ // Adding an index — safe.
81
+ users: defineTable({ name: v.string(), email: v.string() }).index("by_email", ["email"]);
82
+ ```
83
+
84
+ ## Online Data Migrations (the backfill workhorse)
85
+
86
+ For backfilling/reshaping rows, declare a migration with `defineMigration` from
87
+ `@lunora/server`. It transforms one document at a time, runs **inside each
88
+ shard's** Durable Object in keyset batches, and is **resumable** — per-shard
89
+ progress is tracked in a reserved `__lunora_migrations` table, so an interrupted
90
+ run resumes where it stopped. Codegen discovers declarations and emits them into
91
+ the registry the DO and CLI look up by `id`.
92
+
93
+ ```ts
94
+ // lunora/migrations/backfill-display-name.ts
95
+ import { defineMigration } from "@lunora/server";
96
+
97
+ export default defineMigration({
98
+ id: "backfill-display-name",
99
+ table: "users",
100
+ batchSize: 200, // optional; defaults to the runner's batch size
101
+ up: (doc) => {
102
+ if (typeof doc.displayName === "string") {
103
+ return; // already migrated — return undefined to skip (not counted as changed)
104
+ }
105
+ return { ...doc, displayName: doc.name ?? "Anonymous" };
106
+ },
107
+ // optional reverse transform applied by `lunora migrate down`
108
+ down: (doc) => {
109
+ const { displayName, ...rest } = doc as Record<string, unknown>;
110
+ return rest;
111
+ },
112
+ });
113
+ ```
114
+
115
+ The transform must preserve row identity — the runner always keeps the original
116
+ `_id` / `_creationTime`, so do not change them.
117
+
118
+ ### Run it
119
+
120
+ ```bash
121
+ lunora migrate create backfill-display-name # scaffold the migration file
122
+ lunora codegen # discover + register it
123
+ lunora migrate up backfill-display-name --dry-run # preview, no rows rewritten
124
+ lunora migrate up backfill-display-name # run across shards (keyset batches)
125
+ lunora migrate status backfill-display-name # per-shard progress
126
+ lunora migrate down backfill-display-name # revert (if `down` defined)
127
+ ```
128
+
129
+ Useful flags: `--batch-size <n>`, `--steps <n>` (cap batches this run), and
130
+ `--prod --url <worker> --yes` to target production (with `LUNORA_ADMIN_TOKEN`).
131
+
132
+ ## `.global()` (D1) Structural Migration Flow
133
+
134
+ ```bash
135
+ # 1. Edit lunora/schema.ts (widen: add the optional new field to the .global() table).
136
+ lunora codegen
137
+
138
+ # 2. Generate the SQL migration by diffing schema against the snapshot baseline.
139
+ lunora migrate generate --name=add_user_status
140
+
141
+ # Writes lunora/migrations/<timestamp>_add_user_status.sql and updates
142
+ # lunora/migrations/.snapshot.json. Review the SQL before committing.
143
+
144
+ # 3. Deploy — @lunora/d1's runner applies pending migrations.
145
+ lunora deploy
146
+ ```
147
+
148
+ `lunora migrate generate` only considers `.global()` tables (root/sharded tables
149
+ are not D1-backed). Run it after each schema edit in the widen and narrow steps;
150
+ backfill data with an online migration between them.
151
+
152
+ ## The Schema-Drift Gate
153
+
154
+ `lunora deploy` (and `verify` / `prepare`) run a **pre-deploy schema-drift gate**:
155
+ it compares the committed structural baseline (`lunora/.lunora-schema.json`)
156
+ against the snapshot codegen produced this run. Breaking drift **without an
157
+ accompanying data migration blocks the deploy**. The baseline is only re-blessed
158
+ _after_ the deploy succeeds, so a failed deploy never advances it past a change
159
+ that never shipped.
160
+
161
+ If the gate blocks you: that is the signal to stage the change (widen first) or
162
+ add the migration — not to bypass it.
163
+
164
+ ## Common Pitfalls
165
+
166
+ 1. **Making a field required before backfilling.** The drift gate / D1 rejects
167
+ the deploy because existing rows lack it. Widen first.
168
+ 2. **Reshaping rows by hand instead of `defineMigration`.** A hand-rolled
169
+ `internalMutation` that `.collect()`s a large table hits transaction limits
170
+ and is not resumable. Use `defineMigration` — it batches and tracks per-shard
171
+ progress.
172
+ 3. **Not writing the new shape during the migration window.** Rows created mid-
173
+ migration get missed, leaving unmigrated data after it "completes." Start
174
+ dual-writing in the widen step.
175
+ 4. **Skipping the dry run.** `lunora migrate up <id> --dry-run` validates the
176
+ transform before it touches real rows.
177
+ 5. **Deleting a field prematurely.** Deprecate with `v.optional` + a comment;
178
+ delete only once nothing references it.
179
+ 6. **Migrating the wrong layer.** A `.global()` structural change needs `lunora
180
+ migrate generate` (SQL); a data backfill needs a `defineMigration`. Check the
181
+ table's `.global()` / `.shardBy()` modifier first.
182
+
183
+ ## Checklist
184
+
185
+ - [ ] Identified the change and which layer it touches (ShardDO vs `.global()`).
186
+ - [ ] Widened the schema to accept both shapes; `lunora codegen` clean.
187
+ - [ ] Updated reads to handle both shapes; started writing the new shape.
188
+ - [ ] Deployed the widened schema.
189
+ - [ ] Authored a `defineMigration`; previewed with `lunora migrate up --dry-run`.
190
+ - [ ] Ran `lunora migrate up`; `lunora migrate generate` + deploy for `.global()`
191
+ structural changes.
192
+ - [ ] Verified completion with `lunora migrate status`.
193
+ - [ ] Narrowed the schema (required / drop old field); removed both-shapes code.
194
+ - [ ] Deployed the final schema; schema-drift gate passed.
@@ -0,0 +1,143 @@
1
+ ---
2
+ name: lunora-performance-audit
3
+ description: Diagnoses and fixes Lunora performance problems — full-table scans, missing
4
+ indexes, OCC write conflicts, oversized subscriptions, and sharding/`.global()`
5
+ scaling. Use when queries are slow, mutations conflict, or `@lunora/advisor`
6
+ flags a table.
7
+ ---
8
+
9
+ # Lunora Performance Audit
10
+
11
+ Systematically diagnose Lunora performance issues, route to the right fix, and
12
+ apply it across sibling functions consistently.
13
+
14
+ ## When to Use
15
+
16
+ - Queries feel slow or read far more rows than they return.
17
+ - Mutations retry or conflict under load (OCC).
18
+ - Subscriptions fan out updates too broadly or re-run too often.
19
+ - The Lunora Studio **Advisors** tab or `@lunora/advisor` flags a table.
20
+
21
+ ## When Not to Use
22
+
23
+ - Scale is small, traffic is modest, and there is no measured problem — prefer
24
+ simpler code. Do not introduce sharding, digests, or splits on speculation.
25
+
26
+ ## Core Workflow
27
+
28
+ 1. **Scope** one concrete user flow with a clear entry and exit.
29
+ 2. **Trace** every `ctx.db` read and write in that flow.
30
+ 3. **Route** to the matching problem class below.
31
+ 4. **Fix siblings** consistently — every function touching the same table should
32
+ read it the same indexed way.
33
+ 5. **Verify** behavior is unchanged and the advisor finding clears.
34
+
35
+ ## Signal Gathering
36
+
37
+ Start with the static advisors — they need no traffic:
38
+
39
+ - **Lunora Studio → Advisors tab** surfaces `@lunora/advisor` findings live in
40
+ dev.
41
+ - `@lunora/advisor` runs static lints over `defineSchema` + discovered query
42
+ reads / insert writes. Relevant performance/schema rules:
43
+ - `filter-without-index` — a query filters a table with no covering index.
44
+ - `unindexed-foreign-key` — a relation/FK column has no index.
45
+ - `duplicate-index` / `empty-index` — wasted or malformed indexes.
46
+ - `index-references-unknown-field`, `relation-references-unknown-field`,
47
+ `relation-references-unknown-table` — broken index/relation definitions.
48
+ - `table-without-insert` — a table is read but never written (often a
49
+ schema/typo signal).
50
+
51
+ ## Problem Class: Read Amplification (the common one)
52
+
53
+ **Symptom:** a query reads the whole table to return a few rows;
54
+ `filter-without-index` fires.
55
+
56
+ **Fix:** read through an index, not `.filter()`. Declare the index on the table
57
+ and use `.withIndex()` with an equality/range on the leading columns.
58
+
59
+ ```ts
60
+ // Bad — scans every row, then filters in memory.
61
+ const mine = (await ctx.db.query("documents").collect()).filter((d) => d.orgId === orgId);
62
+
63
+ // Good — declare the index…
64
+ documents: defineTable({ orgId: v.string(), createdAt: v.number() /* … */ }).index("by_org_created", ["orgId", "createdAt"]);
65
+
66
+ // …and read through it.
67
+ const mine = await ctx.db
68
+ .query("documents")
69
+ .withIndex("by_org_created", (q) => q.eq("orgId", orgId))
70
+ .collect();
71
+ ```
72
+
73
+ Index columns are ordered: put equality columns first, then the range/sort
74
+ column. Fix every sibling query on the table the same way.
75
+
76
+ ## Problem Class: Write Conflicts (OCC)
77
+
78
+ **Symptom:** mutations on hot rows retry or fail under concurrency. ShardDO uses
79
+ optimistic concurrency control — concurrent writes to the same DO that touch
80
+ overlapping state conflict and retry.
81
+
82
+ **Fixes, in order of preference:**
83
+
84
+ 1. **Narrow the write.** Patch only the fields that changed; avoid read-modify-
85
+ write over rows another mutation also touches.
86
+ 2. **Partition with `.shardBy(key)`.** Move per-user / per-tenant / per-room
87
+ state into its own DO so writes for different keys never contend. This is the
88
+ primary horizontal-scale lever — most write contention is a single-DO
89
+ hotspot.
90
+ 3. **Avoid unbounded counters/aggregates in the hot path.** Accumulate in a
91
+ sharded/append shape and fold lazily rather than serializing every writer
92
+ through one row.
93
+
94
+ ## Problem Class: Subscription Cost
95
+
96
+ **Symptom:** a `useQuery` re-runs and re-pushes to many clients on unrelated
97
+ writes.
98
+
99
+ **Fixes:**
100
+
101
+ - **Scope query args tightly** so a subscription only depends on the rows it
102
+ renders — a query keyed by `orgId` should not re-run for another org's write.
103
+ - **Read through indexes** (above) so the reactive dependency is the narrow
104
+ index range, not the whole table.
105
+ - ShardDO subscriptions are **hibernated WebSockets** — idle connections cost
106
+ nothing; the lever is _how many rows each live query depends on_, not raw
107
+ connection count.
108
+
109
+ ## Problem Class: Cross-Region Reads
110
+
111
+ **Symptom:** read-heavy, rarely-written data is slow for far-away users.
112
+
113
+ **Fix:** chain `.global()` on the table to replicate it to D1 for low-latency
114
+ cross-region reads (with read-your-writes via the Sessions API). Reserve it for
115
+ read-mostly tables — `.global()` adds the D1 migration flow (see the
116
+ `lunora-migration-helper` skill) and write-path cost.
117
+
118
+ ### `.shardBy(key)` vs `.global()` — choose one per table
119
+
120
+ - `.shardBy(key)`: partitions a table across Durable Objects by key — scales
121
+ _writes_ (e.g. messages per room). Reads are per-shard.
122
+ - `.global()`: replicates a table to D1 — scales _cross-region reads_ with
123
+ read-your-writes (e.g. a mostly-read catalog).
124
+ - They are not combined on the same table; the default (neither) is a single
125
+ root-scoped ShardDO.
126
+
127
+ ## Guardrails
128
+
129
+ - Prefer simpler code when scale is small or the signal is weak.
130
+ - Do not recommend structural changes (digest tables, document splitting,
131
+ sharding) without a measured signal or a known hot path.
132
+ - When you change how one function reads/writes a table, change its siblings to
133
+ match — half-migrated access patterns are their own bug.
134
+
135
+ ## Checklist
136
+
137
+ - [ ] Scoped one concrete flow; traced every `ctx.db` read/write.
138
+ - [ ] Checked the Studio Advisors tab / `@lunora/advisor` findings.
139
+ - [ ] Read amplification: replaced `.filter()` with an indexed `.withIndex()`.
140
+ - [ ] Write conflicts: narrowed writes and/or partitioned with `.shardBy(key)`.
141
+ - [ ] Subscription cost: scoped query args so live queries depend on few rows.
142
+ - [ ] Cross-region: applied `.global()` only to read-mostly tables.
143
+ - [ ] Fixed sibling functions consistently; verified behavior unchanged.
@@ -0,0 +1,240 @@
1
+ ---
2
+ name: lunora-quickstart
3
+ description: Creates or adds Lunora to an app. Use for new Lunora projects, `lunora init`,
4
+ framework/provider wiring, the first `lunora dev` run, env vars, or writing
5
+ the first schema + query/mutation round-trip.
6
+ ---
7
+
8
+ # Lunora Quickstart
9
+
10
+ Set up a working Lunora project as fast as possible.
11
+
12
+ ## When to Use
13
+
14
+ - Starting a brand new project with Lunora.
15
+ - Adding Lunora to an existing Vite, Next.js, Astro, Nuxt, SvelteKit, or
16
+ TanStack Start app.
17
+ - Scaffolding a Lunora app for prototyping.
18
+
19
+ ## When Not to Use
20
+
21
+ - The project already has Lunora installed and `lunora/` exists — just build,
22
+ and run `lunora codegen` after schema/function edits.
23
+ - You only need to add auth to an existing Lunora app — use the
24
+ `lunora-setup-auth` skill.
25
+
26
+ ## Workflow
27
+
28
+ 1. Determine the starting point: new project or existing app.
29
+ 2. New project: scaffold with `lunora init` and pick a template.
30
+ 3. Existing app: run `lunora init --here` to patch the Vite config and wire
31
+ Lunora into the current project.
32
+ 4. Run `lunora codegen` to generate `lunora/_generated/` and typecheck the
33
+ schema + functions. This is the agent's feedback loop.
34
+ 5. Start the dev loop with `lunora dev` (ask the user to run it locally, or
35
+ start it in the background for cloud/headless agents — it is long-running and
36
+ does not exit).
37
+ 6. Verify a query/mutation round-trip works end to end.
38
+
39
+ ## Path 1: New Project (Recommended)
40
+
41
+ `lunora init` fetches a whole-project template (frontend + worker entry + Vite
42
+ plugin + `lunora/` already wired together).
43
+
44
+ ```bash
45
+ lunora init my-app --template vite
46
+ cd my-app
47
+ pnpm install
48
+ ```
49
+
50
+ ### Pick a template
51
+
52
+ | Template | Stack |
53
+ | ---------------------- | ---------------------------------------------- |
54
+ | `vite` | React + Vite (the simplest full-stack starter) |
55
+ | `standalone` | Worker-only Lunora backend, no frontend |
56
+ | `astro` | Astro integration |
57
+ | `nuxt` | Nuxt (Vue) |
58
+ | `sveltekit` | SvelteKit |
59
+ | `tanstack-start-react` | TanStack Start (React) |
60
+ | `tanstack-start-solid` | TanStack Start (Solid) |
61
+
62
+ If the user has not specified a preference, default to `vite`. Pass `--template`
63
+ explicitly to avoid the interactive prompt. Templates are fetched remotely (via
64
+ `giget`) from `gh:anolilab/lunora/templates/<type>`; pass `--from <dir>` to use a
65
+ local template directory offline.
66
+
67
+ ### Generate types and push the first run
68
+
69
+ Run this yourself — it is one-shot and exits cleanly:
70
+
71
+ ```bash
72
+ lunora codegen
73
+ ```
74
+
75
+ It writes `lunora/_generated/` and typechecks your schema + functions. Read its
76
+ output to find out whether the code you just wrote is valid.
77
+
78
+ ### Start the dev loop
79
+
80
+ ```bash
81
+ lunora dev
82
+ ```
83
+
84
+ `lunora dev` runs the Vite dev server with the Cloudflare Worker on the same
85
+ origin, plus codegen-on-save and the Lunora Studio. It is long-running and does
86
+ not exit, so:
87
+
88
+ - **Local development (user at the keyboard):** ask the user to run `lunora dev`
89
+ in a terminal.
90
+ - **Cloud or headless agents:** start `lunora dev` in the background.
91
+
92
+ Vite serves on `http://localhost:5173` by default; the Worker is served on the
93
+ same origin via `@cloudflare/vite-plugin`.
94
+
95
+ ## Path 2: Add Lunora to an Existing App
96
+
97
+ Use this when the user already has a Vite-based frontend and wants Lunora as the
98
+ backend.
99
+
100
+ ```bash
101
+ lunora init --here
102
+ ```
103
+
104
+ This finds the existing `vite.config.*` (or creates a minimal one), patches in
105
+ the Lunora Vite plugin, and scaffolds a starter `lunora/`. Then run
106
+ `lunora codegen` and `lunora dev` as above.
107
+
108
+ ### Wire up the client provider
109
+
110
+ Create the `LunoraClient` once at module scope (never inside a component) and
111
+ wrap the app with the framework provider. React example:
112
+
113
+ ```tsx
114
+ // src/client/main.tsx
115
+ import { LunoraClient } from "@lunora/client";
116
+ import { LunoraProvider } from "@lunora/react";
117
+ import { StrictMode } from "react";
118
+ import { createRoot } from "react-dom/client";
119
+
120
+ import { App } from "./App";
121
+
122
+ // @cloudflare/vite-plugin serves the Worker on the same origin as Vite.
123
+ const url = (import.meta.env.VITE_LUNORA_URL as string | undefined) ?? globalThis.location.origin;
124
+ const client = new LunoraClient({ url });
125
+
126
+ createRoot(document.querySelector("#root")!).render(
127
+ <StrictMode>
128
+ <LunoraProvider client={client}>
129
+ <App />
130
+ </LunoraProvider>
131
+ </StrictMode>,
132
+ );
133
+ ```
134
+
135
+ Vue, Solid, and Svelte have matching providers in `@lunora/vue`, `@lunora/solid`,
136
+ and `@lunora/svelte`. `VITE_LUNORA_URL` is optional — it defaults to
137
+ `location.origin`, which is correct for the single-origin dev setup.
138
+
139
+ ## Writing Your First Function
140
+
141
+ Create a schema and a query/mutation to verify the full loop.
142
+
143
+ `lunora/schema.ts`:
144
+
145
+ ```ts
146
+ import { defineSchema, defineTable, v } from "@lunora/server";
147
+
148
+ export default defineSchema({
149
+ todos: defineTable({
150
+ text: v.string(),
151
+ done: v.boolean(),
152
+ createdAt: v.number(),
153
+ }).index("by_creation", ["createdAt"]),
154
+ });
155
+ ```
156
+
157
+ `lunora/todos.ts`:
158
+
159
+ ```ts
160
+ import type { Id } from "@lunora/server";
161
+ import { mutation, query, v } from "@lunora/server";
162
+
163
+ export const list = query.query(async ({ ctx }) => ctx.db.query("todos").withIndex("by_creation").collect());
164
+
165
+ export const add = mutation
166
+ .input({ text: v.string() })
167
+ .mutation(async ({ ctx, args: { text } }): Promise<Id<"todos">> => ctx.db.insert("todos", { text, done: false, createdAt: Date.now() }));
168
+ ```
169
+
170
+ Run `lunora codegen`, then use it in a component. The `api` object and `Doc` /
171
+ `Id` types come from `lunora/_generated/`:
172
+
173
+ ```tsx
174
+ import { useMutation, useQuery } from "@lunora/react";
175
+
176
+ import { api } from "../../lunora/_generated/api";
177
+ import type { Doc } from "../../lunora/_generated/dataModel";
178
+
179
+ function Todos() {
180
+ const todos = useQuery(api.todos.list, {}) as Doc<"todos">[] | undefined;
181
+ const { mutate: add, pending } = useMutation(api.todos.add);
182
+
183
+ return (
184
+ <div>
185
+ <button disabled={pending} onClick={() => add({ text: "New todo" })}>
186
+ Add
187
+ </button>
188
+ {todos?.map((t) => (
189
+ <div key={t._id}>{t.text}</div>
190
+ ))}
191
+ </div>
192
+ );
193
+ }
194
+ ```
195
+
196
+ `useQuery` opens a live subscription: the list re-renders the instant any
197
+ mutation changes the queried rows.
198
+
199
+ ## Development vs Production
200
+
201
+ Use `lunora dev` during development. When ready to ship:
202
+
203
+ ```bash
204
+ lunora deploy
205
+ ```
206
+
207
+ `lunora deploy` runs codegen, the schema-drift gate, and `wrangler deploy`. Do
208
+ not use it during day-to-day development.
209
+
210
+ Before deploying, run the preflight:
211
+
212
+ ```bash
213
+ lunora doctor
214
+ ```
215
+
216
+ It checks `wrangler.jsonc` (the `SHARD` durable-object binding), D1 placeholder
217
+ ids, `.dev.vars` secrets, and container exports.
218
+
219
+ ## Next Steps
220
+
221
+ - Add authentication: use the `lunora-setup-auth` skill.
222
+ - Add a prebuilt capability (mail, presence, storage, rate limit, crons):
223
+ `lunora registry add <item>` (see `lunora registry list`). For capabilities
224
+ with a dedicated skill, use it: `lunora-setup-mail`, `lunora-setup-storage`,
225
+ `lunora-setup-scheduler`. See the `lunora` router's capability entry for the
226
+ full routing.
227
+ - Build your own reusable capability: use the `lunora-create-package` skill.
228
+ - Plan a schema change: use the `lunora-migration-helper` skill.
229
+ - Scaffold more functions: `vis generate lunora-query --name=listMessages`,
230
+ `lunora-mutation`, `lunora-action`, `lunora-table`, `lunora-cron` (always use
231
+ the `--name=value` form).
232
+
233
+ ## Checklist
234
+
235
+ - [ ] Determined starting point: new project or existing app.
236
+ - [ ] New project: scaffolded with `lunora init --template <t>`.
237
+ - [ ] Existing app: ran `lunora init --here` and wired `LunoraProvider`.
238
+ - [ ] Ran `lunora codegen`: `lunora/_generated/` exists and typecheck is clean.
239
+ - [ ] `lunora dev` is running — user terminal, or background for cloud agents.
240
+ - [ ] Verified a query/mutation round-trip re-renders the client live.