create-zenbu-app 0.0.11 → 0.0.12

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,1434 @@
1
+ # Zenbu.js docs
2
+
3
+ # Overview
4
+ Source: https://zenbulabs.mintlify.app/api-reference/overview
5
+
6
+
7
+
8
+ ## Packages
9
+
10
+ | Package | Description |
11
+ | ------------------------ | ---------------------------------------------------- |
12
+ | `@zenbujs/core/runtime` | Service base class and runtime. |
13
+ | `@zenbujs/core/services` | Built-in core services (Window, DB, RPC, HTTP, etc). |
14
+ | `@zenbujs/core/config` | `defineConfig`, `definePlugin`, `defineBuildConfig`. |
15
+ | `@zenbujs/core/db` | Database schema authoring (`createSchema`, `z`). |
16
+ | `@zenbujs/core/advice` | Advice types and helpers. |
17
+ | `@zenbujs/core/react` | React hooks for the renderer. |
18
+ | `create-zenbu-app` | CLI scaffolding tool. |
19
+
20
+ ## Runtime
21
+
22
+ ```typescript theme={null}
23
+ import { Service } from "@zenbujs/core/runtime"
24
+ ```
25
+
26
+ ## Config
27
+
28
+ ```typescript theme={null}
29
+ import {
30
+ defineConfig,
31
+ definePlugin,
32
+ defineBuildConfig,
33
+ } from "@zenbujs/core/config"
34
+ ```
35
+
36
+ ## Database
37
+
38
+ ```typescript theme={null}
39
+ import { createSchema, z } from "@zenbujs/core/db"
40
+ ```
41
+
42
+ ## React hooks
43
+
44
+ ```typescript theme={null}
45
+ import {
46
+ useDb,
47
+ useDbClient,
48
+ useCollection,
49
+ useRpc,
50
+ useEvents,
51
+ useViewArgs,
52
+ } from "@zenbujs/core/react"
53
+ ```
54
+
55
+ ## CLI
56
+
57
+ ```bash theme={null}
58
+ # Create a new app
59
+ pnpx create-zenbu-app my-app
60
+
61
+ # Dev server with hot reload
62
+ pnpm run dev
63
+
64
+ # Regenerate types
65
+ pnpm run link
66
+
67
+ # Generate a database migration
68
+ pnpm run db:generate
69
+
70
+ # Build for production
71
+ pnpm run build:source
72
+ pnpm run build:electron
73
+ ```
74
+
75
+
76
+ # Concepts
77
+ Source: https://zenbulabs.mintlify.app/concepts
78
+
79
+
80
+
81
+ Zenbu.js apps are Electron apps. The two processes you work with most are:
82
+
83
+ * **Main process** - a Node.js process that has access to the file system and operating system.
84
+ * **Renderer process** - a Chromium browser window that runs your React UI.
85
+
86
+ The two processes communicate through Zenbu's RPC and event system instead of raw [Electron IPC](https://www.electronjs.org/docs/latest/tutorial/ipc).
87
+
88
+ ## Supported runtimes
89
+
90
+ | Runtime | Status |
91
+ | -------- | ------ |
92
+ | Electron | ✅ |
93
+ | Tauri | ⏳ |
94
+ | Web | ⏳ |
95
+
96
+ ## Plugins
97
+
98
+ Every Zenbu.js application is itself a plugin. Your app and any third-party plugin that extends it share the same set of capabilities. Everything described below applies equally to both.
99
+
100
+ ## Services
101
+
102
+ Services are shared objects that run in the main process. They hold state, manage resources, and can depend on other services ([dependency injection](https://en.wikipedia.org/wiki/Dependency_injection)). Every public method on a service is automatically available to the renderer process via type-safe RPC.
103
+
104
+ ```typescript src/main/services/counter.ts theme={null}
105
+ import { Service } from "@zenbujs/core/runtime"
106
+
107
+ export class CounterService extends Service.create({
108
+ key: "counter",
109
+ }) {
110
+ private count = 0
111
+
112
+ evaluate() {
113
+ // Called when the service starts
114
+ }
115
+
116
+ increment() {
117
+ this.count++
118
+ return this.count
119
+ }
120
+
121
+ getCount() {
122
+ return this.count
123
+ }
124
+ }
125
+ ```
126
+
127
+ When you edit a service file and save, `evaluate()` re-runs immediately without restarting the app.
128
+
129
+ ## Database
130
+
131
+ Zenbu.js includes a JSON database that syncs across every process. Components that read from the database automatically re-render when the data they depend on changes.
132
+
133
+ ```typescript theme={null}
134
+ import { useDb, useDbClient } from "@zenbujs/core/react"
135
+
136
+ function Counter() {
137
+ const count = useDb(root => root.app.count)
138
+ const client = useDbClient()
139
+
140
+ return (
141
+ <button onClick={() => {
142
+ client.update(root => { root.app.count++ })
143
+ }}>
144
+ Count: {count}
145
+ </button>
146
+ )
147
+ }
148
+ ```
149
+
150
+ ## [RPC](https://en.wikipedia.org/wiki/Remote_procedure_call)
151
+
152
+ Every public method on a service becomes callable from the renderer process with full TypeScript inference. You define a service class in the main process and call it from React through `useRpc()`.
153
+
154
+ ```typescript theme={null}
155
+ import { useRpc } from "@zenbujs/core/react"
156
+
157
+ function App() {
158
+ const rpc = useRpc()
159
+
160
+ const handleClick = async () => {
161
+ const newCount = await rpc.counter.increment()
162
+ console.log(newCount)
163
+ }
164
+
165
+ return <button onClick={handleClick}>Increment</button>
166
+ }
167
+ ```
168
+
169
+ ## Events
170
+
171
+ Services in the main process can emit events that the renderer process subscribes to. Events cross process boundaries automatically.
172
+
173
+ A service emits an event through `this.ctx.rpc.emit`:
174
+
175
+ ```typescript src/main/services/notifications.ts theme={null}
176
+ export class NotificationService extends Service.create({
177
+ key: "notifications",
178
+ }) {
179
+ send(message: string) {
180
+ this.ctx.rpc.emit.app.notification({ message })
181
+ }
182
+ }
183
+ ```
184
+
185
+ The renderer process listens with `useEvents()`:
186
+
187
+ ```typescript theme={null}
188
+ import { useEvents } from "@zenbujs/core/react"
189
+ import { useEffect, useState } from "react"
190
+
191
+ function Notifications() {
192
+ const events = useEvents()
193
+ const [message, setMessage] = useState("")
194
+
195
+ useEffect(() => {
196
+ const unsubscribe = events.app.notification.subscribe((data) => {
197
+ setMessage(data.message)
198
+ })
199
+ return unsubscribe
200
+ }, [])
201
+
202
+ return message ? <div className="toast">{message}</div> : null
203
+ }
204
+ ```
205
+
206
+
207
+ # Advice
208
+ Source: https://zenbulabs.mintlify.app/core/advice
209
+
210
+
211
+
212
+ Advice lets a plugin wrap or replace a function or React component owned by another plugin, without modifying the original source.
213
+
214
+ This API is inspired from Emacs, where [`defadvice`](https://www.gnu.org/software/emacs/manual/html_node/elisp/Advising-Functions.html) is used to modify existing functions without editing their source.
215
+
216
+ ## Registering advice
217
+
218
+ Call `this.advise(...)` from inside a service:
219
+
220
+ ```typescript theme={null}
221
+ import { Service } from "@zenbujs/core/runtime"
222
+
223
+ export class ChromeService extends Service.create({ key: "chrome" }) {
224
+ evaluate() {
225
+ this.setup("wrap-counter", () =>
226
+ this.advise({
227
+ view: "entrypoint",
228
+ moduleId: "App.tsx",
229
+ name: "Counter",
230
+ type: "around",
231
+ modulePath: "src/content/wrap-counter.tsx",
232
+ exportName: "WrapCounter",
233
+ }),
234
+ )
235
+ }
236
+ }
237
+ ```
238
+
239
+ | Field | Meaning |
240
+ | ------------ | ----------------------------------------------------------------------- |
241
+ | `view` | View type to apply the advice in. `"*"` for every view. |
242
+ | `moduleId` | Suffix of the source file that exports the target. |
243
+ | `name` | Name of the export to advise. |
244
+ | `type` | One of `"replace"`, `"before"`, `"after"`, `"around"`. |
245
+ | `modulePath` | Path to your wrapper module, relative to the plugin root (or absolute). |
246
+ | `exportName` | Named export inside the wrapper module. |
247
+
248
+ Returns an unregister function. Use it as a `setup` cleanup.
249
+
250
+ ## Around advice
251
+
252
+ Receives the original as `__original` in props, making it the most flexible advice type.
253
+
254
+ ```tsx src/content/wrap-counter.tsx theme={null}
255
+ export function WrapCounter(props) {
256
+ const Original = props.__original
257
+ return (
258
+ <div className="bordered">
259
+ <Original {...props} />
260
+ </div>
261
+ )
262
+ }
263
+ ```
264
+
265
+ Use this when you want to render the original but add structure around it or change its props.
266
+
267
+ ## Replace advice
268
+
269
+ Substitutes the export entirely. The wrapper is used in place of the original.
270
+
271
+ ```tsx theme={null}
272
+ export function WrapCounter() {
273
+ return <button>Replaced</button>
274
+ }
275
+ ```
276
+
277
+ ## Before / After
278
+
279
+ `before` and `after` advice run extra logic around the original function.
280
+
281
+ ```typescript theme={null}
282
+ // before: runs your function, then calls the original
283
+ export function beforeSave(args: { path: string }) {
284
+ console.log("saving", args.path)
285
+ }
286
+
287
+ // after: calls the original, then runs your function with the result
288
+ export function afterSave(result: void, args: { path: string }) {
289
+ console.log("saved", args.path)
290
+ }
291
+ ```
292
+
293
+ ## Hot reloading
294
+
295
+ Adding, removing, or editing a `this.advise(...)` call reloads the affected views so the new advice takes effect. Edits inside the advice module itself hot-replace through Vite's normal HMR.
296
+
297
+
298
+ # Content Scripts
299
+ Source: https://zenbulabs.mintlify.app/core/content-scripts
300
+
301
+
302
+
303
+ A content script is JavaScript that gets injected into a page at runtime. It runs in the same environment as the page, so it can use React hooks, read from the database, and call RPC methods. Content scripts target views by type, so you can inject into specific views or use `"*"` to inject into all of them.
304
+
305
+ ## Use cases
306
+
307
+ * Add UI that appears on every page, like a devtools panel or a chat overlay.
308
+ * Add UI to a specific page without modifying that page's source code.
309
+ * Let a plugin contribute visible elements to the host app.
310
+
311
+ ## Registering a content script
312
+
313
+ Call `this.injectContentScript(...)` from a service:
314
+
315
+ ```typescript theme={null}
316
+ import { Service } from "@zenbujs/core/runtime"
317
+
318
+ export class DevtoolsService extends Service.create({ key: "devtools" }) {
319
+ evaluate() {
320
+ this.setup("inject", () =>
321
+ this.injectContentScript({
322
+ view: "*",
323
+ modulePath: "src/content/toolbar.tsx",
324
+ }),
325
+ )
326
+ }
327
+ }
328
+ ```
329
+
330
+ | Field | Meaning |
331
+ | ------------ | -------------------------------------------------------------------------------------------------------------------------- |
332
+ | `view` | The view type to inject into. Pass `"*"` to inject into every view. |
333
+ | `modulePath` | Path to the script. Relative paths resolve against the plugin's directory; absolute paths are accepted as an escape hatch. |
334
+
335
+ The call returns an unsubscribe function, so wrapping it in `this.setup()` ensures cleanup on hot reload.
336
+
337
+ ## Authoring a content script
338
+
339
+ A content script is a module that runs when the page loads. It can mount its own React tree into the DOM. This example adds a toolbar using a shadow root to keep its styles separate from the host page:
340
+
341
+ ```tsx src/content/toolbar.tsx theme={null}
342
+ import { StrictMode } from "react"
343
+ import { createRoot } from "react-dom/client"
344
+ import { ZenbuProvider } from "@zenbujs/core/react"
345
+ import { Toolbar } from "./Toolbar"
346
+
347
+ function mount() {
348
+ if (document.body?.dataset.myToolbarMounted === "1") return
349
+ document.body.dataset.myToolbarMounted = "1"
350
+
351
+ const host = document.createElement("div")
352
+ document.body.appendChild(host)
353
+ const shadow = host.attachShadow({ mode: "closed" })
354
+ const reactRoot = document.createElement("div")
355
+ shadow.appendChild(reactRoot)
356
+
357
+ createRoot(reactRoot).render(
358
+ <StrictMode>
359
+ <ZenbuProvider>
360
+ <Toolbar />
361
+ </ZenbuProvider>
362
+ </StrictMode>
363
+ )
364
+ }
365
+
366
+ mount()
367
+ ```
368
+
369
+ ## Content scripts vs advice
370
+
371
+ Content scripts add new UI alongside the host view's component tree. They cannot replace or wrap existing components. For that, use [advice](/guides/advice-functions) instead.
372
+
373
+
374
+ # Events
375
+ Source: https://zenbulabs.mintlify.app/core/events
376
+
377
+
378
+
379
+ Events let the main process send messages to the renderer process. They're useful for things like push notifications, streaming output, or reacting to something that happened on the server side. Unlike RPC (where the renderer calls the main process and waits for a response), events are one-way: the main process fires them and any listener in the renderer process receives them.
380
+
381
+ ## Defining events
382
+
383
+ You define events in a TypeScript file as a type with a name and payload for each event. This file needs to be registered in your config via `definePlugin({ events: "./src/main/events.ts" })` so the framework can keep everything type-safe.
384
+
385
+ ```typescript src/main/events.ts theme={null}
386
+ export type Events = {
387
+ todoCreated: { id: string; title: string }
388
+ todoCompleted: { id: string }
389
+ todoDeleted: { id: string }
390
+ }
391
+ ```
392
+
393
+ ## Emitting events
394
+
395
+ From a service, emit events through `this.ctx.rpc.emit`:
396
+
397
+ ```typescript theme={null}
398
+ import { Service } from "@zenbujs/core/runtime"
399
+ import { RpcService } from "@zenbujs/core/services"
400
+
401
+ export class TodoService extends Service.create({
402
+ key: "todo",
403
+ deps: { rpc: RpcService },
404
+ }) {
405
+ create(args: { title: string }) {
406
+ const id = crypto.randomUUID()
407
+ // ... create the todo
408
+ // emit.<plugin name>.<event name>(payload)
409
+ this.ctx.rpc.emit.app.todoCreated({ id, title: args.title })
410
+ }
411
+ }
412
+ ```
413
+
414
+ ## Listening to events
415
+
416
+ From React, use the `useEvents` hook to subscribe:
417
+
418
+ ```typescript theme={null}
419
+ import { useEvents } from "@zenbujs/core/react"
420
+
421
+ function Notifications() {
422
+ const events = useEvents()
423
+
424
+ useEffect(() => {
425
+ const off = events.app.todoCreated.subscribe(({ title }) => {
426
+ showToast(`New todo: ${title}`)
427
+ })
428
+ return off
429
+ }, [events])
430
+
431
+ return null
432
+ }
433
+ ```
434
+
435
+ The returned function unsubscribes the listener, so return it from your effect's cleanup to avoid accumulating subscriptions across renders.
436
+
437
+ ## Events vs RPC vs database
438
+
439
+ * **Events** are for transient updates that don't need to be persisted, like streaming terminal output or push notifications.
440
+ * **RPC** is for getting the main process to run code the renderer process can't, like reading a file or calling a system API.
441
+ * **Database** is for state that should persist and drive your UI.
442
+
443
+
444
+ # Plugins
445
+ Source: https://zenbulabs.mintlify.app/core/plugins
446
+
447
+
448
+
449
+ Every Zenbu.js application is itself a plugin. Your app is defined with `definePlugin()` the same way any third-party extension would be, and it has the same capabilities. The only thing that makes the main application special is the `uiEntrypoint` in `defineConfig()`, which tells the framework which plugin's code to load in the renderer process first.
450
+
451
+ ## Defining a plugin
452
+
453
+ Plugins are declared with `definePlugin()` from `@zenbujs/core/config`:
454
+
455
+ ```typescript zenbu.config.ts theme={null}
456
+ import { defineConfig, definePlugin } from "@zenbujs/core/config"
457
+
458
+ export default defineConfig({
459
+ db: "./.zenbu/db",
460
+ uiEntrypoint: "./src/renderer",
461
+ plugins: [
462
+ definePlugin({
463
+ name: "app",
464
+ services: ["./src/main/services/*.ts"],
465
+ schema: "./src/main/schema.ts",
466
+ events: "./src/main/events.ts",
467
+ migrations: "./migrations",
468
+ }),
469
+ ],
470
+ })
471
+ ```
472
+
473
+ | Field | Required | Description |
474
+ | ------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
475
+ | `name` | yes | Unique plugin identifier. Used as the top-level namespace for the plugin's database section, RPC services, and events (e.g. `db.<name>`, `rpc.<name>.<service>`, `events.<name>.<event>`). Reserved value: `core` belongs to the framework. |
476
+ | `services` | yes | Glob patterns for main process service files. |
477
+ | `schema` | no | Path to the database schema definition. |
478
+ | `events` | no | Path to event type definitions. |
479
+ | `migrations` | no | Directory of generated migration files. |
480
+ | `dependsOn` | no | Declares which other plugins this plugin needs type definitions from. See [Type dependencies](#type-dependencies). |
481
+
482
+ ## Scaffolding a plugin
483
+
484
+ `create-zenbu-app` can scaffold a plugin folder from anywhere on disk:
485
+
486
+ ```bash theme={null}
487
+ pnpm create zenbu-app --plugin my-plugin
488
+ ```
489
+
490
+ | Flag | Description |
491
+ | ------------------ | ------------------------------------------------------------------------------------------------------- |
492
+ | `--plugin` | Use the plugin template instead of the app template. Skips app-only prompts (Tailwind, build config). |
493
+ | `--no-add-to-host` | Skip the interactive prompt that offers to add the new plugin to each upstream host's `plugins:` array. |
494
+ | `--no-git` | Skip the git init prompt. Git init is auto-skipped when an ancestor already has a `.git/`. |
495
+ | `--yes` / `-y` | Auto-confirm every prompt with the default. |
496
+
497
+ The command scaffolds the plugin folder and installs its dependencies.
498
+
499
+ ## Adding a plugin
500
+
501
+ Plugins can be inlined with `definePlugin()` or referenced by path to a `zenbu.plugin.ts` file:
502
+
503
+ ```typescript theme={null}
504
+ plugins: [
505
+ definePlugin({ name: "app", services: ["./src/main/services/*.ts"] }),
506
+ "./plugins/devtools/zenbu.plugin.ts",
507
+ ]
508
+ ```
509
+
510
+ Adding or removing a plugin in `zenbu.config.ts` is a hot-reloadable change that takes effect without restarting the app.
511
+
512
+ ## Plugin capabilities
513
+
514
+ A plugin has the same capabilities as the host application:
515
+
516
+ * **Services** run in the main process and expose methods via RPC.
517
+ * **Database schemas** define sections of the shared database.
518
+ * **Events** can be emitted and listened to across plugins.
519
+ * **Views** are iframe-mounted renderer entry points other plugins can embed.
520
+ * **Content scripts** inject JavaScript into any view.
521
+ * **Advice** lets a plugin wrap or replace any React component in the renderer.
522
+
523
+ ## Type dependencies
524
+
525
+ If a plugin needs to call another plugin's RPC methods, read its database, or subscribe to its events, it needs that plugin's type definitions. You declare this with `dependsOn`:
526
+
527
+ ```typescript zenbu.plugin.ts theme={null}
528
+ import { definePlugin } from "@zenbujs/core/config"
529
+
530
+ export default definePlugin({
531
+ name: "devtools",
532
+ services: ["./src/main/services/*.ts"],
533
+ dependsOn: [
534
+ { name: "app", from: "../../zenbu.config.ts" },
535
+ ],
536
+ })
537
+ ```
538
+
539
+ `name` is the plugin you depend on, and `from` is the path to the file that defines it. With this in place, you get typed access to that plugin's API:
540
+
541
+ ```typescript theme={null}
542
+ // Read app's database
543
+ const count = useDb(root => root.app.count)
544
+
545
+ // Call app's RPC methods
546
+ const rpc = useRpc()
547
+ await rpc.app.counter.increment()
548
+
549
+ // Subscribe to app's events
550
+ events.app.notification.subscribe(data => { ... })
551
+ ```
552
+
553
+ This is a type-only dependency. `dependsOn` tells `zen link` where to find the other plugin's service, schema, and event definitions so it can generate TypeScript types under `<plugin>/.zenbu/types/`. The generated files are `import type` pointers to the actual source on disk, and the entire `.zenbu/types/` directory is gitignored.
554
+
555
+ `zen link` runs automatically inside `zen dev`. Outside dev, run it manually before typechecking:
556
+
557
+ ```bash theme={null}
558
+ zen link
559
+ ```
560
+
561
+
562
+ # RPC
563
+ Source: https://zenbulabs.mintlify.app/core/rpc
564
+
565
+
566
+
567
+ When you define a service class in the main process, every public method becomes callable from the renderer process. Zenbu.js handles all the wiring between processes for you, so you just call methods like normal functions.
568
+
569
+ Under the hood, communication happens over a WebSocket, which means the same RPC system works whether your app runs in Electron or in the browser. The transport is pluggable, so you can swap it for Electron IPC, WebRTC, or anything else.
570
+
571
+ Services are scoped under their owning plugin's name. For a plugin named `app`, the call site is `rpc.app.<service>.<method>(...)`. Core's own services live under `rpc.core.<service>`.
572
+
573
+ ```typescript theme={null}
574
+ // Main process — declared inside the `app` plugin
575
+ export class MathService extends Service.create({
576
+ key: "math",
577
+ }) {
578
+ add(args: { a: number; b: number }) {
579
+ return args.a + args.b
580
+ }
581
+ }
582
+
583
+ // Renderer
584
+ const rpc = useRpc()
585
+ const result = await rpc.app.math.add({ a: 1, b: 2 }) // 3
586
+ ```
587
+
588
+ ## Type inference
589
+
590
+ TypeScript types flow from the service definition to the renderer call site. Parameters, return types, and method names are all inferred.
591
+
592
+ Types are generated by `zen link` (runs automatically during `zen dev`) and stored in `.zenbu/types/`, so if you rename a method or change its signature, the renderer code shows a type error immediately.
593
+
594
+ ## Async
595
+
596
+ All RPC calls are async from the renderer's perspective, even if the service method is synchronous. This is because calls cross a process boundary.
597
+
598
+ ```typescript theme={null}
599
+ // This service method is synchronous
600
+ getCount() {
601
+ return this.count
602
+ }
603
+
604
+ // But from React, it returns a Promise
605
+ const count = await rpc.app.counter.getCount()
606
+ ```
607
+
608
+ ## Error handling
609
+
610
+ Errors thrown in service methods are serialized and re-thrown in the renderer.
611
+
612
+ ```typescript theme={null}
613
+ // Main process
614
+ export class AuthService extends Service.create({ key: "auth" }) {
615
+ login(args: { email: string; password: string }) {
616
+ if (!args.email) throw new Error("Email is required")
617
+ // ...
618
+ }
619
+ }
620
+
621
+ // Renderer
622
+ try {
623
+ await rpc.app.auth.login({ email: "", password: "pass" })
624
+ } catch (e) {
625
+ console.log(e.message) // "Email is required"
626
+ }
627
+ ```
628
+
629
+ ## Method conventions
630
+
631
+ * **Take a single object argument.** This keeps argument signatures stable as the API grows.
632
+ * **Return JSON-serializable values.** Anything that round-trips through `JSON.stringify` works.
633
+
634
+
635
+ # Services
636
+ Source: https://zenbulabs.mintlify.app/core/services
637
+
638
+
639
+
640
+ A service is a class that extends `Service.create()` from `@zenbujs/core/runtime`. It runs in the main process.
641
+
642
+ ```typescript src/main/services/files.ts theme={null}
643
+ import { Service } from "@zenbujs/core/runtime"
644
+ import fs from "fs/promises"
645
+
646
+ export class FilesService extends Service.create({
647
+ key: "files",
648
+ }) {
649
+ evaluate() {
650
+ // Called when the service starts.
651
+ }
652
+
653
+ async readFile(args: { path: string }) {
654
+ return fs.readFile(args.path, "utf-8")
655
+ }
656
+
657
+ async writeFile(args: { path: string; content: string }) {
658
+ await fs.writeFile(args.path, args.content)
659
+ }
660
+ }
661
+ ```
662
+
663
+ Every public method is automatically exposed to the renderer process via RPC. You call them through `rpc.<plugin>.<service>.<method>`, so if this service belongs to a plugin named `app`, `readFile` is available as `rpc.app.files.readFile(...)`.
664
+
665
+ ## Calling from React
666
+
667
+ ```typescript theme={null}
668
+ import { useRpc } from "@zenbujs/core/react"
669
+
670
+ function Editor() {
671
+ const rpc = useRpc()
672
+
673
+ const load = async () => {
674
+ const content = await rpc.app.files.readFile({ path: "/path/to/file.txt" })
675
+ console.log(content)
676
+ }
677
+
678
+ return <button onClick={load}>Load</button>
679
+ }
680
+ ```
681
+
682
+ The call is fully type-safe. Parameters and return types are inferred from the service class.
683
+
684
+ ## Dependencies
685
+
686
+ Services declare dependencies on other services through the `deps` field. The framework automatically figures out the right order to start them in, so each service's dependencies are ready before it runs.
687
+
688
+ ```typescript theme={null}
689
+ import { Service } from "@zenbujs/core/runtime"
690
+ import { WindowService } from "@zenbujs/core/services"
691
+
692
+ export class AppService extends Service.create({
693
+ key: "app",
694
+ deps: { window: WindowService },
695
+ }) {
696
+ async evaluate() {
697
+ await this.ctx.window.openView({ type: "entrypoint" })
698
+ }
699
+ }
700
+ ```
701
+
702
+ By the time `evaluate()` runs, all dependencies in `this.ctx` are fully initialized.
703
+
704
+ ## Setups and cleanups
705
+
706
+ Inside `evaluate()`, anything that needs to be torn down on hot reload or shutdown should be wrapped in `this.setup()`:
707
+
708
+ ```typescript theme={null}
709
+ evaluate() {
710
+ this.setup("interval", () => {
711
+ const id = setInterval(tick, 1000)
712
+ return () => clearInterval(id)
713
+ })
714
+ }
715
+ ```
716
+
717
+ The returned function is the cleanup. It runs before the next setup with the same name, or when the service is torn down.
718
+
719
+ ## Hot reloading
720
+
721
+ When you edit a service file and save, the framework re-evaluates affected services automatically. This works in both development and production.
722
+
723
+
724
+ # Database
725
+ Source: https://zenbulabs.mintlify.app/core/state
726
+
727
+
728
+
729
+ Zenbu.js includes a JSON database that syncs across every process. Every process (main and renderer) holds an in-memory copy (also known as a replica) of the database. Reads are always local and synchronous. When a write happens in any process, it syncs to all other replicas automatically.
730
+
731
+ This means both the main process and the renderer read and write through the same interface. The only difference is how you access the client: through `DbService` in a service, or through React hooks in the renderer.
732
+
733
+ ## Schema
734
+
735
+ Each plugin defines a schema using zod that describes the shape of its data.
736
+
737
+ ```typescript src/main/schema.ts theme={null}
738
+ import { createSchema, z } from "@zenbujs/core/db"
739
+
740
+ export default createSchema({
741
+ todos: z
742
+ .array(
743
+ z.object({
744
+ id: z.string(),
745
+ title: z.string(),
746
+ done: z.boolean(),
747
+ })
748
+ )
749
+ .default([]),
750
+ settings: z
751
+ .object({
752
+ theme: z.string(),
753
+ fontSize: z.number(),
754
+ })
755
+ .default({ theme: "dark", fontSize: 14 }),
756
+ })
757
+ ```
758
+
759
+ Fields without a `default()` will be `undefined` initially. Use `.default()` to set an initial value for a field when the database is first created.
760
+
761
+ ## Reading data
762
+
763
+ The database is a single JSON object called the `root`. Each plugin's data lives under its name, so `root.app` holds everything defined by the `app` plugin.
764
+
765
+ In the renderer, use the `useDb` hook to read from the database.
766
+
767
+ ```typescript theme={null}
768
+ import { useDb } from "@zenbujs/core/react"
769
+
770
+ function TodoList() {
771
+ const todos = useDb(root => root.app.todos)
772
+
773
+ return (
774
+ <ul>
775
+ {todos.map(todo => (
776
+ <li key={todo.id}>{todo.title}</li>
777
+ ))}
778
+ </ul>
779
+ )
780
+ }
781
+ ```
782
+
783
+ `useDb` is a subscription. When the selected value changes, the component re-renders automatically. Unrelated changes don't trigger re-renders.
784
+
785
+ In a service, read through `DbService.client`:
786
+
787
+ ```typescript theme={null}
788
+ const root = this.ctx.db.client.readRoot()
789
+ const todos = root.app.todos
790
+ ```
791
+
792
+ `readRoot()` returns a synchronous snapshot since the entire root is held in memory. You can also subscribe to a specific field to react when it changes:
793
+
794
+ ```typescript theme={null}
795
+ const unsubscribe = this.ctx.db.client.app.todos.subscribe(todos => {
796
+ console.log("todos changed", todos)
797
+ })
798
+ ```
799
+
800
+ The returned function unsubscribes. Wrap it in `this.setup()` so it cleans up on hot reload.
801
+
802
+ ## Writing data
803
+
804
+ In the renderer, use `useDbClient` to get a client that can write to the database:
805
+
806
+ ```typescript theme={null}
807
+ import { useDbClient } from "@zenbujs/core/react"
808
+
809
+ function AddTodo() {
810
+ const client = useDbClient()
811
+
812
+ const add = () => {
813
+ client.update(root => {
814
+ root.app.todos.push({
815
+ id: crypto.randomUUID(),
816
+ title: "New todo",
817
+ done: false,
818
+ })
819
+ })
820
+ }
821
+
822
+ return <button onClick={add}>Add</button>
823
+ }
824
+ ```
825
+
826
+ In a service, write through `DbService.client`:
827
+
828
+ ```typescript theme={null}
829
+ await this.ctx.db.client.update(root => {
830
+ root.app.todos.push({
831
+ id: crypto.randomUUID(),
832
+ title: "New todo",
833
+ createdAt: Date.now(),
834
+ })
835
+ })
836
+ ```
837
+
838
+ Inside `update()`, you mutate the root object directly, the same way you would with a regular JavaScript object. The database tracks these mutations and syncs them to other processes in the background.
839
+
840
+ ## Collections
841
+
842
+ Regular data fields are always held in memory across every process. Collections are for data that can grow large (like agent messages or logs) and should only be loaded into memory when needed.
843
+
844
+ Define a collection in your schema with `f.collection()`:
845
+
846
+ ```typescript src/main/schema.ts theme={null}
847
+ import { createSchema, f, z } from "@zenbujs/core/db"
848
+
849
+ export default createSchema({
850
+ messages: f.collection(
851
+ z.object({
852
+ text: z.string(),
853
+ author: z.string(),
854
+ }),
855
+ { debugName: "messages" }
856
+ ),
857
+ })
858
+ ```
859
+
860
+ Collections are append-only. In a service, use `concat` to add items:
861
+
862
+ ```typescript theme={null}
863
+ await this.ctx.db.client.app.messages.concat([
864
+ { text: "Hello", author: "alice" },
865
+ ])
866
+ ```
867
+
868
+ In the renderer, use `useCollection` to subscribe to a collection's data:
869
+
870
+ ```typescript theme={null}
871
+ import { useDb, useCollection } from "@zenbujs/core/react"
872
+
873
+ function Messages() {
874
+ const messagesRef = useDb(root => root.app.messages)
875
+ const { items, concat } = useCollection(messagesRef)
876
+ const [text, setText] = useState("")
877
+
878
+ return (
879
+ <div>
880
+ {items.map((msg, i) => (
881
+ <div key={i}>{msg.author}: {msg.text}</div>
882
+ ))}
883
+ <input value={text} onChange={e => setText(e.target.value)} />
884
+ <button onClick={() => {
885
+ concat([{ text, author: "alice" }])
886
+ setText("")
887
+ }}>Send</button>
888
+ </div>
889
+ )
890
+ }
891
+ ```
892
+
893
+ Collection data is only loaded into memory when you subscribe or read from it. This keeps the in-memory footprint small for data that can grow without bound.
894
+
895
+ ## Blobs
896
+
897
+ Blobs store binary data (`Uint8Array`) like files or images. Like collections, they live on disk and are only loaded into memory when you read them.
898
+
899
+ Define a blob in your schema with `f.blob()`:
900
+
901
+ ```typescript src/main/schema.ts theme={null}
902
+ import { createSchema, f, z } from "@zenbujs/core/db"
903
+
904
+ export default createSchema({
905
+ avatar: f.blob({ debugName: "avatar" }),
906
+ })
907
+ ```
908
+
909
+ Write binary data with `set`:
910
+
911
+ ```typescript theme={null}
912
+ const data = new TextEncoder().encode("hello")
913
+ await this.ctx.db.client.app.avatar.set(data)
914
+ ```
915
+
916
+ Read it back with `read`:
917
+
918
+ ```typescript theme={null}
919
+ const data = await this.ctx.db.client.app.avatar.read()
920
+ ```
921
+
922
+ ## Migrations
923
+
924
+ When you change your schema, existing databases need to be updated to match the new shape. Running `pnpm run db:generate` compares your current schema to the previous version and creates a migration file that describes what changed.
925
+
926
+ ```bash theme={null}
927
+ pnpm run db:generate
928
+ ```
929
+
930
+ The generated file is placed in your plugin's `migrations/` directory. Here's an example of what one looks like after adding a new `activeTabId` field to the schema:
931
+
932
+ ```typescript migrations/0003.ts theme={null}
933
+ import type { KyjuMigration } from "@zenbu/kyju"
934
+
935
+ export const migration: KyjuMigration = {
936
+ // Each migration increments the version number.
937
+ version: 3,
938
+ // The operations array describes the changes to apply.
939
+ operations: [
940
+ // "add" introduces a new key with a default value.
941
+ { op: "add", key: "activeTabId", kind: "data", hasDefault: true, default: null },
942
+ ],
943
+ }
944
+ ```
945
+
946
+ Three operation types cover most schema changes:
947
+
948
+ * **add**: introduces a new key, optionally with a default value.
949
+ * **remove**: drops an existing key.
950
+ * **alter**: updates the metadata of an existing key, like changing its default.
951
+
952
+ For more complex changes, add a `migrate` function to transform the data with custom logic. Use `ctx.apply` to run the declared operations first, then modify the result:
953
+
954
+ ```typescript migrations/0004.ts theme={null}
955
+ import type { KyjuMigration } from "@zenbu/kyju"
956
+
957
+ export const migration: KyjuMigration = {
958
+ version: 4,
959
+ operations: [
960
+ { op: "add", key: "fullName", kind: "data", hasDefault: true, default: "" },
961
+ ],
962
+ migrate: (prev, { apply }) => {
963
+ // Runs the auto-generated operations above
964
+ const result = apply(prev)
965
+ result.fullName = `${prev.firstName} ${prev.lastName}`
966
+ return result
967
+ },
968
+ }
969
+ ```
970
+
971
+ Migrations run automatically when the app starts. Each migration runs at most once, and during development, adding or editing a migration file triggers a reload without restarting the app.
972
+
973
+
974
+ # Views
975
+ Source: https://zenbulabs.mintlify.app/core/views
976
+
977
+
978
+
979
+ <Warning>The Views API is under construction</Warning>
980
+
981
+ A view is the rendering primitive in Zenbu.js. Every page your app renders is a view, including the main application itself, which is a view called `entrypoint` defined by your `uiEntrypoint` config.
982
+
983
+ The `entrypoint` view runs directly in the renderer process. Additional views run in [out of process iframes](https://www.chromium.org/developers/design-documents/oop-iframes/), and connect to the same [RPC](/core/rpc), [events](/core/events), and [database](/core/state).
984
+
985
+ Additional views are useful for:
986
+
987
+ * Letting a plugin embed content into your app.
988
+ * Isolating heavy features so a slow render in one view doesn't block the rest of the app.
989
+ * Letting teams or plugins develop parts of the app independently.
990
+
991
+ ## Registering a view
992
+
993
+ From a service, use `ViewRegistryService` to register a view type:
994
+
995
+ ```typescript theme={null}
996
+ import path from "node:path"
997
+ import { fileURLToPath } from "node:url"
998
+ import { Service } from "@zenbujs/core/runtime"
999
+ import { ViewRegistryService } from "@zenbujs/core/services"
1000
+
1001
+ export class TerminalService extends Service.create({
1002
+ key: "terminal",
1003
+ deps: { viewRegistry: ViewRegistryService },
1004
+ }) {
1005
+ async evaluate() {
1006
+ const here = path.dirname(fileURLToPath(import.meta.url))
1007
+ const viewRoot = path.resolve(here, "..", "..", "views", "terminal")
1008
+ await this.ctx.viewRegistry.register({
1009
+ type: "terminal",
1010
+ root: viewRoot,
1011
+ configFile: false,
1012
+ meta: { kind: "view", label: "Terminal" },
1013
+ })
1014
+ }
1015
+ }
1016
+ ```
1017
+
1018
+ The view directory must contain an `index.html` and a `main.tsx` that mounts a React tree wrapped in `<ZenbuProvider>`.
1019
+
1020
+ ## Embedding a view
1021
+
1022
+ ```tsx theme={null}
1023
+ import { View } from "@zenbujs/core/react"
1024
+
1025
+ <View type="terminal" args={{ tabId }} />
1026
+ ```
1027
+
1028
+ ## Reading view args
1029
+
1030
+ Inside the child view, use `useViewArgs` to read the args passed by the parent:
1031
+
1032
+ ```tsx theme={null}
1033
+ import { useViewArgs } from "@zenbujs/core/react"
1034
+
1035
+ function TerminalApp() {
1036
+ const { tabId } = useViewArgs<{ tabId: string }>()
1037
+ // ...
1038
+ }
1039
+ ```
1040
+
1041
+ When the parent updates `args`, the hook re-renders with the new values.
1042
+
1043
+
1044
+ # Releasing to Production
1045
+ Source: https://zenbulabs.mintlify.app/guides/production
1046
+
1047
+
1048
+
1049
+ A Zenbu.js app ships in two pieces:
1050
+
1051
+ * **The Electron app.** A small `.app` that contains a launcher and a bundled package manager. This is what the user installs.
1052
+ * **A git repository (the mirror).** A separate git repository that mirrors your development repo. It contains only the files needed to run the app. On first launch the Electron app clones the mirror, and on subsequent launches it pulls the latest version.
1053
+
1054
+ Because the source lives in a separate repository, you can push updates without rebuilding or re-distributing the Electron app. Auto-updates can be implemented through git pulls, so shipping a new version is as fast as pushing a commit.
1055
+
1056
+ ## Build config
1057
+
1058
+ The build is configured inside `zenbu.config.ts` using `defineBuildConfig`:
1059
+
1060
+ ```typescript zenbu.config.ts theme={null}
1061
+ import {
1062
+ defineConfig,
1063
+ definePlugin,
1064
+ defineBuildConfig,
1065
+ } from "@zenbujs/core/config";
1066
+
1067
+ export default defineConfig({
1068
+ // ...
1069
+ build: defineBuildConfig({
1070
+ include: [
1071
+ "src/**/*",
1072
+ "package.json",
1073
+ "pnpm-lock.yaml",
1074
+ "tsconfig.json",
1075
+ "zenbu.config.ts",
1076
+ "vite.config.ts",
1077
+ ],
1078
+ ignore: ["src/**/*.test.ts", "src/**/*.spec.ts"],
1079
+ mirror: {
1080
+ target: "your-org/your-app",
1081
+ branch: "main",
1082
+ },
1083
+ }),
1084
+ });
1085
+ ```
1086
+
1087
+ ### include and ignore
1088
+
1089
+ `include` is an array of glob patterns that determines which files from your project end up in the staged source. Everything else is excluded. Use `ignore` to filter out files that match an include pattern but shouldn't ship, like tests or dev-only code.
1090
+
1091
+ ### Transforms
1092
+
1093
+ Build plugins let you transform files during staging or emit new files. Each plugin receives every file and can modify its contents, drop it entirely, or leave it unchanged.
1094
+
1095
+ ```typescript theme={null}
1096
+ import { defineBuildConfig } from "@zenbujs/core/config";
1097
+ import type { BuildPlugin } from "@zenbujs/core/config";
1098
+
1099
+ const injectLicense: BuildPlugin = {
1100
+ name: "inject-license",
1101
+ done(ctx) {
1102
+ ctx.emit("LICENSE", "MIT License\n...");
1103
+ },
1104
+ };
1105
+
1106
+ export default defineBuildConfig({
1107
+ // ...
1108
+ plugins: [injectLicense],
1109
+ });
1110
+ ```
1111
+
1112
+ A build plugin can define a `transform(path, contents)` function that runs on each file. Returning a string replaces the file's contents, returning `null` drops the file, and returning nothing leaves it as-is. The `done` function runs after all files are processed and can emit additional files with `ctx.emit()`.
1113
+
1114
+ ## Git repository
1115
+
1116
+ Any git remote works as the source repository, for example GitHub, GitLab, or a self-hosted server.
1117
+
1118
+ When the user launches the app for the first time, the Electron app clones this repository into `~/.zenbu/<app-name>/`. On subsequent launches, it fetches the latest commits and checks out the most recent compatible version. This is also how updates are delivered: push new source to the mirror, and users pick it up on their next launch.
1119
+
1120
+ The staged source is published to the mirror with:
1121
+
1122
+ ```bash theme={null}
1123
+ # First time
1124
+ pnpm run publish:source init
1125
+
1126
+ # Subsequent pushes
1127
+ pnpm run publish:source push
1128
+ ```
1129
+
1130
+ ## Host version
1131
+
1132
+ The Electron app's version number comes from `package.json#version`. `zen build:electron` reads it at build time and bakes it into the `.app` so subsequent `git pull`s of the source can't change it. Bump `package.json#version` whenever you ship a new `.app`.
1133
+
1134
+ The source declares which Electron app versions it's compatible with through a semver range in the same `package.json`:
1135
+
1136
+ ```json package.json theme={null}
1137
+ {
1138
+ "version": "0.0.6",
1139
+ "zenbu": {
1140
+ "host": ">=0.0.0 <0.1.0"
1141
+ }
1142
+ }
1143
+ ```
1144
+
1145
+ When the launcher fetches new source, it checks whether the latest commit is compatible with the installed Electron app. If it is, it checks out that commit. If not, it searches backwards through the git history to find the most recent compatible commit. This means you can push source that requires a newer Electron app without breaking users who haven't updated yet.
1146
+
1147
+ ## Package manager
1148
+
1149
+ The Electron app bundles a package manager that runs `install` on first launch and whenever the lockfile changes. By default it bundles pnpm, but you can change it in the build config:
1150
+
1151
+ ```typescript theme={null}
1152
+ defineBuildConfig({
1153
+ // ...
1154
+ packageManager: { type: "bun", version: "1.3.12" },
1155
+ });
1156
+ ```
1157
+
1158
+ Supported options are `pnpm`, `npm`, `yarn`, and `bun`. The specified version is downloaded and cached during `zen build:electron`, then packaged into the `.app` bundle so the user's machine doesn't need a package manager installed.
1159
+
1160
+ ## Installing screen
1161
+
1162
+ The first launch requires cloning the source and installing dependencies, which can take a few seconds. You can provide an `installing.html` file that the launcher shows during this process.
1163
+
1164
+ Drop it next to your renderer entry:
1165
+
1166
+ ```
1167
+ src/renderer/
1168
+ ├─ index.html
1169
+ ├─ main.tsx
1170
+ └─ installing.html
1171
+ ```
1172
+
1173
+ `zen build:electron` picks it up automatically. The page receives progress events through a small API exposed on `window.zenbuInstall`:
1174
+
1175
+ ```html installing.html theme={null}
1176
+ <!doctype html>
1177
+ <html>
1178
+ <head>
1179
+ <style>
1180
+ body {
1181
+ background: #111;
1182
+ color: #e5e5e5;
1183
+ font-family: system-ui, sans-serif;
1184
+ }
1185
+ .progress {
1186
+ width: 280px;
1187
+ height: 4px;
1188
+ background: #222;
1189
+ border-radius: 2px;
1190
+ overflow: hidden;
1191
+ }
1192
+ .bar {
1193
+ background: #6a59ff;
1194
+ height: 100%;
1195
+ transition: width 200ms;
1196
+ }
1197
+ </style>
1198
+ </head>
1199
+ <body>
1200
+ <div id="step">Starting...</div>
1201
+ <div class="progress"><div class="bar" id="bar"></div></div>
1202
+ <script>
1203
+ window.zenbuInstall.on("step", ({ label }) => {
1204
+ document.getElementById("step").textContent = label;
1205
+ });
1206
+ window.zenbuInstall.on("progress", ({ ratio }) => {
1207
+ if (ratio != null)
1208
+ document.getElementById("bar").style.width = `${ratio * 100}%`;
1209
+ });
1210
+ window.zenbuInstall.on("error", ({ message }) => {
1211
+ document.getElementById("step").textContent = message;
1212
+ });
1213
+ </script>
1214
+ </body>
1215
+ </html>
1216
+ ```
1217
+
1218
+ The available events are:
1219
+
1220
+ | Event | Payload |
1221
+ | ---------- | --------------------------------------------------------------------- |
1222
+ | `step` | `{ id: "clone" \| "fetch" \| "install" \| "handoff", label: string }` |
1223
+ | `message` | `{ text: string }` |
1224
+ | `progress` | `{ phase?: string, loaded?: number, total?: number, ratio?: number }` |
1225
+ | `done` | `{ id: string }` |
1226
+ | `error` | `{ id?: string, message: string }` |
1227
+
1228
+ The window closes automatically once the app is ready. This screen only runs in production. It never appears during development.
1229
+
1230
+ ## Building
1231
+
1232
+ There are two build steps:
1233
+
1234
+ ```bash theme={null}
1235
+ # 1. Stage source files (apply include/ignore/transforms)
1236
+ pnpm run build:source
1237
+
1238
+ # 2. Build the Electron binary
1239
+ pnpm run build:electron
1240
+ ```
1241
+
1242
+ `build:source` walks your project, applies the include and ignore patterns, runs any transform plugins, and writes the staged output. `build:electron` bundles the launcher, the toolchain, and the installing screen into an Electron `.app` using electron-builder.
1243
+
1244
+ You can pass flags through to electron-builder:
1245
+
1246
+ ```bash theme={null}
1247
+ pnpm run build:electron -- --mac dmg
1248
+ pnpm run build:electron -- --publish always
1249
+ ```
1250
+
1251
+
1252
+ # Introduction
1253
+ Source: https://zenbulabs.mintlify.app/introduction
1254
+
1255
+
1256
+
1257
+ Zenbu.js is a framework for building hackable desktop apps.
1258
+
1259
+ Apps built with Zenbu.js are designed to be modified after they ship. Users can edit the source code directly, or install plugins that extend the app with new functionality.
1260
+
1261
+ The framework also handles the hard parts of desktop development for you, like syncing state between processes, RPC, and hot-reloading everything as you make changes.
1262
+
1263
+ ### Why build a hackable app
1264
+
1265
+ * Coding agents can generate and customize software on demand for a specific use case. A hackable app gives them full access to do that.
1266
+ * Letting people modify your app means more directions get explored than you could reach on your own.
1267
+ * Extensible code tends to be more maintainable, because it's already written to be changed.
1268
+
1269
+ ## Quick links
1270
+
1271
+ <CardGroup>
1272
+ <Card title="Quickstart" icon="rocket" href="/quickstart">
1273
+ Create your first Zenbu.js app in under a minute.
1274
+ </Card>
1275
+
1276
+ <Card title="Concepts" icon="book" href="/concepts">
1277
+ Understand the plugin model, state, and RPC.
1278
+ </Card>
1279
+ </CardGroup>
1280
+
1281
+
1282
+ # Quickstart
1283
+ Source: https://zenbulabs.mintlify.app/quickstart
1284
+
1285
+
1286
+
1287
+ <Steps>
1288
+ <Step title="Scaffold the project">
1289
+ <Tabs>
1290
+ <Tab title="pnpm">
1291
+ ```bash theme={null}
1292
+ pnpx create-zenbu-app my-app
1293
+ ```
1294
+ </Tab>
1295
+
1296
+ <Tab title="npm">
1297
+ ```bash theme={null}
1298
+ npx create-zenbu-app my-app
1299
+ ```
1300
+ </Tab>
1301
+
1302
+ <Tab title="yarn">
1303
+ ```bash theme={null}
1304
+ yarn dlx create-zenbu-app my-app
1305
+ ```
1306
+ </Tab>
1307
+
1308
+ <Tab title="bun">
1309
+ ```bash theme={null}
1310
+ bunx create-zenbu-app my-app
1311
+ ```
1312
+ </Tab>
1313
+ </Tabs>
1314
+
1315
+ This creates a new directory with everything you need to get started.
1316
+ </Step>
1317
+
1318
+ <Step title="Install dependencies">
1319
+ <Tabs>
1320
+ <Tab title="pnpm">
1321
+ ```bash theme={null}
1322
+ cd my-app && pnpm install
1323
+ ```
1324
+ </Tab>
1325
+
1326
+ <Tab title="npm">
1327
+ ```bash theme={null}
1328
+ cd my-app && npm install
1329
+ ```
1330
+ </Tab>
1331
+
1332
+ <Tab title="yarn">
1333
+ ```bash theme={null}
1334
+ cd my-app && yarn
1335
+ ```
1336
+ </Tab>
1337
+
1338
+ <Tab title="bun">
1339
+ ```bash theme={null}
1340
+ cd my-app && bun install
1341
+ ```
1342
+ </Tab>
1343
+ </Tabs>
1344
+ </Step>
1345
+
1346
+ <Step title="Start the app">
1347
+ <Tabs>
1348
+ <Tab title="pnpm">
1349
+ ```bash theme={null}
1350
+ pnpm run dev
1351
+ ```
1352
+ </Tab>
1353
+
1354
+ <Tab title="npm">
1355
+ ```bash theme={null}
1356
+ npm run dev
1357
+ ```
1358
+ </Tab>
1359
+
1360
+ <Tab title="yarn">
1361
+ ```bash theme={null}
1362
+ yarn dev
1363
+ ```
1364
+ </Tab>
1365
+
1366
+ <Tab title="bun">
1367
+ ```bash theme={null}
1368
+ bun run dev
1369
+ ```
1370
+ </Tab>
1371
+ </Tabs>
1372
+
1373
+ Your app will open in a new window, and any changes you make will hot-reload.
1374
+ </Step>
1375
+ </Steps>
1376
+
1377
+ ## Project structure
1378
+
1379
+ After scaffolding, your project looks like this:
1380
+
1381
+ ```
1382
+ my-app/
1383
+ ├── zenbu.config.ts # The only required config file
1384
+ ├── package.json
1385
+ ├── vite.config.ts
1386
+ ├── tsconfig.json
1387
+ └── src/
1388
+ ├── main/
1389
+ │ ├── schema.ts # Zod schema for the database
1390
+ │ └── services/
1391
+ │ └── app.ts # Opens the main window on boot
1392
+ └── renderer/
1393
+ ├── index.html
1394
+ ├── main.tsx
1395
+ └── App.tsx
1396
+ ```
1397
+
1398
+ The `zenbu.config.ts` file is the entry point. It tells Zenbu.js where everything in your project is.
1399
+
1400
+ ```typescript zenbu.config.ts theme={null}
1401
+ import { defineConfig, definePlugin } from "@zenbujs/core/config"
1402
+
1403
+ export default defineConfig({
1404
+ db: "./.zenbu/db",
1405
+ uiEntrypoint: "./src/renderer",
1406
+ plugins: [
1407
+ definePlugin({
1408
+ name: "app",
1409
+ services: ["./src/main/services/*.ts"],
1410
+ schema: "./src/main/schema.ts",
1411
+ }),
1412
+ ],
1413
+ })
1414
+ ```
1415
+
1416
+ ## Available scripts
1417
+
1418
+ | Script | What it does |
1419
+ | ------------------------- | ------------------------------------------------------- |
1420
+ | `pnpm run dev` | Run the app locally with hot reloading. |
1421
+ | `pnpm run link` | Regenerate types after changing your project structure. |
1422
+ | `pnpm run db:generate` | Create a migration after changing your database schema. |
1423
+ | `pnpm run build:source` | Stage the source tree for publishing. |
1424
+ | `pnpm run build:electron` | Produce a signed `.app` / installer. |
1425
+
1426
+ ## Next steps
1427
+
1428
+ <CardGroup>
1429
+ <Card title="Concepts" icon="book" href="/concepts">
1430
+ Learn the plugin model, state, and RPC.
1431
+ </Card>
1432
+ </CardGroup>
1433
+
1434
+