everything-dev 1.42.1 → 1.42.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.
- package/dist/cli/prompts.cjs +2 -1
- package/dist/cli/prompts.cjs.map +1 -1
- package/dist/cli/prompts.mjs +2 -1
- package/dist/cli/prompts.mjs.map +1 -1
- package/dist/plugin.cjs +1 -0
- package/dist/plugin.cjs.map +1 -1
- package/dist/plugin.mjs +1 -0
- package/dist/plugin.mjs.map +1 -1
- package/package.json +1 -1
- package/skills/api-and-auth/SKILL.md +355 -0
- package/skills/api-and-auth/references/generated-types.md +14 -0
- package/skills/api-and-auth/references/middleware.md +38 -0
- package/skills/cli-reference/SKILL.md +249 -0
- package/skills/plugin-development/SKILL.md +478 -0
- package/skills/ui-integration/SKILL.md +383 -0
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plugin-development
|
|
3
|
+
description: Build, register, and deploy plugins within everything.dev. Covers the _template scaffold, contract/service/index pattern, database setup with Drizzle, bos.config.json registration, plugin UI/sidebar, and CLI workflow. Use when creating new plugins, adding database-backed routes, or deploying plugins to production.
|
|
4
|
+
metadata:
|
|
5
|
+
sources: "plugins/_template/src/index.ts,plugins/_template/src/contract.ts,plugins/_template/src/service.ts,plugins/_template/rspack.config.js,plugins/_template/plugin.dev.ts,plugins/projects/src/db/schema.ts,plugins/projects/src/db/layer.ts,api/src/db/index.ts,api/src/db/migrator.ts,packages/every-plugin/src/plugin.ts"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Plugin Development
|
|
9
|
+
|
|
10
|
+
## Plugin Structure
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
plugins/your-plugin/
|
|
14
|
+
├── src/
|
|
15
|
+
│ ├── contract.ts # oRPC route definitions + Zod schemas
|
|
16
|
+
│ ├── service.ts # Business logic (class or plain functions)
|
|
17
|
+
│ ├── index.ts # createPlugin() wiring
|
|
18
|
+
│ ├── db/ # Optional: database schema + migrations
|
|
19
|
+
│ │ ├── schema.ts
|
|
20
|
+
│ │ ├── layer.ts
|
|
21
|
+
│ │ ├── migrator.ts
|
|
22
|
+
│ │ └── migrations/
|
|
23
|
+
│ ├── plugins-client.gen.ts # Generated — in-process plugin client types
|
|
24
|
+
│ └── __tests__/ # Tests
|
|
25
|
+
├── package.json
|
|
26
|
+
├── rspack.config.js # Build config
|
|
27
|
+
├── plugin.dev.ts # Dev server (port, variables, secrets)
|
|
28
|
+
└── tsconfig.json
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Step 1: Copy from `_template`
|
|
32
|
+
|
|
33
|
+
The quickest start is to copy the template:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
cp -r plugins/_template plugins/your-plugin
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Then rename in `package.json`, `plugin.dev.ts`, and `rspack.config.js`.
|
|
40
|
+
|
|
41
|
+
Or use the CLI (no automated command yet — copy template manually).
|
|
42
|
+
|
|
43
|
+
## Step 2: Define the Contract (`src/contract.ts`)
|
|
44
|
+
|
|
45
|
+
Use `oc.router()` to define typed routes. Each route has a method, path, optional input/output schemas, and optional error declarations:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { eventIterator, oc } from "every-plugin/orpc";
|
|
49
|
+
import { z } from "every-plugin/zod";
|
|
50
|
+
|
|
51
|
+
// Define errors this plugin throws
|
|
52
|
+
const Errors = {
|
|
53
|
+
NOT_FOUND: { status: 404, message: "Item not found" },
|
|
54
|
+
UNAUTHORIZED: { status: 401, message: "User ID required" },
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const ItemSchema = z.object({
|
|
58
|
+
id: z.string(),
|
|
59
|
+
title: z.string(),
|
|
60
|
+
createdAt: z.string().datetime(),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export const contract = oc.router({
|
|
64
|
+
getById: oc
|
|
65
|
+
.route({ method: "GET", path: "/items/{id}" })
|
|
66
|
+
.input(z.object({ id: z.string().min(1) }))
|
|
67
|
+
.output(z.object({ item: ItemSchema, userId: z.string() }))
|
|
68
|
+
.errors(Errors),
|
|
69
|
+
|
|
70
|
+
ping: oc
|
|
71
|
+
.route({ method: "GET", path: "/ping" })
|
|
72
|
+
.output(z.object({ status: z.literal("ok"), timestamp: z.string().datetime() })),
|
|
73
|
+
|
|
74
|
+
search: oc
|
|
75
|
+
.route({ method: "GET", path: "/search" })
|
|
76
|
+
.input(z.object({ query: z.string(), limit: z.number().min(1).max(100).default(10) }))
|
|
77
|
+
.output(eventIterator(SearchResultSchema)), // SSE streaming
|
|
78
|
+
|
|
79
|
+
enqueueBackground: oc
|
|
80
|
+
.route({ method: "POST", path: "/background/events" })
|
|
81
|
+
.input(z.object({ id: z.string().optional() }))
|
|
82
|
+
.output(z.object({ ok: z.boolean() })),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export type ContractType = typeof contract;
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Key rules:
|
|
89
|
+
- Import from `every-plugin/zod`, `every-plugin/orpc`, `every-plugin/errors` — never directly from `zod` or `@orpc/*`
|
|
90
|
+
- Path params (`{id}`) automatically bind to Zod input keys
|
|
91
|
+
- `.output(eventIterator(Schema))` enables SSE streaming routes
|
|
92
|
+
- `.errors(Errors)` enables typed error handling in generated clients
|
|
93
|
+
|
|
94
|
+
## Step 3: Build Services (`src/service.ts`)
|
|
95
|
+
|
|
96
|
+
Services are plain classes or functions, optionally using Effect:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { Effect } from "every-plugin/effect";
|
|
100
|
+
|
|
101
|
+
export class TemplateService {
|
|
102
|
+
constructor(
|
|
103
|
+
private baseUrl: URL,
|
|
104
|
+
private apiKey: string,
|
|
105
|
+
private timeout: number,
|
|
106
|
+
) {}
|
|
107
|
+
|
|
108
|
+
ping(): Effect.Effect<{ status: string; timestamp: string }> {
|
|
109
|
+
return Effect.succeed({ status: "ok", timestamp: new Date().toISOString() });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
getById(id: string): Effect.Effect<{ id: string; title: string; createdAt: string }> {
|
|
113
|
+
return Effect.tryPromise(async () => {
|
|
114
|
+
const res = await fetch(`${this.baseUrl}/items/${id}`, {
|
|
115
|
+
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
116
|
+
signal: AbortSignal.timeout(this.timeout),
|
|
117
|
+
});
|
|
118
|
+
if (!res.ok) throw new Error("Item not found");
|
|
119
|
+
return res.json();
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Use `Effect.runPromise(service.method())` to bridge Effect and async handlers in `createRouter`.
|
|
126
|
+
|
|
127
|
+
## Step 4: Wire with `createPlugin` (`src/index.ts`)
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
import { createPlugin } from "every-plugin";
|
|
131
|
+
import { Effect } from "every-plugin/effect";
|
|
132
|
+
import { MemoryPublisher, ORPCError } from "every-plugin/orpc";
|
|
133
|
+
import { z } from "every-plugin/zod";
|
|
134
|
+
import { contract } from "./contract";
|
|
135
|
+
import type { PluginsClient } from "./plugins-client.gen";
|
|
136
|
+
import { TemplateService } from "./service";
|
|
137
|
+
|
|
138
|
+
export default createPlugin.withPlugins<PluginsClient>()({
|
|
139
|
+
variables: z.object({
|
|
140
|
+
baseUrl: z.url().default("https://api.example.com"),
|
|
141
|
+
timeout: z.number().min(1000).max(60000).default(10000),
|
|
142
|
+
}),
|
|
143
|
+
|
|
144
|
+
secrets: z.object({
|
|
145
|
+
apiKey: z.string().min(1).default("dev-key"),
|
|
146
|
+
}),
|
|
147
|
+
|
|
148
|
+
context: z.object({
|
|
149
|
+
userId: z.string().optional(),
|
|
150
|
+
user: z.object({
|
|
151
|
+
id: z.string(),
|
|
152
|
+
role: z.string().optional(),
|
|
153
|
+
email: z.string().optional(),
|
|
154
|
+
name: z.string().optional(),
|
|
155
|
+
}).optional(),
|
|
156
|
+
reqHeaders: z.custom<Headers>().optional(),
|
|
157
|
+
getRawBody: z.custom<() => Promise<string>>().optional(),
|
|
158
|
+
}),
|
|
159
|
+
|
|
160
|
+
contract,
|
|
161
|
+
|
|
162
|
+
initialize: (config) =>
|
|
163
|
+
Effect.gen(function* () {
|
|
164
|
+
const service = new TemplateService(config.variables.baseUrl, config.secrets.apiKey, config.variables.timeout);
|
|
165
|
+
yield* service.ping(); // health check on startup
|
|
166
|
+
|
|
167
|
+
const publisher = new MemoryPublisher({ resumeRetentionSeconds: 120 });
|
|
168
|
+
|
|
169
|
+
return { service, publisher };
|
|
170
|
+
}),
|
|
171
|
+
|
|
172
|
+
shutdown: () => Effect.void,
|
|
173
|
+
|
|
174
|
+
createRouter: (context, builder) => {
|
|
175
|
+
const { service, publisher } = context;
|
|
176
|
+
const requireAuth = builder.middleware(async ({ context, next }) => {
|
|
177
|
+
if (!context.userId) throw new ORPCError("UNAUTHORIZED", { message: "User ID required" });
|
|
178
|
+
return next({ context: { ...context, userId: context.userId } });
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
getById: builder.getById.use(requireAuth).handler(async ({ input, context }) => {
|
|
183
|
+
const item = await Effect.runPromise(service.getById(input.id));
|
|
184
|
+
return { item, userId: context.userId };
|
|
185
|
+
}),
|
|
186
|
+
|
|
187
|
+
ping: builder.ping.handler(async () => {
|
|
188
|
+
return await Effect.runPromise(service.ping());
|
|
189
|
+
}),
|
|
190
|
+
|
|
191
|
+
search: builder.search.handler(async function* ({ input }) {
|
|
192
|
+
const generator = await Effect.runPromise(service.search(input.query, input.limit));
|
|
193
|
+
for await (const result of generator) yield result;
|
|
194
|
+
}),
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Thing Provider Pattern
|
|
201
|
+
|
|
202
|
+
When the API owns the registry but the plugin owns the payload, add a small provider surface to your contract:
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
createThing: oc
|
|
206
|
+
.route({ method: "POST", path: "/things" })
|
|
207
|
+
.input(z.object({ thingId: z.string(), payload: z.unknown() }))
|
|
208
|
+
.output(z.object({ type: z.string(), payload: z.unknown(), action: z.string().optional() })),
|
|
209
|
+
|
|
210
|
+
getThing: oc
|
|
211
|
+
.route({ method: "GET", path: "/things/{thingId}" })
|
|
212
|
+
.input(z.object({ thingId: z.string() }))
|
|
213
|
+
.output(z.object({ type: z.string(), payload: z.unknown() })),
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
In `service.ts`, keep the plugin-specific type derivation close to the data source:
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
createThing(thingId: string, payload: unknown) {
|
|
220
|
+
return Effect.sync(() => {
|
|
221
|
+
const type = this.resolveThingType(payload);
|
|
222
|
+
this.things.set(thingId, { thingId, type, payload, createdAt: now, updatedAt: now });
|
|
223
|
+
return { type, payload, action: `${type}.created` };
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Good practice:
|
|
229
|
+
- Let the API store only `thingId`, `pluginId`, and timestamps.
|
|
230
|
+
- Let the plugin decide the public `type` string and payload shape.
|
|
231
|
+
- Use `pluginId` as the API routing key, not as a UI concern.
|
|
232
|
+
- Keep `plugins-client.gen.ts` local to the plugin scaffold so `createPlugin.withPlugins<PluginsClient>()` stays typed.
|
|
233
|
+
|
|
234
|
+
### Key Patterns
|
|
235
|
+
|
|
236
|
+
**`variables`** — Public config exposed in `bos.config.json` (typed, with defaults).
|
|
237
|
+
**`secrets`** — Private values from `process.env` (typed, dev defaults).
|
|
238
|
+
**`context`** — Per-request context injected by the host. See "Request Context Reference" below for all available fields.
|
|
239
|
+
**`initialize`** — Effect-based startup. Create services, publishers, DB connections. Return value is passed as `context` to `createRouter`.
|
|
240
|
+
**`createRouter`** — Maps contract procedures to handlers. Receives the value returned by `initialize` plus a pre-configured `builder`.
|
|
241
|
+
|
|
242
|
+
## Request Context Reference
|
|
243
|
+
|
|
244
|
+
The host injects a per-request context object into every plugin. The plugin **must declare the fields it uses** in its context zod schema. Only declared fields are available to route handlers and middleware:
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
context: z.object({
|
|
248
|
+
userId: z.string().optional(),
|
|
249
|
+
user: z.object({
|
|
250
|
+
id: z.string(),
|
|
251
|
+
role: z.string().optional(), // "admin", "member", or null for anon
|
|
252
|
+
email: z.string().optional(),
|
|
253
|
+
name: z.string().optional(),
|
|
254
|
+
}).optional(),
|
|
255
|
+
organizationId: z.string().optional(), // Active organization UUID
|
|
256
|
+
organization: z.object({
|
|
257
|
+
activeOrganizationId: z.string().nullable().optional(),
|
|
258
|
+
organization: z.object({
|
|
259
|
+
id: z.string(),
|
|
260
|
+
name: z.string(),
|
|
261
|
+
slug: z.string(),
|
|
262
|
+
logo: z.string().nullable().optional(),
|
|
263
|
+
metadata: z.record(z.string(), z.unknown()).optional(), // daoAccountId lives here
|
|
264
|
+
}).nullable().optional(),
|
|
265
|
+
member: z.object({
|
|
266
|
+
id: z.string(),
|
|
267
|
+
role: z.string(), // User's role within this org ("admin", "member")
|
|
268
|
+
}).nullable().optional(),
|
|
269
|
+
isPersonal: z.boolean(),
|
|
270
|
+
hasOrganization: z.boolean(),
|
|
271
|
+
}).optional(),
|
|
272
|
+
near: z.object({
|
|
273
|
+
primaryAccountId: z.string().nullable(),
|
|
274
|
+
linkedAccounts: z.array(z.object({
|
|
275
|
+
accountId: z.string(),
|
|
276
|
+
network: z.string(),
|
|
277
|
+
publicKey: z.string(),
|
|
278
|
+
isPrimary: z.boolean(),
|
|
279
|
+
})),
|
|
280
|
+
hasNearAccount: z.boolean(),
|
|
281
|
+
}).optional(),
|
|
282
|
+
walletAddress: z.string().optional(),
|
|
283
|
+
apiKey: z.object({
|
|
284
|
+
id: z.string(),
|
|
285
|
+
name: z.string().nullable().optional(),
|
|
286
|
+
permissions: z.record(z.string(), z.array(z.string())).nullable().optional(),
|
|
287
|
+
}).optional(),
|
|
288
|
+
reqHeaders: z.custom<Headers>().optional(),
|
|
289
|
+
getRawBody: z.custom<() => Promise<string>>().optional(),
|
|
290
|
+
}),
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
The zod schema is a filter — the host passes all fields, but your plugin only sees what you declare. For pre-built auth/organization middleware, see the `api-and-auth` skill.
|
|
294
|
+
|
|
295
|
+
## Step 5: Register in `bos.config.json`
|
|
296
|
+
|
|
297
|
+
Add a plugin entry:
|
|
298
|
+
|
|
299
|
+
```json
|
|
300
|
+
{
|
|
301
|
+
"plugins": {
|
|
302
|
+
"your-plugin": {
|
|
303
|
+
"name": "your-plugin",
|
|
304
|
+
"development": "local:plugins/your-plugin",
|
|
305
|
+
"production": "https://cdn.example.com/your-plugin/remoteEntry.js",
|
|
306
|
+
"ssr": "https://cdn.example.com/your-plugin/ssr/remoteEntry.js",
|
|
307
|
+
"variables": {
|
|
308
|
+
"baseUrl": "https://api.example.com",
|
|
309
|
+
"timeout": 5000
|
|
310
|
+
},
|
|
311
|
+
"secrets": ["YOURPLUGIN_API_KEY"],
|
|
312
|
+
"sidebar": [
|
|
313
|
+
{
|
|
314
|
+
"label": "Your Plugin",
|
|
315
|
+
"icon": "Activity",
|
|
316
|
+
"to": "/your-plugin",
|
|
317
|
+
"roleRequired": "member"
|
|
318
|
+
}
|
|
319
|
+
],
|
|
320
|
+
"routes": ["/your-plugin/*"],
|
|
321
|
+
"shared": {
|
|
322
|
+
"react": { "version": "^19.0.0", "singleton": true },
|
|
323
|
+
"react-dom": { "version": "^19.0.0", "singleton": true }
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
The CLI updates `bos.config.json` automatically when you run `bos plugin add` / `bos plugin remove`.
|
|
331
|
+
|
|
332
|
+
## Step 6: `plugin.dev.ts`
|
|
333
|
+
|
|
334
|
+
Dev server config for local development:
|
|
335
|
+
|
|
336
|
+
```ts
|
|
337
|
+
import { defineConfig } from "every-plugin/dev";
|
|
338
|
+
|
|
339
|
+
export default defineConfig({
|
|
340
|
+
port: 3015,
|
|
341
|
+
variables: {
|
|
342
|
+
baseUrl: "https://api.example.com",
|
|
343
|
+
timeout: 10000,
|
|
344
|
+
},
|
|
345
|
+
secrets: {
|
|
346
|
+
apiKey: "dev-key",
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
## Step 7: Build Config (`rspack.config.js`)
|
|
352
|
+
|
|
353
|
+
```js
|
|
354
|
+
const { createRspackConfig } = require("every-plugin/rspack");
|
|
355
|
+
|
|
356
|
+
module.exports = createRspackConfig({
|
|
357
|
+
name: "your-plugin",
|
|
358
|
+
exposes: {
|
|
359
|
+
".": "./src/index.ts",
|
|
360
|
+
"./contract": "./src/contract.ts",
|
|
361
|
+
},
|
|
362
|
+
plugins: [],
|
|
363
|
+
});
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Key exports:
|
|
367
|
+
- `"."` — the plugin entry point (`createPlugin` export)
|
|
368
|
+
- `"./contract"` — the contract module (for type generation from remote plugins)
|
|
369
|
+
|
|
370
|
+
## Database Setup
|
|
371
|
+
|
|
372
|
+
### Schema (`src/db/schema.ts`)
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
import { pgTable, text, timestamp, uniqueIndex } from "drizzle-orm/pg-core";
|
|
376
|
+
|
|
377
|
+
export const items = pgTable("items", {
|
|
378
|
+
id: text("id").primaryKey(),
|
|
379
|
+
title: text("title").notNull(),
|
|
380
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
381
|
+
}, (table) => ({
|
|
382
|
+
titleIdx: uniqueIndex("items_title_idx").on(table.title),
|
|
383
|
+
}));
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### Driver (`src/db/index.ts` or use `api/src/db/index.ts`)
|
|
387
|
+
|
|
388
|
+
```ts
|
|
389
|
+
export async function createDatabaseDriver(url: string) {
|
|
390
|
+
// Supports:
|
|
391
|
+
// "pglite:.bos/your-plugin/:memory:" — in-memory PGlite (dev)
|
|
392
|
+
// "pglite:.bos/your-plugin/data" — persistent PGlite (dev)
|
|
393
|
+
// "<postgres connection string>" — real PostgreSQL (prod)
|
|
394
|
+
// Returns { db: drizzle client, close(): Promise<void> }
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Layer Pattern
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
// src/db/layer.ts
|
|
402
|
+
import { Effect, Layer } from "every-plugin/effect";
|
|
403
|
+
import { createDatabaseDriver } from "./index";
|
|
404
|
+
|
|
405
|
+
export class DatabaseTag extends Context.Tag("plugins/your-plugin/Database")<
|
|
406
|
+
DatabaseTag,
|
|
407
|
+
{ db: any; close: () => Promise<void> }
|
|
408
|
+
>() {}
|
|
409
|
+
|
|
410
|
+
export function DatabaseLive(url: string) {
|
|
411
|
+
return Layer.scoped(
|
|
412
|
+
DatabaseTag,
|
|
413
|
+
Effect.acquireRelease(
|
|
414
|
+
Effect.promise(async () => {
|
|
415
|
+
const driver = await createDatabaseDriver(url);
|
|
416
|
+
return driver;
|
|
417
|
+
}),
|
|
418
|
+
(driver) => Effect.promise(() => driver.close()),
|
|
419
|
+
),
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### Migration Generation
|
|
425
|
+
|
|
426
|
+
1. Create `drizzle.config.ts` in your plugin directory
|
|
427
|
+
2. Run `drizzle-kit generate` to produce SQL migration files
|
|
428
|
+
3. Store migrations in `src/db/migrations/`
|
|
429
|
+
4. Use the custom `migrate()` function during `initialize`:
|
|
430
|
+
|
|
431
|
+
```ts
|
|
432
|
+
initialize: (config) =>
|
|
433
|
+
Effect.gen(function* () {
|
|
434
|
+
const driver = yield* DatabaseLive(config.secrets.DATABASE_URL);
|
|
435
|
+
// Or load and apply migrations directly:
|
|
436
|
+
const { loadMigrations } = yield* Effect.promise(() => import("./db/load-migrations"));
|
|
437
|
+
const { migrate } = yield* Effect.promise(() => import("./db/migrator"));
|
|
438
|
+
const migrations = yield* loadMigrations();
|
|
439
|
+
yield* migrate(driver.db, migrations);
|
|
440
|
+
|
|
441
|
+
return { db: driver.db, ... };
|
|
442
|
+
}),
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### Per-Plugin Isolation
|
|
446
|
+
|
|
447
|
+
Each plugin manages its own database or schema. Connection strings are passed via secrets (`YOURPLUGIN_DATABASE_URL`). The convention:
|
|
448
|
+
|
|
449
|
+
```
|
|
450
|
+
YOURPLUGIN_DATABASE_URL=pglite:.bos/your-plugin/:memory:
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
In production, set this to a real PostgreSQL connection string in the host's environment.
|
|
454
|
+
|
|
455
|
+
## Calling Other Plugins
|
|
456
|
+
|
|
457
|
+
Use `createPlugin.withPlugins<PluginsClient>()` to get typed access to other plugin factories. The `plugins-client.gen.ts` file provides the `PluginsClient` type. Regenerate with `bos types gen`.
|
|
458
|
+
|
|
459
|
+
## Plugin UI
|
|
460
|
+
|
|
461
|
+
Add a UI remote via the `ui` field in `bos.config.json`. Sidebar entries use `icon` from `lucide-react` and `roleRequired` from `anon`, `member`, `admin`. These merge into `plugin-sidebar.gen.ts` at `ui/src/lib/`.
|
|
462
|
+
|
|
463
|
+
## Deploy
|
|
464
|
+
|
|
465
|
+
```bash
|
|
466
|
+
bos plugin publish # Build and publish just this plugin
|
|
467
|
+
bos publish --deploy # Build ALL packages + deploy + publish config
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Deploy builds via `rspack.config.js`, uploads to Zephyr CDN, updates `bos.config.json` with production URL + integrity hash, and publishes to FastKV registry. Restart the host after publishing.
|
|
471
|
+
|
|
472
|
+
### CLI Lifecycle
|
|
473
|
+
|
|
474
|
+
1. `cp -r plugins/_template plugins/your-plugin` then edit names
|
|
475
|
+
2. Register in `bos.config.json` (or `bos plugin add`)
|
|
476
|
+
3. `bos types gen`
|
|
477
|
+
4. `bos dev` to develop
|
|
478
|
+
5. `bos plugin publish` to deploy
|