@zhama/tego-plugin-sdk 1.36.1 → 1.37.0
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/README.md +50 -3
- package/agent/RULES.md +123 -0
- package/agent/guides/access.md +39 -0
- package/agent/guides/backend.md +76 -0
- package/agent/guides/claw.md +18 -0
- package/agent/guides/desktop-web.md +48 -0
- package/agent/guides/frontend.md +83 -0
- package/agent/guides/host-api-versions.md +34 -0
- package/agent/guides/i18n.md +17 -0
- package/agent/guides/manifest.md +26 -0
- package/agent/guides/migrations.md +17 -0
- package/agent/guides/storage.md +18 -0
- package/agent/guides/testing.md +17 -0
- package/agent/guides/ui.md +22 -0
- package/agent/reference/plugin-sdk-api.md +561 -0
- package/bin/tego-plugin-pack.mjs +1 -1
- package/bin/tego-plugin.mjs +4 -2
- package/dist/chunk-2NTAWYCG.js +1 -0
- package/dist/chunk-2P6UEAPV.cjs +1 -0
- package/dist/chunk-45Y4CR4R.cjs +1 -0
- package/dist/{chunk-THMWRUAT.cjs → chunk-4LKRXDYY.cjs} +13 -12
- package/dist/{chunk-7XHF3YSK.js → chunk-56QOZQTY.js} +1 -1
- package/dist/{chunk-C2OCOPZQ.cjs → chunk-6HNG5OZI.cjs} +1 -1
- package/dist/chunk-DEGQDJF6.js +1 -0
- package/dist/chunk-EIVA5BR6.js +1 -0
- package/dist/chunk-FCYAAOHC.js +20 -0
- package/dist/chunk-HZ4YWRYX.cjs +5 -0
- package/dist/chunk-I5Z3ECC4.js +5 -0
- package/dist/chunk-INCA3GFW.js +1 -0
- package/dist/chunk-IVJECPGG.js +1 -0
- package/dist/chunk-MWMGJJ74.cjs +1 -0
- package/dist/chunk-N4SXZZPY.cjs +1 -0
- package/dist/chunk-SKNPNPJP.cjs +1 -0
- package/dist/dev.cjs +358 -0
- package/dist/dev.d.cts +144 -0
- package/dist/dev.d.ts +144 -0
- package/dist/dev.js +358 -0
- package/dist/doctor.cjs +8 -0
- package/dist/doctor.d.cts +62 -0
- package/dist/doctor.d.ts +62 -0
- package/dist/doctor.js +8 -0
- package/dist/frontend.d.cts +33 -1
- package/dist/frontend.d.ts +33 -1
- package/dist/hono.cjs +1 -0
- package/dist/hono.d.cts +84 -0
- package/dist/hono.d.ts +84 -0
- package/dist/hono.js +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +8 -3
- package/dist/index.d.ts +8 -3
- package/dist/index.js +1 -1
- package/dist/{manifest-types-DQARCQJt.d.cts → manifest-types-CUTNfJTa.d.cts} +1 -1
- package/dist/{manifest-types-DQARCQJt.d.ts → manifest-types-CUTNfJTa.d.ts} +1 -1
- package/dist/manifest.cjs +1 -1
- package/dist/manifest.d.cts +55 -2
- package/dist/manifest.d.ts +55 -2
- package/dist/manifest.js +1 -1
- package/dist/migrations-sync.cjs +1 -0
- package/dist/migrations-sync.d.cts +20 -0
- package/dist/migrations-sync.d.ts +20 -0
- package/dist/migrations-sync.js +1 -0
- package/dist/pack.cjs +1 -1
- package/dist/pack.d.cts +8 -2
- package/dist/pack.d.ts +8 -2
- package/dist/pack.js +1 -1
- package/dist/publish.cjs +1 -1
- package/dist/publish.d.cts +1 -1
- package/dist/publish.d.ts +1 -1
- package/dist/publish.js +1 -1
- package/dist/react.cjs +1 -0
- package/dist/react.d.cts +101 -0
- package/dist/react.d.ts +101 -0
- package/dist/react.js +1 -0
- package/dist/scaffold.cjs +672 -17
- package/dist/scaffold.d.cts +42 -18
- package/dist/scaffold.d.ts +42 -18
- package/dist/scaffold.js +674 -19
- package/dist/server-runtime.cjs +1 -1
- package/dist/server-runtime.d.cts +27 -2
- package/dist/server-runtime.d.ts +27 -2
- package/dist/server-runtime.js +1 -1
- package/dist/testing.cjs +6 -2
- package/dist/testing.d.cts +19 -1
- package/dist/testing.d.ts +19 -1
- package/dist/testing.js +6 -2
- package/dist/verify-FF2RD2MV.cjs +1 -0
- package/dist/verify-W7ZTUCO4.js +1 -0
- package/dist/vite.cjs +1 -1
- package/dist/vite.d.cts +27 -1
- package/dist/vite.d.ts +27 -1
- package/dist/vite.js +1 -1
- package/package.json +37 -3
- package/dist/chunk-3KUOBGJN.js +0 -19
- package/dist/chunk-3WA3M347.cjs +0 -1
- package/dist/chunk-5CU33JVY.js +0 -1
- package/dist/chunk-7K37PVGV.js +0 -5
- package/dist/chunk-BTHXQWHT.js +0 -1
- package/dist/chunk-ROFUQLOZ.cjs +0 -1
- package/dist/chunk-W7C357A6.cjs +0 -5
package/README.md
CHANGED
|
@@ -563,7 +563,10 @@ import { resolve } from 'node:path';
|
|
|
563
563
|
import { tegoPluginFrontend, tegoPluginTailwind } from '@tego/plugin-sdk/vite';
|
|
564
564
|
import tailwindcss from 'tailwindcss';
|
|
565
565
|
|
|
566
|
-
const config = tegoPluginFrontend({
|
|
566
|
+
const config = tegoPluginFrontend({
|
|
567
|
+
entry: 'frontend/src/index.tsx',
|
|
568
|
+
pluginId: 'acme-crm',
|
|
569
|
+
});
|
|
567
570
|
config.css = {
|
|
568
571
|
postcss: {
|
|
569
572
|
plugins: [
|
|
@@ -600,6 +603,45 @@ await packPlugin({ root: process.cwd(), backend: { external: ['sharp'] } });
|
|
|
600
603
|
Without this, changing one line of frontend code costs a pack, an upload, an
|
|
601
604
|
install and a refresh.
|
|
602
605
|
|
|
606
|
+
### Preview without a Tego server: `tego-plugin dev`
|
|
607
|
+
|
|
608
|
+
```bash
|
|
609
|
+
pnpm run dev # scaffolded plugins have "dev": "tego-plugin dev"
|
|
610
|
+
DATABASE_URL=postgres://me:pw@localhost:5432/dev pnpm run dev # with the backend
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
It prints the address (`http://127.0.0.1:5280/p/<id>/`, or the next free port;
|
|
614
|
+
`--port 0` takes any) and a machine-readable `TEGO_DEV_READY {"url":…}` line.
|
|
615
|
+
Everything is served from that one origin:
|
|
616
|
+
|
|
617
|
+
- **Frontend**: a stand-in for the shell's plugin page mounts your entry from
|
|
618
|
+
source through Vite, with your own `vite.config.ts` and Tailwind. Saving
|
|
619
|
+
reloads the page. The context is built as the shell builds it — `basePath`
|
|
620
|
+
`/p/<id>`, history routing, `ctx.api` at `/api/v1/tego-os/plugin-api/<id>/*`,
|
|
621
|
+
the frontend config's defaults — and the top bar switches page, language
|
|
622
|
+
(your locale files, the access UI's strings merged in), light/dark, and the
|
|
623
|
+
user and roles the API calls are made as (`ctx.can` answers for any nav key
|
|
624
|
+
the manifest declares).
|
|
625
|
+
- **Backend** (with `DATABASE_URL`, unless `--no-backend`): built with the
|
|
626
|
+
packer's own settings and started with the environment `serveBackend` reads.
|
|
627
|
+
Requests carry the same signed identity headers the gateway adds. Migrations
|
|
628
|
+
run first, each file in its own transaction with `search_path` set to
|
|
629
|
+
`tego_dev_<id>` alone, recorded in a `tego_dev_migrations` table in that
|
|
630
|
+
schema so each file runs once; the backend's connection is pinned to the
|
|
631
|
+
schema too. Saving under `backend/` or `shared/` rebuilds, migrates and
|
|
632
|
+
restarts it. The database is yours to provide; any PostgreSQL works.
|
|
633
|
+
- **Host API stand-in**: `config` (the backend section's defaults), `log`,
|
|
634
|
+
`storage` (files under `.tego-dev/storage/`), `events` and `notifications`
|
|
635
|
+
(printed), and a `directory` that knows the users you previewed as. Anything
|
|
636
|
+
else — mail, knowledge, accounts, delegations, AI, Office, live events —
|
|
637
|
+
answers 501 naming the path; test those on a real server.
|
|
638
|
+
|
|
639
|
+
Without `DATABASE_URL` the page still works and API calls answer 503 saying
|
|
640
|
+
why. `.tego-dev/` holds the generated shell, the backend build and local
|
|
641
|
+
storage; keep it out of git. Python backends run on a server only.
|
|
642
|
+
|
|
643
|
+
### Inside the shell
|
|
644
|
+
|
|
603
645
|
```bash
|
|
604
646
|
pnpm exec tego-plugin-pack --watch # serves the bundle on :5180
|
|
605
647
|
VITE_TEGO_LOCAL_PLUGINS=acme-crm=http://localhost:5180/index.js pnpm -F @tego/ai-studio dev
|
|
@@ -614,7 +656,7 @@ Hot replacement across the SystemJS boundary means unmounting the plugin's React
|
|
|
614
656
|
root and re-registering shared modules, which is a lot of machinery for
|
|
615
657
|
something that would silently desync; a one-second reload is always right.
|
|
616
658
|
|
|
617
|
-
|
|
659
|
+
Against a full local server, the backend runs on the host instead of in a container:
|
|
618
660
|
|
|
619
661
|
```bash
|
|
620
662
|
TEGO_PLUGIN_BACKEND_BASE_URL=http://localhost:18800 pnpm -F @tego/server dev
|
|
@@ -779,7 +821,12 @@ const unregister = ctx.ai?.registerForm({
|
|
|
779
821
|
element: formElement,
|
|
780
822
|
getSnapshot: () => ({
|
|
781
823
|
fields: [
|
|
782
|
-
{
|
|
824
|
+
{
|
|
825
|
+
name: 'quantity',
|
|
826
|
+
label: '数量',
|
|
827
|
+
type: 'number',
|
|
828
|
+
value: state.quantity,
|
|
829
|
+
},
|
|
783
830
|
{ name: 'note', label: '说明', type: 'text', value: state.note },
|
|
784
831
|
],
|
|
785
832
|
instructions: '数量使用件,不推测未提供的数量。',
|
package/agent/RULES.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Tego plugin rules
|
|
2
|
+
|
|
3
|
+
A Tego plugin runs inside the Tego platform: its frontend inside the **web app** and the
|
|
4
|
+
**desktop app** (Tauri), its backend in a platform-managed container, its claw part inside an
|
|
5
|
+
employee's container. These rules are what makes a plugin load, pass upload, and behave the same
|
|
6
|
+
in both shells. Read the guide named in each section before writing that kind of code.
|
|
7
|
+
|
|
8
|
+
Rules with an id are checked by `tego-plugin doctor` (`--json` for tools). A line can opt out with
|
|
9
|
+
`tego-doctor-ignore <RULE-ID>: <reason>` on it or on the line above; give a real reason.
|
|
10
|
+
|
|
11
|
+
## Working loop
|
|
12
|
+
|
|
13
|
+
1. Read the relevant guide (`agent/guides/*.md`) and the code you change.
|
|
14
|
+
2. Make focused changes; reuse the SDK's helpers (`@tego/plugin-sdk/react`, `/hono`, `/testing`)
|
|
15
|
+
instead of writing your own versions.
|
|
16
|
+
3. Run the gates: `tego-plugin doctor`, then lint, type-check, test, build. Fix everything they
|
|
17
|
+
report; never weaken a check or a test to make it pass.
|
|
18
|
+
4. Every new user-visible string goes into all ten locales.
|
|
19
|
+
|
|
20
|
+
## Manifest and packaging — guides/manifest.md, guides/host-api-versions.md
|
|
21
|
+
|
|
22
|
+
- **TP-MAN-001** `tego.plugin.json` is valid: `manifestVersion: 1`, stable `id`, semver `version`
|
|
23
|
+
(never lower it), `scope`, at least one part, non-overlapping part directories.
|
|
24
|
+
- **TP-MAN-002** `hostApiRange` (`^X.Y.Z`) admits every host capability the code uses. Declare the
|
|
25
|
+
lowest version you need, and raise it when you use something newer (table in the guide) — or
|
|
26
|
+
feature-detect it (`if (ctx.download) … else …`) to keep supporting older shells.
|
|
27
|
+
- In-plugin routes have no dotted segment (`/p/x/report.pdf`): the desktop app serves a reloaded
|
|
28
|
+
URL like that as a file, not your page. (Nav paths are already refused by the manifest schema.)
|
|
29
|
+
- **TP-PKG-001** SDK packages are installed as npm aliases (`"@tego/ui": "npm:@zhama/tego-ui@^1.37.0"`)
|
|
30
|
+
or workspace links. On npm, a plain `@tego/*` name belongs to another project.
|
|
31
|
+
- **TP-PKG-002** `package.json` has the four gates: `lint`, `type-check` (tsgo), `test` (vitest),
|
|
32
|
+
`build` (`tego-plugin-pack`).
|
|
33
|
+
- Never import `@tego/shared`, host app code, or anything not published to plugins.
|
|
34
|
+
|
|
35
|
+
## Running in the web app and the desktop app — guides/desktop-web.md
|
|
36
|
+
|
|
37
|
+
The two shells share one frontend but differ in CSP, origin, network proxy and file handling. Code
|
|
38
|
+
that works in a browser tab can fail on desktop, and the reverse.
|
|
39
|
+
|
|
40
|
+
- **TP-DESK-001** No `eval` / `new Function` (desktop CSP: `script-src 'self' 'wasm-unsafe-eval'`),
|
|
41
|
+
including in libraries you add.
|
|
42
|
+
- **TP-DESK-002** Never build addresses from `location.origin` (it is `tauri://localhost` on
|
|
43
|
+
desktop). Use `ctx.hostUrl(path)` for host paths and `ctx.publicPagesUrl` for share links.
|
|
44
|
+
- **TP-DESK-003** Open external links with `ctx.openExternal(url)`, not `window.open`.
|
|
45
|
+
- **TP-DESK-004** Save files with `ctx.download({ path | blob, fileName })`, not `<a download>`.
|
|
46
|
+
- **TP-DESK-005** Do not stream from the frontend: desktop buffers `ctx.api` responses whole and caps
|
|
47
|
+
them at 100 MB. Large files go through storage paths; live updates come from `ctx.events`.
|
|
48
|
+
- **TP-DESK-006** Copy text with `ctx.copyText(text)`.
|
|
49
|
+
- **TP-WEB-001** The frontend calls no third-party URL (the web CSP only allows the platform). The
|
|
50
|
+
backend calls it, with the host declared in `parts.backend.egress`.
|
|
51
|
+
- **TP-WEB-002** No remote or injected scripts; all code ships in the bundle.
|
|
52
|
+
- No cookie-based auth: `ctx.api` carries the credentials in both shells.
|
|
53
|
+
|
|
54
|
+
## Frontend — guides/frontend.md
|
|
55
|
+
|
|
56
|
+
- **TP-UI-004** The frontend has its own error boundary (`PluginErrorBoundary`); the shell's cannot
|
|
57
|
+
see into your React root.
|
|
58
|
+
- Root order: `I18nextProvider` (plugin namespace) → error boundary → `QueryClientProvider` →
|
|
59
|
+
`AccessProvider` (with access) → `HostProvider`; the root element carries the scope class.
|
|
60
|
+
- Route with `ctx.route` / `ctx.navigate` under `ctx.basePath`; the page is not remounted when the
|
|
61
|
+
in-plugin path changes.
|
|
62
|
+
- Call your backend only through `ctx.api` (via `createApiClient`), keeping the error body.
|
|
63
|
+
|
|
64
|
+
## UI and design — guides/ui.md
|
|
65
|
+
|
|
66
|
+
- **TP-UI-001** Colors only through token classes (`bg-card`, `text-muted-foreground`, `bg-primary`…):
|
|
67
|
+
no hex, `rgb()`, palette classes (`bg-blue-500`, `text-white`) or `dark:` overrides.
|
|
68
|
+
- **TP-UI-002** Use `@tego/ui` components; never import `@radix-ui/*` directly.
|
|
69
|
+
- **TP-UI-003** With `tegoPluginTailwind`, the root element (and portalled content, or use
|
|
70
|
+
`tegoPluginScopeRoot`) carries `className="<pluginId>"`, or none of your utilities apply.
|
|
71
|
+
- Status uses text or an icon, not color alone; machine data (ids, numbers, times) uses
|
|
72
|
+
`font-mono tabular-nums`.
|
|
73
|
+
|
|
74
|
+
## i18n — guides/i18n.md
|
|
75
|
+
|
|
76
|
+
- **TP-I18N-001** All ten locales — `zh-CN zh-TW en ja ko de es fr it pt` — in
|
|
77
|
+
`frontend/i18n/<locale>.json`, with exactly the keys of `zh-CN`; every nav `labelKey` exists.
|
|
78
|
+
- **TP-I18N-002** Every value keeps the `{{placeholders}}` of `zh-CN` and is not empty.
|
|
79
|
+
- **TP-I18N-003** Every literal `t('…')` key exists.
|
|
80
|
+
- **TP-I18N-004** Say 数字员工 / Employee through `{{avatarTerm}}`; never 数字分身 / digital avatar.
|
|
81
|
+
- Keys live under one root named after the plugin (`notes.home.title`); don't copy `tegoAccess.*`.
|
|
82
|
+
|
|
83
|
+
## Backend — guides/backend.md, guides/storage.md
|
|
84
|
+
|
|
85
|
+
- **TP-BE-001** The caller is the verified `ctx` that `serveBackend` hands you. Never read the raw
|
|
86
|
+
`x-tego-plugin-*` headers; a user id in a path or body names a target, never the caller.
|
|
87
|
+
- **TP-BE-005** Log through `host.log`, never `console`.
|
|
88
|
+
- Every error becomes JSON (`pluginErrorHandler`): coded 4xx for the caller's mistakes, a generic
|
|
89
|
+
500 that never echoes SQL; host 429s pass through with `Retry-After`.
|
|
90
|
+
- Validate every body and query with zod (`readBody` / `readQuery`).
|
|
91
|
+
- Data lives in your schema only (`host.db`); configuration in `host.config.current()`, never env.
|
|
92
|
+
- Storage: `put` up to 25 MiB; larger files upload from the browser to `uploadPath` and are
|
|
93
|
+
verified with `head`. Download paths are `signedPath` / `managedPath`, opened via `ctx.hostUrl`.
|
|
94
|
+
- Background work in `start()` never acts as a user.
|
|
95
|
+
|
|
96
|
+
## Migrations — guides/migrations.md
|
|
97
|
+
|
|
98
|
+
- **TP-BE-003** `backend/migrations/<ordinal>_<name>.sql`, unique ordinals.
|
|
99
|
+
- **TP-BE-004** Committed migrations are append-only: never edit or delete one; add a new file.
|
|
100
|
+
- Generate them from the drizzle schema (`tego-plugin migrations sync`); hand-write only what
|
|
101
|
+
drizzle cannot express (functions, triggers, RLS).
|
|
102
|
+
|
|
103
|
+
## Access control — guides/access.md
|
|
104
|
+
|
|
105
|
+
- **TP-BE-002** Never decide by host role names (`roles.includes('admin')`); declare permissions in
|
|
106
|
+
the catalog (`hostRoleBindings`) and ask the engine.
|
|
107
|
+
- **TP-ACC-001** With `access.catalog`, every route carries exactly one policy, and a test calls
|
|
108
|
+
`assertRouteCoverage(app)`.
|
|
109
|
+
- **TP-ACC-002** With `access.catalog`, `start()` calls `access.reconcile(host)`.
|
|
110
|
+
- Lists use `session.filter`, single records `policy.resource`; what the frontend hides is
|
|
111
|
+
presentation only.
|
|
112
|
+
|
|
113
|
+
## Claw part — guides/claw.md
|
|
114
|
+
|
|
115
|
+
- **TP-CLAW-002** Each `claw/skills/<name>/SKILL.md` declares `name: <name>`.
|
|
116
|
+
- Tools are listed in `contracts.tools`; the claw config schema lives in `openclaw.plugin.json`,
|
|
117
|
+
never in the manifest.
|
|
118
|
+
|
|
119
|
+
## Tests — guides/testing.md
|
|
120
|
+
|
|
121
|
+
- **TP-TEST-001** The plugin has tests: backend routes against a real database
|
|
122
|
+
(`createTestPluginHost`), pure frontend logic in `.ts` files. No component tests.
|
|
123
|
+
- Each test file starts with a Business Cases list; each `it` title starts with its case id.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Access control
|
|
2
|
+
|
|
3
|
+
Accounts come from the host; what someone may do _inside_ the plugin is declared in your catalog,
|
|
4
|
+
stored in your own tables and decided by the SDK. Design: `docs/design/plugin-access.md` in tego-os.
|
|
5
|
+
|
|
6
|
+
Add it as soon as some users may do something others may not:
|
|
7
|
+
|
|
8
|
+
1. `"access": { "catalog": "shared/access.json" }` in the manifest; `hostApiRange` ≥ `^1.22.0`;
|
|
9
|
+
`directory.read` permission when you use `unit` principals.
|
|
10
|
+
2. Catalog: permissions (with `implies`, and `agent.requires` for what employees may use), scopes,
|
|
11
|
+
built-in roles, `hostRoleBindings` (how host roles map to yours), default role, `menus`
|
|
12
|
+
(nav key → permissions).
|
|
13
|
+
3. Backend:
|
|
14
|
+
```ts
|
|
15
|
+
export const access = createAccess({
|
|
16
|
+
catalog: accessCatalogFromJson(catalogJson),
|
|
17
|
+
});
|
|
18
|
+
const { policy, routes } = accessHono({ session: c => c.env.access });
|
|
19
|
+
app.get('/records', policy.permission('record.view'), list);
|
|
20
|
+
app.delete(
|
|
21
|
+
'/records/:id',
|
|
22
|
+
policy.resource('record.delete', loadRecord, { conceal: true }),
|
|
23
|
+
remove
|
|
24
|
+
);
|
|
25
|
+
app.route('/', routes()); // /access/me, /access/roles, …
|
|
26
|
+
// start(host): await access.reconcile(host) (TP-ACC-002)
|
|
27
|
+
```
|
|
28
|
+
- **Exactly one policy per route** — `public()` (only for `publicRoutes`), `authenticated()`,
|
|
29
|
+
`permission()`, `resource()` — and a test: `assertRouteCoverage(app, { publicRoutes })` (TP-ACC-001).
|
|
30
|
+
- Lists: `session.filter('record.view', { alias: 'r' })` gives the SQL condition; single records:
|
|
31
|
+
`policy.resource` / `authorize`. Never write a second copy of scope logic in SQL.
|
|
32
|
+
- Tables: re-export `@tego/plugin-sdk/access/drizzle` in your schema and generate migrations.
|
|
33
|
+
- Derived data (compiled rules, caches) updates in `hooks.onChange`, in the same transaction.
|
|
34
|
+
4. Frontend: `<AccessProvider api={ctx.api}>` under your `QueryClientProvider`; gate with
|
|
35
|
+
`useAccess()`, `<Can>`, `filterNav`; embed `<AccessSettings />` in your settings page;
|
|
36
|
+
install `@tego/plugin-access-ui` under the npm alias.
|
|
37
|
+
|
|
38
|
+
Never (TP-BE-002): check host role names (`ctx.roles.includes('admin')`), invent `isAdmin`, keep a
|
|
39
|
+
parallel role table, let background jobs act as a user, or treat frontend gating as the boundary.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Backend part
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
// backend/src/index.ts
|
|
5
|
+
import { serveBackend } from '@tego/plugin-sdk/server-runtime';
|
|
6
|
+
import { backend } from './app';
|
|
7
|
+
serveBackend(backend);
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
`definePluginBackend({ fetch, start?, stop?, fetchPublic?, authorizeStorage? })`. `fetch(request, ctx, host)`
|
|
11
|
+
receives a request whose identity the gateway signed and `serveBackend` verified: `ctx.userId`,
|
|
12
|
+
`ctx.tenantId`, `ctx.roles`, and for claw calls `ctx.avatarId` / `ctx.delegation`.
|
|
13
|
+
|
|
14
|
+
## Identity (TP-BE-001)
|
|
15
|
+
|
|
16
|
+
The caller is `ctx`, always. A `userId` in a path or body names a _target_ (whose role to change),
|
|
17
|
+
never who is asking. Do not parse `x-tego-plugin-*` headers yourself.
|
|
18
|
+
|
|
19
|
+
## Routes with Hono
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { Hono } from 'hono';
|
|
23
|
+
import {
|
|
24
|
+
HttpError,
|
|
25
|
+
jsonNotFound,
|
|
26
|
+
pluginErrorHandler,
|
|
27
|
+
readBody,
|
|
28
|
+
} from '@tego/plugin-sdk/hono';
|
|
29
|
+
import { z } from 'zod';
|
|
30
|
+
|
|
31
|
+
type Env = {
|
|
32
|
+
Bindings: {
|
|
33
|
+
host: PluginHostApi;
|
|
34
|
+
ctx: PluginUserContext;
|
|
35
|
+
access: AccessSession;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
const app = new Hono<Env>();
|
|
39
|
+
app.onError(pluginErrorHandler()); // access denials, HttpError, host 429 → JSON; other errors logged via host.log, generic 500
|
|
40
|
+
app.notFound(jsonNotFound);
|
|
41
|
+
|
|
42
|
+
const NoteInput = z.object({ body: z.string().trim().min(1).max(4000) });
|
|
43
|
+
app.post('/notes', policy.permission('note.create'), async c => {
|
|
44
|
+
const { body } = await readBody(c, NoteInput); // 400 { error: { code: 'invalid_body', path } }
|
|
45
|
+
const [row] = await c.env.host.db.query<{ id: string }>(
|
|
46
|
+
'INSERT INTO notes (author_id, body) VALUES ($1, $2) RETURNING id',
|
|
47
|
+
[c.env.ctx.userId, body]
|
|
48
|
+
);
|
|
49
|
+
return c.json({ id: row!.id }, 201);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export const backend = definePluginBackend({
|
|
53
|
+
fetch: (request, ctx, host) =>
|
|
54
|
+
app.fetch(request, { host, ctx, access: access.forRequest(ctx, host) }),
|
|
55
|
+
start: async host => {
|
|
56
|
+
await access.reconcile(host);
|
|
57
|
+
maintenance.start(host);
|
|
58
|
+
},
|
|
59
|
+
stop: () => maintenance.stop(),
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- Throw `new HttpError(409, 'note_locked', 'This note is locked')` for the caller's mistakes; the
|
|
64
|
+
frontend translates `code`.
|
|
65
|
+
- Never return SQL or stack traces; `pluginErrorHandler` logs them with `host.log.error` (TP-BE-005).
|
|
66
|
+
- Query only the columns you need; parameterize everything.
|
|
67
|
+
- Data: only your schema through `host.db` (one schema per install). Config: `host.config.current()`;
|
|
68
|
+
`host.config.refresh()` after a credential rotation. Never read config from env.
|
|
69
|
+
- Changes users should see live: `host.events.publish('changed', { kind })` (no personal data — the
|
|
70
|
+
stream is tenant-wide); `changedEventsMiddleware()` does it for every successful write.
|
|
71
|
+
- `start()` runs installation-level maintenance (timers, queues) as trusted system code: it never
|
|
72
|
+
acts as a user. Keep each step isolated so one failure does not stop the others
|
|
73
|
+
(`createMaintenance` from `@tego/plugin-sdk/server-runtime`).
|
|
74
|
+
- Outbound HTTP only to hosts in `parts.backend.egress`.
|
|
75
|
+
|
|
76
|
+
See storage.md for files and access.md for permissions.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Claw part
|
|
2
|
+
|
|
3
|
+
Anything inside a conversation (tools, skills, cards) is the claw part, running inside the employee's
|
|
4
|
+
OpenClaw container.
|
|
5
|
+
|
|
6
|
+
- `claw/openclaw.plugin.json`: `id`, `contracts.tools` (every tool you register), `configSchema`
|
|
7
|
+
(closed object; integer min/max/default; `uiHints` labels in all ten locales), audit notes for
|
|
8
|
+
tools. The packer stamps the plugin's version into it and `claw/package.json`.
|
|
9
|
+
- `claw/src/index.ts`: `definePluginEntry`; register tools only when the Tego API is installed;
|
|
10
|
+
`register()` must be idempotent and tolerate early-phase config. `api.logger` takes one string.
|
|
11
|
+
- Call your backend from a tool with the Tego API's `request({ path: '/api/v1/tego-os/plugin-api/<id>/…',
|
|
12
|
+
executionContext: 'current-user' })`; return 400/404/409/422 as values the model can read, throw
|
|
13
|
+
for anything else with a message written for the model.
|
|
14
|
+
- Tool results: JSON in both `content` and `details`.
|
|
15
|
+
- Skills (TP-CLAW-002): `claw/skills/<prefix>-<name>/SKILL.md` with `name: <prefix>-<name>` and a
|
|
16
|
+
`description`; every tool a skill mentions exists. `assertClawSkills(skillsDir, toolNames)` from
|
|
17
|
+
`@tego/plugin-sdk/testing` tests it.
|
|
18
|
+
- Config marked secret comes back as `__OPENCLAW_REDACTED__` in some reads; treat it as unusable.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Running in both shells: web and desktop
|
|
2
|
+
|
|
3
|
+
The plugin's frontend is loaded by two shells built from one codebase:
|
|
4
|
+
|
|
5
|
+
| | Web app (browser) | Desktop app (Tauri v2) |
|
|
6
|
+
| --------------------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
7
|
+
| Origin | the platform's https origin | `tauri://localhost` (macOS), `https://tauri.localhost` (Windows) |
|
|
8
|
+
| Script CSP | nonce + `'strict-dynamic'` | `'self' 'wasm-unsafe-eval'` — no `eval`, no `new Function`, no `blob:` scripts |
|
|
9
|
+
| `connect-src` | the platform only (plus its fixed CDNs) | any http(s) |
|
|
10
|
+
| `ctx.api` transport | browser fetch to the platform | a Rust protocol proxy that **buffers the whole response**, caps it at **100 MB** (413), strips `accept-encoding`, and answers 403 `LICENSE_INVALID` when the desktop licence lapses |
|
|
11
|
+
| Reloaded URL ending in `name.ext` | the SPA page | proxied to the server as a file |
|
|
12
|
+
| `window.open`, `<a download>` | browser behaviour | not reliably handled by the webview |
|
|
13
|
+
| Cookies | not used | not available |
|
|
14
|
+
|
|
15
|
+
`ctx.platform` tells you which one you run in (host API 1.37), but **write code that is correct in
|
|
16
|
+
both** rather than branching on it.
|
|
17
|
+
|
|
18
|
+
## Do / don't
|
|
19
|
+
|
|
20
|
+
| Instead of | Use | Rule |
|
|
21
|
+
| ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- | ------------------------- |
|
|
22
|
+
| `eval`, `new Function`, libraries that compile strings (some template engines, older chart libs) | precompiled code; check a new dependency with `tego-plugin doctor` and the preview's desktop mode | TP-DESK-001 |
|
|
23
|
+
| `` `${location.origin}/…` `` | `ctx.hostUrl(path)` for host paths, `ctx.publicPagesUrl` for share/QR links | TP-DESK-002 |
|
|
24
|
+
| `window.open(url)` | `ctx.openExternal(url)` | TP-DESK-003 |
|
|
25
|
+
| `<a download>`, `a.download = …`, `URL.createObjectURL` + click | `ctx.download({ path })` for a stored file, `ctx.download({ blob, fileName })` for generated content | TP-DESK-004 |
|
|
26
|
+
| `response.body.getReader()`, `EventSource`, NDJSON over `ctx.api` | `ctx.events.subscribe` for pushed changes; storage paths for big files | TP-DESK-005 |
|
|
27
|
+
| `navigator.clipboard.writeText` | `ctx.copyText(text)` | TP-DESK-006 |
|
|
28
|
+
| `fetch('https://api.example.com/…')` from the frontend | a backend route that calls it, with `parts.backend.egress: ["api.example.com"]` | TP-WEB-001 |
|
|
29
|
+
| `<script src>`, `import('https://…')`, CDN loaders | bundle the dependency | TP-WEB-002 |
|
|
30
|
+
| `/p/<id>/report.pdf` style routes | `/p/<id>/report?file=…` | (manifest schema for nav) |
|
|
31
|
+
|
|
32
|
+
## Files
|
|
33
|
+
|
|
34
|
+
- **Upload**: ask your backend for a grant (`host.storage.uploadPath(key, { maxBytes, ttlSeconds })`),
|
|
35
|
+
`fetch(ctx.hostUrl(grant.path), { method: 'PUT', body: file })`, then tell the backend to register it
|
|
36
|
+
(it checks with `host.storage.head(key)`). Never send large files through `ctx.api`.
|
|
37
|
+
- **Download**: the backend returns `signedPath` / `managedPath`; the frontend calls
|
|
38
|
+
`ctx.download({ path, fileName })`.
|
|
39
|
+
- **Generated content** (CSV, a template): `ctx.download({ blob, fileName })`.
|
|
40
|
+
|
|
41
|
+
Before host API 1.37 (plugins that cannot raise `hostApiRange` yet), feature-detect:
|
|
42
|
+
`ctx.download ? ctx.download(...) : legacyDownload(...)`, and mark the legacy line with
|
|
43
|
+
`tego-doctor-ignore TP-DESK-004: <reason>`.
|
|
44
|
+
|
|
45
|
+
## Checking
|
|
46
|
+
|
|
47
|
+
`tego-plugin dev --target desktop` serves the preview with the desktop CSP and a buffering proxy, and
|
|
48
|
+
reports CSP violations; try every page there before releasing.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Frontend part
|
|
2
|
+
|
|
3
|
+
The entry (`frontend/src/index.tsx`) default-exports `definePluginFrontend({ mount(el, ctx) })` and
|
|
4
|
+
returns a function that fully tears down. The shell shares `react`, `react-dom`, `@tego/ui`,
|
|
5
|
+
`react-i18next` and `lucide-react` with you (`SHARED_MODULE_IDS`); everything else you bundle.
|
|
6
|
+
|
|
7
|
+
## Root
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { getI18n, I18nextProvider } from 'react-i18next';
|
|
11
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
12
|
+
import { pluginI18nNamespace } from '@tego/plugin-sdk';
|
|
13
|
+
import { HostProvider, PluginErrorBoundary } from '@tego/plugin-sdk/react';
|
|
14
|
+
|
|
15
|
+
export function App({ ctx }: { ctx: PluginHostContext }) {
|
|
16
|
+
const [queryClient] = useState(
|
|
17
|
+
() =>
|
|
18
|
+
new QueryClient({ defaultOptions: { queries: { staleTime: 10_000 } } })
|
|
19
|
+
);
|
|
20
|
+
return (
|
|
21
|
+
<I18nextProvider
|
|
22
|
+
i18n={getI18n()}
|
|
23
|
+
defaultNS={pluginI18nNamespace(ctx.pluginId)}
|
|
24
|
+
>
|
|
25
|
+
<PluginErrorBoundary
|
|
26
|
+
fallback={(_error, reset) => <Crashed onRetry={reset} />}
|
|
27
|
+
>
|
|
28
|
+
<QueryClientProvider client={queryClient}>
|
|
29
|
+
{/* <AccessProvider api={ctx.api}> when the plugin has an access catalog */}
|
|
30
|
+
<HostProvider value={ctx}>
|
|
31
|
+
<div className={`${ctx.pluginId} flex h-full min-h-0 flex-col`}>
|
|
32
|
+
<Routes />
|
|
33
|
+
</div>
|
|
34
|
+
</HostProvider>
|
|
35
|
+
</QueryClientProvider>
|
|
36
|
+
</PluginErrorBoundary>
|
|
37
|
+
</I18nextProvider>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- **Error boundary (TP-UI-004)**: the shell's boundary is outside your React root; without your own,
|
|
43
|
+
a render error is a blank page. `Crashed` is your translated `EmptyState` with a retry button.
|
|
44
|
+
- **Namespace**: the shell merged your locale files under `plugin.<id>`; name it once at the root.
|
|
45
|
+
- **Scope class (TP-UI-003)**: `tegoPluginTailwind` compiles your utilities under `.<pluginId>`.
|
|
46
|
+
|
|
47
|
+
## Routing
|
|
48
|
+
|
|
49
|
+
`ctx.basePath` is `/p/<id>`. The shell does not remount the page when the in-plugin path changes:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
const route = usePluginRoute(); // { path: '/deals/42', search: '?tab=x' }
|
|
53
|
+
const host = useHost();
|
|
54
|
+
host.navigate('/deals/42'); // relative to basePath; absolute /p/... also fine
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Keep route parsing in a pure `lib/route.ts` (`parseRoute(path, search)` returning a union) and test it.
|
|
58
|
+
`ctx.can(navKey)` only says whether the manifest declares that page; real permissions come from the
|
|
59
|
+
access catalog (`useAccess`).
|
|
60
|
+
|
|
61
|
+
## Calling the backend
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { createApiClient, PluginApiError } from '@tego/plugin-sdk/react';
|
|
65
|
+
export const api = createApiClient(ctx.api);
|
|
66
|
+
const notes = await api.get<{ notes: Note[] }>('/notes');
|
|
67
|
+
await api.post('/notes', { body: text });
|
|
68
|
+
// errors: PluginApiError { status, code, message, details } — both the backend's {error:{code,message}}
|
|
69
|
+
// and the gateway's {success:false,error:{message}} shapes are understood
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Never use `ctx.api.json` for calls whose errors you show (it drops the body). Show errors with
|
|
73
|
+
`describeApiError(error, '<ns>')` → `{ key, message }`: `key ? t(key, { defaultValue: message }) : message`.
|
|
74
|
+
|
|
75
|
+
## Live data
|
|
76
|
+
|
|
77
|
+
`useLiveUpdates(() => queryClient.invalidateQueries({ queryKey: ['<ns>'] }))` subscribes to
|
|
78
|
+
`ctx.events` (your backend's `host.events.publish`), batching bursts; React Query's `refetchInterval` should be slow
|
|
79
|
+
(120 s) while `ctx.events.connected()` and faster (15 s) when not.
|
|
80
|
+
|
|
81
|
+
## Optional host capabilities
|
|
82
|
+
|
|
83
|
+
`ctx.office`, `ctx.ai`, `ctx.delegation` may be absent: feature-detect and keep a fallback.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Host API versions
|
|
2
|
+
|
|
3
|
+
`hostApiRange` (`^X.Y.Z` or exact `X.Y.Z`) says which shells can run the plugin. Declare the
|
|
4
|
+
**lowest** version whose features you use; the shell refuses a plugin whose range it does not satisfy,
|
|
5
|
+
and upload refuses a manifest field newer than the range (TP-MAN-001), as does doctor for code
|
|
6
|
+
(TP-MAN-002). New plugins with an access catalog start at `^1.22.0`; the scaffold uses `^1.37.0`.
|
|
7
|
+
|
|
8
|
+
| Host API | Adds |
|
|
9
|
+
| --------------- | -------------------------------------------------------------------------------- |
|
|
10
|
+
| 1.2.0 | frontend route store, user display name |
|
|
11
|
+
| 1.3.0 | frontend event stream (`ctx.events`) |
|
|
12
|
+
| 1.4.0 | plugin configuration |
|
|
13
|
+
| 1.5.0 | `host.directory` |
|
|
14
|
+
| 1.9.0 | AI form filling (`ctx.ai`) |
|
|
15
|
+
| 1.10.0 | Python backends, `egress`, public routes and pages, plugin knowledge collections |
|
|
16
|
+
| 1.12.0 | `storage.uploadPath`, `get(key, { firstBytes })`, `ctx.hostUrl` |
|
|
17
|
+
| 1.14.0 | `ctx.layout.collapseSidebar` |
|
|
18
|
+
| 1.15.0 | managed downloads (`storage.managedPath`, `authorizeStorage`) |
|
|
19
|
+
| 1.17.0 / 1.18.0 | account provisioning / password reset |
|
|
20
|
+
| 1.19.0 | `storage.copy`, streams |
|
|
21
|
+
| 1.20.0 | Mermaid and HTML diagram rendering |
|
|
22
|
+
| 1.21.0 | `ctx.office` |
|
|
23
|
+
| 1.22.0 | access catalog, `@tego/plugin-sdk/access`, `host.tenantId` |
|
|
24
|
+
| 1.23.0 | `knowledge.manage` |
|
|
25
|
+
| 1.26.0 | `proactive.emit` |
|
|
26
|
+
| 1.29.0 | `mail.send` |
|
|
27
|
+
| 1.30.0 | `agentReadiness`, `settingsRoute` |
|
|
28
|
+
| 1.33.0 | `workDelegation` |
|
|
29
|
+
| 1.34.0 | `attention.request` |
|
|
30
|
+
| 1.37.0 | `ctx.platform`, `ctx.openExternal`, `ctx.download`, `ctx.copyText` |
|
|
31
|
+
|
|
32
|
+
Optional members (`ctx.office`, `ctx.ai`, `ctx.delegation`, `ctx.download` before 1.37) must be
|
|
33
|
+
feature-detected when the range admits an older shell. The full API is in
|
|
34
|
+
`agent/reference/plugin-sdk-api.md`.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# i18n
|
|
2
|
+
|
|
3
|
+
- **Ten locales (TP-I18N-001)**: `zh-CN zh-TW en ja ko de es fr it pt` in
|
|
4
|
+
`frontend/i18n/<locale>.json`. `zh-CN` is the reference: every other file has exactly its keys.
|
|
5
|
+
Upload refuses a missing locale, a missing key, or a nav `labelKey` that no file defines.
|
|
6
|
+
- **One root**: every key under the plugin's own root (`notes.home.title`, `notes.nav.home`,
|
|
7
|
+
`notes.errors.<code>`). No flat keys with dots in them.
|
|
8
|
+
- **Placeholders (TP-I18N-002)**: `{{name}}` sets match `zh-CN` in every locale; no empty strings.
|
|
9
|
+
- **Literal keys (TP-I18N-003)**: `t('notes.home.title')` must exist. For keys built at runtime
|
|
10
|
+
(`t(\`notes.status.${status}\`)`) list the variants in a test.
|
|
11
|
+
- **Terms (TP-I18N-004)**: 数字员工 / Employee via `{{avatarTerm}}` (`{{avatarTermShort}}`); 空间
|
|
12
|
+
Workspace; 技能 Skill; 模板 Template. Never 数字分身 / digital avatar.
|
|
13
|
+
- **Access strings**: with `@tego/plugin-access-ui`, `tego-plugin-pack` merges `tegoAccess.*` into
|
|
14
|
+
your files; don't copy them.
|
|
15
|
+
- **Test**: `assertLocaleParity(fileURLToPath(new URL('../i18n', import.meta.url)))` from
|
|
16
|
+
`@tego/plugin-sdk/testing` in a frontend test file runs these checks in `pnpm test`.
|
|
17
|
+
- Translate everything you add in the same change: adding a key to `zh-CN` only fails the build.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Manifest and packaging
|
|
2
|
+
|
|
3
|
+
`tego.plugin.json` at the package root (TP-MAN-001):
|
|
4
|
+
|
|
5
|
+
- `manifestVersion: 1`; `id` `^[a-z0-9][a-z0-9._-]*$`, stable forever; `version` semver — upgrades
|
|
6
|
+
install over the same id, downgrades are refused.
|
|
7
|
+
- `displayName` / `description` per locale; `scope`: `avatar` (a capability of chosen employees;
|
|
8
|
+
required for a claw part) or `tenant` (a workspace app for everyone).
|
|
9
|
+
- `parts.frontend` — `icon` from the closed `PLUGIN_NAV_ICONS` list; `nav` (≤ 20 entries, unique
|
|
10
|
+
`key`, `labelKey` in your locales, optional `path` of letters, digits, `/_-` only). No `nav` = no page.
|
|
11
|
+
- `parts.backend` — `limits`, `egress` (hosts the backend may call; a bare host means 443),
|
|
12
|
+
`publicRoutes` (served by `fetchPublic`, no user), `migrationsDir`.
|
|
13
|
+
- `parts.claw` — `claw/openclaw.plugin.json` + `claw/package.json`; the packer stamps the version.
|
|
14
|
+
- `declares` (`database`, `storage`, `events`, `notifications`) are disclosures shown at install and
|
|
15
|
+
need a backend part.
|
|
16
|
+
- Configuration: `parts.frontend.config` / `parts.backend.config` hold a flat schema (scalars,
|
|
17
|
+
string enums, string arrays; ≤ 50 fields). `sensitive` only in the backend section. Read it with
|
|
18
|
+
`host.config.current()` (backend) or `ctx.config` (frontend). Claw config is
|
|
19
|
+
`configSchema` in `openclaw.plugin.json`, never in the manifest.
|
|
20
|
+
- `hostApiRange`: see host-api-versions.md.
|
|
21
|
+
|
|
22
|
+
Packaging: `tego-plugin-pack` (the `build` script) builds every part, merges access UI strings,
|
|
23
|
+
runs the upload-blocking doctor checks and writes `build/<id>-<version>.zip`. Never hand-roll the
|
|
24
|
+
SystemJS output. Dependencies (TP-PKG-001): SDK packages under npm aliases —
|
|
25
|
+
`"@tego/plugin-sdk": "npm:@zhama/tego-plugin-sdk@^1.37.0"`, likewise `@tego/ui`,
|
|
26
|
+
`@tego/plugin-access-ui` — or `workspace:*` inside a tego-os checkout.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Database migrations
|
|
2
|
+
|
|
3
|
+
Each install has its own Postgres schema and a restricted role. The host applies
|
|
4
|
+
`backend/migrations/*.sql` in ordinal order, one transaction per file, with `search_path` set to your
|
|
5
|
+
schema only — so nothing is schema-qualified and nothing references a platform table.
|
|
6
|
+
|
|
7
|
+
- **Names (TP-BE-003)**: `0001_initial.sql`, `0002_add_tags.sql` — numeric ordinal, unique.
|
|
8
|
+
- **Append-only (TP-BE-004)**: a committed migration may already have run on an install; never edit or
|
|
9
|
+
delete it. Fix forward with a new file.
|
|
10
|
+
- **Source of truth**: the drizzle schema in `backend/schema/*.ts`. `pnpm db:generate` runs
|
|
11
|
+
`drizzle-kit generate` into `backend/.drizzle/` (committed), then `tego-plugin migrations sync` copies
|
|
12
|
+
new files into `backend/migrations/` with the next free ordinal and strips `"public".`.
|
|
13
|
+
`tego-plugin migrations check` (part of `build`) fails when they are out of sync.
|
|
14
|
+
- Hand-write only what drizzle cannot express: functions, triggers, RLS policies.
|
|
15
|
+
- Never `drizzle-kit push`.
|
|
16
|
+
- Test SQL against a real database with `createTestPluginHost({ pluginId, migrationsDir })`
|
|
17
|
+
(`TEST_DATABASE_URL`); it applies your migrations into a throwaway schema.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Files and storage
|
|
2
|
+
|
|
3
|
+
`declares.storage: true`. Keys are relative to your install's prefix.
|
|
4
|
+
|
|
5
|
+
| Need | Backend | Frontend |
|
|
6
|
+
| ------------------------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
7
|
+
| Small file (≤ 25 MiB) you produce | `host.storage.put(key, bytes, contentType)` | — |
|
|
8
|
+
| User uploads a file | `uploadPath(key, { maxBytes, ttlSeconds: 900 })` → return `{ path }` | `fetch(ctx.hostUrl(path), { method: 'PUT', body: file })`, then POST `/files/register` |
|
|
9
|
+
| Check an upload | `head(key)` (size, type), `get(key, { firstBytes: 8 })` (magic bytes) | — |
|
|
10
|
+
| Let a user download | `signedPath(key, 300)` or `managedPath(key, policy, 300)` | `ctx.download({ path, fileName })` |
|
|
11
|
+
| Re-check permission on every download | `managedPath` + `authorizeStorage` (needs `storage.managed`) | same |
|
|
12
|
+
| Copy / stream | `copy`, `getStream`, `putStream` (needs `storage.transfer`, host 1.19) | — |
|
|
13
|
+
|
|
14
|
+
- Never move large files through `ctx.api`: the desktop app buffers it and caps it at 100 MB.
|
|
15
|
+
- Validate type and size on the client first (shared limits in `shared/`), and again on the backend.
|
|
16
|
+
- `authorizeStorage` decides with the access engine:
|
|
17
|
+
`access.forSubject(await access.subjectFor(userId, host), host)`; deny on any error, and test
|
|
18
|
+
both allow and deny.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Tests
|
|
2
|
+
|
|
3
|
+
Gates (TP-PKG-002): `lint` (eslint), `type-check` (`tsgo --noEmit` per part), `test` (`vitest run`),
|
|
4
|
+
`build` (`tego-plugin-pack`). Run them, plus `tego-plugin doctor`, after every change.
|
|
5
|
+
|
|
6
|
+
- **Backend**: routes against a real Postgres with `createTestPluginHost({ pluginId, migrationsDir })`
|
|
7
|
+
(`TEST_DATABASE_URL`); call `backend.fetch(new Request(...), CALLER, harness.host)`. A missing
|
|
8
|
+
database fails the run, it never silently skips.
|
|
9
|
+
- **Access**: `assertRouteCoverage(app)`; allow _and_ deny cases for every permission you add.
|
|
10
|
+
- **Frontend**: pure logic in `.ts` files (route parser, formatters, API error mapping) — no
|
|
11
|
+
component tests.
|
|
12
|
+
- **i18n**: `assertLocaleParity(dir)`.
|
|
13
|
+
- **Claw**: `assertClawSkills(skillsDir, toolNames)`; registered tools equal `contracts.tools`.
|
|
14
|
+
- **Style**: each test file starts with a Business Cases comment (domain, then one line per case
|
|
15
|
+
`NOTE-001 posting a note → 201 and visible on the next list`); every `it` title starts with
|
|
16
|
+
`[NOTE-001]`. Assert exact status codes and values; at least one failure path per file; no
|
|
17
|
+
conditional assertions.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# UI and design
|
|
2
|
+
|
|
3
|
+
Your page sits inside the shell next to its own UI; it must look like part of it in light and dark.
|
|
4
|
+
|
|
5
|
+
- **Components (TP-UI-002)**: `@tego/ui` first — Button, Input, Select, Dialog, Popover, Tooltip,
|
|
6
|
+
Tabs, Table, Badge, EmptyState, Loading, Skeleton, toast/Toaster, charts. Never import
|
|
7
|
+
`@radix-ui/*`; never rebuild a primitive locally.
|
|
8
|
+
- **Colors (TP-UI-001)**: token classes only — surfaces `bg-background bg-card bg-sunken bg-inset
|
|
9
|
+
bg-popover`, text `text-foreground text-muted-foreground`, lines `border-border border-border-strong`,
|
|
10
|
+
semantic `bg-X text-X-foreground` / `bg-X-soft text-X` with X in
|
|
11
|
+
`primary signal success warning destructive info`. No hex, `rgb()`, palette classes
|
|
12
|
+
(`bg-slate-50`, `text-white`, `bg-black/40`) or `dark:` — tokens carry both themes.
|
|
13
|
+
`primary` means selection/focus, `signal` means "happening now".
|
|
14
|
+
- **Scope (TP-UI-003)**: `tegoPluginTailwind` scopes utilities under `.<pluginId>` (no preflight).
|
|
15
|
+
Put the class on your root element; portalled content (dialogs, popovers, selects) renders outside
|
|
16
|
+
it — add `tegoPluginScopeRoot(pluginId)` to PostCSS, or put the class on each `*Content`.
|
|
17
|
+
- **Status** is text or an icon plus color, never color alone. Machine data (ids, amounts, times,
|
|
18
|
+
versions) uses `font-mono tabular-nums`.
|
|
19
|
+
- **Shapes**: `rounded-control rounded-card rounded-container`; cards `shadow-card`; no card inside
|
|
20
|
+
a card. Motion: `duration-hover duration-enter`, no decorative infinite animation.
|
|
21
|
+
- **Copy**: short and specific; no explanatory paragraphs on pages; an empty state is one line plus
|
|
22
|
+
one action. Chinese copy follows the platform terms (空间、数字员工、技能、模板).
|