@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/dist/scaffold.js
CHANGED
|
@@ -1,4 +1,658 @@
|
|
|
1
|
-
var
|
|
1
|
+
var I=Object.defineProperty;var o=(e,t)=>I(e,"name",{value:t,configurable:!0});import{HOST_API_VERSION as $}from"./chunk-56QOZQTY.js";import{existsSync as j,mkdirSync as F,mkdtempSync as U,writeFileSync as q,renameSync as M,rmSync as z}from"fs";import{join as m,dirname as H,resolve as G}from"path";var K=`-- Generated by \`pnpm -F @tego/plugin-sdk access:sql\` from src/access/drizzle.ts. Do not edit.
|
|
2
|
+
CREATE TABLE IF NOT EXISTS "tego_access_assignments" (
|
|
3
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
4
|
+
"tenant_id" text NOT NULL,
|
|
5
|
+
"role_id" text NOT NULL,
|
|
6
|
+
"principal_kind" text NOT NULL,
|
|
7
|
+
"principal_id" text NOT NULL,
|
|
8
|
+
"include_descendants" boolean NOT NULL,
|
|
9
|
+
"unit_roles" text[],
|
|
10
|
+
"domain_type" text,
|
|
11
|
+
"domain_id" text,
|
|
12
|
+
"created_by" text NOT NULL,
|
|
13
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
14
|
+
CONSTRAINT "tego_access_assignments_domain_pair" CHECK (("tego_access_assignments"."domain_type" IS NULL) = ("tego_access_assignments"."domain_id" IS NULL))
|
|
15
|
+
);
|
|
16
|
+
--> statement-breakpoint
|
|
17
|
+
CREATE TABLE IF NOT EXISTS "tego_access_audit" (
|
|
18
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
19
|
+
"tenant_id" text NOT NULL,
|
|
20
|
+
"actor_id" text,
|
|
21
|
+
"action" text NOT NULL,
|
|
22
|
+
"target_type" text NOT NULL,
|
|
23
|
+
"target_id" text NOT NULL,
|
|
24
|
+
"before" jsonb,
|
|
25
|
+
"after" jsonb,
|
|
26
|
+
"at" timestamp with time zone DEFAULT now() NOT NULL
|
|
27
|
+
);
|
|
28
|
+
--> statement-breakpoint
|
|
29
|
+
CREATE TABLE IF NOT EXISTS "tego_access_roles" (
|
|
30
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
31
|
+
"tenant_id" text NOT NULL,
|
|
32
|
+
"key" text,
|
|
33
|
+
"builtin" boolean NOT NULL,
|
|
34
|
+
"domain_type" text,
|
|
35
|
+
"domain_id" text,
|
|
36
|
+
"name" text NOT NULL,
|
|
37
|
+
"description" text NOT NULL,
|
|
38
|
+
"grants" jsonb NOT NULL,
|
|
39
|
+
"catalog_version" integer NOT NULL,
|
|
40
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
41
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
42
|
+
"updated_by" text,
|
|
43
|
+
CONSTRAINT "tego_access_roles_builtin_shape" CHECK (("tego_access_roles"."builtin" AND "tego_access_roles"."key" IS NOT NULL AND "tego_access_roles"."domain_type" IS NULL AND "tego_access_roles"."domain_id" IS NULL) OR (NOT "tego_access_roles"."builtin" AND "tego_access_roles"."key" IS NULL)),
|
|
44
|
+
CONSTRAINT "tego_access_roles_domain_pair" CHECK (("tego_access_roles"."domain_type" IS NULL) = ("tego_access_roles"."domain_id" IS NULL))
|
|
45
|
+
);
|
|
46
|
+
--> statement-breakpoint
|
|
47
|
+
CREATE TABLE IF NOT EXISTS "tego_access_settings" (
|
|
48
|
+
"tenant_id" text PRIMARY KEY NOT NULL,
|
|
49
|
+
"unassigned_role_id" text,
|
|
50
|
+
"version" integer NOT NULL,
|
|
51
|
+
"epoch" text NOT NULL,
|
|
52
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
53
|
+
"updated_by" text
|
|
54
|
+
);
|
|
55
|
+
--> statement-breakpoint
|
|
56
|
+
DO $$ BEGIN
|
|
57
|
+
ALTER TABLE "tego_access_assignments" ADD CONSTRAINT "tego_access_assignments_role_fk" FOREIGN KEY ("role_id") REFERENCES "tego_access_roles"("id") ON DELETE cascade ON UPDATE no action;
|
|
58
|
+
EXCEPTION
|
|
59
|
+
WHEN duplicate_object THEN null;
|
|
60
|
+
END $$;
|
|
61
|
+
--> statement-breakpoint
|
|
62
|
+
DO $$ BEGIN
|
|
63
|
+
ALTER TABLE "tego_access_settings" ADD CONSTRAINT "tego_access_settings_unassigned_role_fk" FOREIGN KEY ("unassigned_role_id") REFERENCES "tego_access_roles"("id") ON DELETE restrict ON UPDATE no action;
|
|
64
|
+
EXCEPTION
|
|
65
|
+
WHEN duplicate_object THEN null;
|
|
66
|
+
END $$;
|
|
67
|
+
--> statement-breakpoint
|
|
68
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "tego_access_assignments_unique" ON "tego_access_assignments" USING btree ("tenant_id","role_id","principal_kind","principal_id",coalesce("domain_type", ''),coalesce("domain_id", ''));--> statement-breakpoint
|
|
69
|
+
CREATE INDEX IF NOT EXISTS "tego_access_assignments_principal_idx" ON "tego_access_assignments" USING btree ("tenant_id","principal_kind","principal_id");--> statement-breakpoint
|
|
70
|
+
CREATE INDEX IF NOT EXISTS "tego_access_assignments_role_idx" ON "tego_access_assignments" USING btree ("role_id");--> statement-breakpoint
|
|
71
|
+
CREATE INDEX IF NOT EXISTS "tego_access_audit_tenant_at_idx" ON "tego_access_audit" USING btree ("tenant_id","at");--> statement-breakpoint
|
|
72
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "tego_access_roles_builtin_key_unique" ON "tego_access_roles" USING btree ("tenant_id","key") WHERE "tego_access_roles"."builtin";--> statement-breakpoint
|
|
73
|
+
CREATE INDEX IF NOT EXISTS "tego_access_roles_tenant_idx" ON "tego_access_roles" USING btree ("tenant_id");`,Q=`-- The backend part's schema.
|
|
74
|
+
--
|
|
75
|
+
-- Applied by the host once, inside a transaction whose \`search_path\` is this
|
|
76
|
+
-- install's own schema and nothing else. So nothing here is schema-qualified
|
|
77
|
+
-- and nothing references a platform table. Migrations are append-only:
|
|
78
|
+
-- correct a past one with a new file.
|
|
79
|
+
|
|
80
|
+
CREATE TABLE notes (
|
|
81
|
+
id text PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
82
|
+
tenant_id text NOT NULL,
|
|
83
|
+
author_id text NOT NULL,
|
|
84
|
+
body text NOT NULL,
|
|
85
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
86
|
+
);
|
|
87
|
+
`;function V(e){return[{path:"backend/src/index.ts",contents:W},{path:"backend/src/app.ts",contents:J(e)},{path:"backend/src/app.test.ts",contents:Y(e)},{path:"backend/migrations/0001_init.sql",contents:Q},...e.withAccess?[{path:"backend/src/access.ts",contents:X},{path:"backend/migrations/0002_access.sql",contents:K}]:[]]}o(V,"standardBackendFiles");var W=`import { serveBackend } from '@tego/plugin-sdk/server-runtime';
|
|
88
|
+
|
|
89
|
+
import { backend } from './app';
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The backend part's entry, run as \`node index.js\` by the plugin runner.
|
|
93
|
+
* \`serveBackend\` owns the socket, the health path and the verified caller.
|
|
94
|
+
*/
|
|
95
|
+
serveBackend(backend);
|
|
96
|
+
`,X=`import { accessCatalogFromJson, createAccess } from '@tego/plugin-sdk/access';
|
|
97
|
+
|
|
98
|
+
import catalogJson from '../../shared/access.json';
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Who may do what inside this plugin. The catalog (shared/access.json) is the
|
|
102
|
+
* source of truth; never check host role names in code \u2014 bind them in the
|
|
103
|
+
* catalog's hostRoleBindings instead.
|
|
104
|
+
*/
|
|
105
|
+
export const catalog = accessCatalogFromJson(catalogJson);
|
|
106
|
+
export const access = createAccess({ catalog });
|
|
107
|
+
`;function J({withAccess:e}){const t=e?`import type { AccessSession } from '@tego/plugin-sdk/access';
|
|
108
|
+
import { accessHono } from '@tego/plugin-sdk/access/hono';
|
|
109
|
+
`:"",n=o(s=>e?`policy.permission('${s}'), `:"","policy");return`${t}import {
|
|
110
|
+
changedEventsMiddleware,
|
|
111
|
+
jsonNotFound,
|
|
112
|
+
notFound,
|
|
113
|
+
pluginErrorHandler,
|
|
114
|
+
readBody,
|
|
115
|
+
} from '@tego/plugin-sdk/hono';
|
|
116
|
+
import { definePluginBackend } from '@tego/plugin-sdk/server';
|
|
117
|
+
import type { PluginHostApi, PluginUserContext } from '@tego/plugin-sdk/server';
|
|
118
|
+
import { Hono } from 'hono';
|
|
119
|
+
import { z } from 'zod';
|
|
120
|
+
${e?`
|
|
121
|
+
import { access } from './access';
|
|
122
|
+
`:""}
|
|
123
|
+
/**
|
|
124
|
+
* The backend's HTTP surface, reached at /api/v1/tego-os/plugin-api/<id>/.
|
|
125
|
+
*
|
|
126
|
+
* \`ctx\` is the caller the gateway signed and serveBackend verified \u2014 the only
|
|
127
|
+
* identity there is. A user id in a path or body names a target, never the caller.
|
|
128
|
+
*/
|
|
129
|
+
interface Env {
|
|
130
|
+
Bindings: {
|
|
131
|
+
host: PluginHostApi;
|
|
132
|
+
ctx: PluginUserContext;${e?`
|
|
133
|
+
access: AccessSession;`:""}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const app = new Hono<Env>();
|
|
138
|
+
${e?`const { policy, routes } = accessHono({ session: c => c.env.access });
|
|
139
|
+
`:""}
|
|
140
|
+
// Every error becomes JSON with a code the frontend translates; anything
|
|
141
|
+
// unexpected is logged through host.log and answered with a generic 500.
|
|
142
|
+
app.onError(pluginErrorHandler());
|
|
143
|
+
app.notFound(jsonNotFound);
|
|
144
|
+
// Open pages refresh when a write succeeds (useLiveUpdates on the frontend).
|
|
145
|
+
app.use('*', changedEventsMiddleware());
|
|
146
|
+
|
|
147
|
+
const NoteInput = z.object({ body: z.string().trim().min(1).max(4000) });
|
|
148
|
+
|
|
149
|
+
app.get('/notes', ${n("note.view")}async c => {
|
|
150
|
+
const notes = await c.env.host.db.query<{ id: string; body: string }>(
|
|
151
|
+
'SELECT id, body FROM notes WHERE tenant_id = $1 ORDER BY created_at DESC LIMIT 200',
|
|
152
|
+
[c.env.ctx.tenantId]
|
|
153
|
+
);
|
|
154
|
+
return c.json({ notes });
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
app.post('/notes', ${n("note.create")}async c => {
|
|
158
|
+
const { body } = await readBody(c, NoteInput);
|
|
159
|
+
const [row] = await c.env.host.db.query<{ id: string }>(
|
|
160
|
+
'INSERT INTO notes (tenant_id, author_id, body) VALUES ($1, $2, $3) RETURNING id',
|
|
161
|
+
[c.env.ctx.tenantId, c.env.ctx.userId, body]
|
|
162
|
+
);
|
|
163
|
+
return c.json({ id: row!.id }, 201);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
app.delete('/notes/:id', ${n("note.delete")}async c => {
|
|
167
|
+
const deleted = await c.env.host.db.query<{ id: string }>(
|
|
168
|
+
'DELETE FROM notes WHERE tenant_id = $1 AND id = $2 RETURNING id',
|
|
169
|
+
[c.env.ctx.tenantId, c.req.param('id')]
|
|
170
|
+
);
|
|
171
|
+
if (deleted.length === 0) throw notFound('The note');
|
|
172
|
+
return c.body(null, 204);
|
|
173
|
+
});
|
|
174
|
+
${e?`
|
|
175
|
+
// /access/me, /access/roles, \u2026 for the access settings page.
|
|
176
|
+
app.route('/', routes());
|
|
177
|
+
`:""}
|
|
178
|
+
export const backend = definePluginBackend({
|
|
179
|
+
fetch: async (request, ctx, host) =>
|
|
180
|
+
app.fetch(request, { host, ctx${e?", access: access.forRequest(ctx, host)":""} }),${e?`
|
|
181
|
+
// Brings the built-in roles in line with the catalog on every start.
|
|
182
|
+
start: async host => {
|
|
183
|
+
await access.reconcile(host);
|
|
184
|
+
},`:""}
|
|
185
|
+
});
|
|
186
|
+
`}o(J,"appTs");function Y({withAccess:e}){return`import { dirname, join } from 'node:path';
|
|
187
|
+
import { fileURLToPath } from 'node:url';
|
|
188
|
+
${e?`
|
|
189
|
+
import { assertRouteCoverage } from '@tego/plugin-sdk/access/hono';`:""}
|
|
190
|
+
import {
|
|
191
|
+
createTestPluginHost,
|
|
192
|
+
type TestPluginHost,
|
|
193
|
+
} from '@tego/plugin-sdk/testing';
|
|
194
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
195
|
+
${e?`
|
|
196
|
+
import { access } from './access';`:""}
|
|
197
|
+
import { app, backend } from './app';
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Business Cases \u2014 \u7B14\u8BB0\u63A5\u53E3\uFF08\u811A\u624B\u67B6\u793A\u4F8B\uFF0C\u6539\u529F\u80FD\u65F6\u6309\u540C\u6837\u65B9\u5F0F\u8865\u7528\u4F8B\uFF09
|
|
201
|
+
*
|
|
202
|
+
* \u540E\u7AEF\u53EA\u8BA4 serveBackend \u9A8C\u8FC7\u7684\u8C03\u7528\u8005\uFF08ctx\uFF09\uFF1B\u6570\u636E\u5728\u672C\u5B89\u88C5\u81EA\u5DF1\u7684 schema \u91CC\uFF1B
|
|
203
|
+
* \u6BCF\u4E2A\u8BF7\u6C42\u7684\u9519\u8BEF\u90FD\u662F\u5E26 code \u7684 JSON\u3002${e?`\u6743\u9650\u6765\u81EA shared/access.json\uFF1A\u5BBF\u4E3B\u7BA1\u7406\u5458\u7ED1\u5B9A\u4E3A admin\uFF0C
|
|
204
|
+
* \u5176\u4ED6\u4EBA\u9ED8\u8BA4\u662F member\uFF08\u53EF\u67E5\u770B\u3001\u6DFB\u52A0\uFF0C\u4E0D\u80FD\u5220\u9664\uFF09\u3002`:""}
|
|
205
|
+
*
|
|
206
|
+
${e?` * NOTE-000 \u6BCF\u6761\u8DEF\u7531\u6070\u597D\u4E00\u4E2A\u6743\u9650\u7B56\u7565\uFF08assertRouteCoverage\uFF09
|
|
207
|
+
`:""} * NOTE-001 \u6DFB\u52A0\u540E\u5217\u8868\u91CC\u80FD\u770B\u5230\uFF08201 + id\uFF1BGET \u8FD4\u56DE\u8FD9\u6761\uFF09
|
|
208
|
+
* NOTE-002 \u7A7A\u5185\u5BB9 \u2192 400 invalid_body\uFF0C\u4EC0\u4E48\u90FD\u4E0D\u5199
|
|
209
|
+
* NOTE-003 \u5220\u9664\u4E0D\u5B58\u5728\u7684\u7B14\u8BB0 \u2192 404 not_found
|
|
210
|
+
${e?` * NOTE-004 member \u5220\u9664 \u2192 403 permission_required\uFF1Badmin \u5220\u9664 \u2192 204 \u4E14\u5217\u8868\u91CC\u6D88\u5931
|
|
211
|
+
`:""} */
|
|
212
|
+
|
|
213
|
+
const migrationsDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'migrations');
|
|
214
|
+
|
|
215
|
+
let harness: TestPluginHost;
|
|
216
|
+
|
|
217
|
+
function caller(roles: string[]) {
|
|
218
|
+
return {
|
|
219
|
+
userId: roles.length ? '00000000-0000-4000-8000-00000000000a' : '00000000-0000-4000-8000-00000000000b',
|
|
220
|
+
tenantId: harness.host.tenantId,
|
|
221
|
+
roles,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
const ADMIN = () => caller(['admin']);
|
|
225
|
+
const MEMBER = () => caller([]);
|
|
226
|
+
|
|
227
|
+
function call(
|
|
228
|
+
who: ReturnType<typeof caller>,
|
|
229
|
+
method: string,
|
|
230
|
+
path: string,
|
|
231
|
+
body?: unknown
|
|
232
|
+
): Promise<Response> {
|
|
233
|
+
const init: RequestInit =
|
|
234
|
+
body === undefined
|
|
235
|
+
? { method }
|
|
236
|
+
: { method, headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) };
|
|
237
|
+
return backend.fetch(new Request(\`http://plugin.internal\${path}\`, init), who, harness.host);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
beforeEach(async () => {
|
|
241
|
+
harness = await createTestPluginHost({ pluginId: 'scaffold', migrationsDir });${e?`
|
|
242
|
+
await access.reconcile(harness.host);`:""}
|
|
243
|
+
});
|
|
244
|
+
afterEach(() => harness?.close());
|
|
245
|
+
|
|
246
|
+
describe('notes API', () => {${e?`
|
|
247
|
+
it('[NOTE-000] guards every route with exactly one policy', () => {
|
|
248
|
+
expect(assertRouteCoverage(app).length).toBeGreaterThan(3);
|
|
249
|
+
});
|
|
250
|
+
`:"\n it('[NOTE-000] exposes the notes routes', () => {\n expect(app.routes.map(route => `${route.method} ${route.path}`)).toContain('GET /notes');\n });\n"}
|
|
251
|
+
it('[NOTE-001] lists a note right after adding it', async () => {
|
|
252
|
+
const created = await call(MEMBER(), 'POST', '/notes', { body: 'first note' });
|
|
253
|
+
expect(created.status).toBe(201);
|
|
254
|
+
const { id } = (await created.json()) as { id: string };
|
|
255
|
+
const listed = await call(MEMBER(), 'GET', '/notes');
|
|
256
|
+
expect(listed.status).toBe(200);
|
|
257
|
+
expect(await listed.json()).toEqual({ notes: [{ id, body: 'first note' }] });
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('[NOTE-002] refuses an empty note and writes nothing', async () => {
|
|
261
|
+
const refused = await call(MEMBER(), 'POST', '/notes', { body: ' ' });
|
|
262
|
+
expect(refused.status).toBe(400);
|
|
263
|
+
expect(await refused.json()).toMatchObject({ error: { code: 'invalid_body' } });
|
|
264
|
+
expect(await (await call(MEMBER(), 'GET', '/notes')).json()).toEqual({ notes: [] });
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('[NOTE-003] answers 404 for a note that does not exist', async () => {
|
|
268
|
+
const missing = await call(ADMIN(), 'DELETE', '/notes/nope');
|
|
269
|
+
expect(missing.status).toBe(404);
|
|
270
|
+
expect(await missing.json()).toMatchObject({ error: { code: 'not_found' } });
|
|
271
|
+
});
|
|
272
|
+
${e?`
|
|
273
|
+
it('[NOTE-004] lets admins delete and stops members', async () => {
|
|
274
|
+
const { id } = (await (await call(MEMBER(), 'POST', '/notes', { body: 'keep' })).json()) as { id: string };
|
|
275
|
+
const denied = await call(MEMBER(), 'DELETE', \`/notes/\${id}\`);
|
|
276
|
+
expect(denied.status).toBe(403);
|
|
277
|
+
expect(await denied.json()).toMatchObject({ error: { code: 'permission_required' } });
|
|
278
|
+
expect((await call(ADMIN(), 'DELETE', \`/notes/\${id}\`)).status).toBe(204);
|
|
279
|
+
expect(await (await call(ADMIN(), 'GET', '/notes')).json()).toEqual({ notes: [] });
|
|
280
|
+
});
|
|
281
|
+
`:""}});
|
|
282
|
+
`}o(Y,"appTest");function Z(e){return[{path:"frontend/vite.config.ts",contents:ee(e)},{path:"frontend/src/styles.css",contents:`@tailwind utilities;
|
|
283
|
+
`},{path:"frontend/src/vite-env.d.ts",contents:`/// <reference types="vite/client" />
|
|
284
|
+
`},{path:"frontend/src/index.tsx",contents:te()},{path:"frontend/src/app.tsx",contents:ne(e)},{path:"frontend/src/lib/route.ts",contents:oe(e)},{path:"frontend/src/lib/route.test.ts",contents:se(e)},{path:"frontend/src/i18n.test.ts",contents:ie()},{path:"frontend/src/components/navigation.tsx",contents:re(e)},{path:"frontend/src/screens/home.tsx",contents:e.withBackend?ce(e):ae(e)},...e.withBackend?[{path:"frontend/src/lib/api.ts",contents:ue()}]:[],...e.withAccess?[{path:"frontend/src/screens/settings.tsx",contents:de(e)}]:[]]}o(Z,"standardFrontendFiles");function ee({id:e}){return`import { readFile, readdir } from 'node:fs/promises';
|
|
285
|
+
import { resolve } from 'node:path';
|
|
286
|
+
|
|
287
|
+
import {
|
|
288
|
+
tegoPluginFrontend,
|
|
289
|
+
tegoPluginScopeRoot,
|
|
290
|
+
tegoPluginTailwind,
|
|
291
|
+
} from '@tego/plugin-sdk/vite';
|
|
292
|
+
import tailwindcss from 'tailwindcss';
|
|
293
|
+
|
|
294
|
+
const config = tegoPluginFrontend({
|
|
295
|
+
entry: 'frontend/src/index.tsx',
|
|
296
|
+
outDir: 'frontend/dist',
|
|
297
|
+
pluginId: '${e}',
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// The plugin's own utilities, compiled with the shell's design tokens and
|
|
301
|
+
// scoped under .${e}; tegoPluginScopeRoot makes them apply on portalled
|
|
302
|
+
// roots (dialogs, popovers) that carry the class themselves.
|
|
303
|
+
config.css = {
|
|
304
|
+
postcss: {
|
|
305
|
+
plugins: [
|
|
306
|
+
tailwindcss(
|
|
307
|
+
tegoPluginTailwind({
|
|
308
|
+
pluginId: '${e}',
|
|
309
|
+
root: import.meta.dirname,
|
|
310
|
+
content: [resolve(import.meta.dirname, 'src/**/*.{ts,tsx}')],
|
|
311
|
+
})
|
|
312
|
+
),
|
|
313
|
+
tegoPluginScopeRoot('${e}'),
|
|
314
|
+
],
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// The local override must serve translations from the same build as the UI.
|
|
319
|
+
config.plugins!.push({
|
|
320
|
+
name: '${e}-locales',
|
|
321
|
+
async buildStart() {
|
|
322
|
+
const directory = resolve(import.meta.dirname, 'i18n');
|
|
323
|
+
for (const name of await readdir(directory)) {
|
|
324
|
+
if (!name.endsWith('.json')) continue;
|
|
325
|
+
const file = resolve(directory, name);
|
|
326
|
+
this.addWatchFile(file);
|
|
327
|
+
this.emitFile({
|
|
328
|
+
type: 'asset',
|
|
329
|
+
fileName: \`i18n/\${name}\`,
|
|
330
|
+
source: await readFile(file, 'utf8'),
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
export default config;
|
|
336
|
+
`}o(ee,"viteConfig");function te(){return`import './styles.css';
|
|
337
|
+
|
|
338
|
+
import { definePluginFrontend } from '@tego/plugin-sdk/frontend';
|
|
339
|
+
import { createRoot } from 'react-dom/client';
|
|
340
|
+
|
|
341
|
+
import { App } from './app';
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* The frontend part's entry. React comes from the shell, but its providers do
|
|
345
|
+
* not cross into this root: App establishes everything the pages need.
|
|
346
|
+
*/
|
|
347
|
+
export default definePluginFrontend({
|
|
348
|
+
mount(el, ctx) {
|
|
349
|
+
const root = createRoot(el);
|
|
350
|
+
root.render(<App ctx={ctx} />);
|
|
351
|
+
return () => root.unmount();
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
`}o(te,"indexTsx");function ne({id:e,ns:t,withAccess:n}){const s=n?`import { AccessProvider, useAccess } from '@tego/plugin-access-ui';
|
|
355
|
+
`:"",a=n?`import { SettingsScreen } from './screens/settings';
|
|
356
|
+
`:"",i=n?`<AccessProvider api={ctx.api}>
|
|
357
|
+
<HostProvider value={ctx}>
|
|
358
|
+
<Shell />
|
|
359
|
+
</HostProvider>
|
|
360
|
+
</AccessProvider>`:`<HostProvider value={ctx}>
|
|
361
|
+
<Shell />
|
|
362
|
+
</HostProvider>`,r=n?`
|
|
363
|
+
const { t } = useTranslation();
|
|
364
|
+
const access = useAccess();
|
|
365
|
+
if (access.loading) return <Loading />;
|
|
366
|
+
// The catalog's \`menus\` decide who sees a page; the backend still checks every call.
|
|
367
|
+
if (route.page !== 'notFound' && !access.sees(route.page)) {
|
|
368
|
+
return <EmptyState icon={<Lock />} title={t('${t}.errors.permission_required')} />;
|
|
369
|
+
}`:"";return`import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
370
|
+
${s}import { pluginI18nNamespace } from '@tego/plugin-sdk';
|
|
371
|
+
import type { PluginHostContext } from '@tego/plugin-sdk/frontend';
|
|
372
|
+
import {
|
|
373
|
+
HostProvider,
|
|
374
|
+
PluginErrorBoundary,
|
|
375
|
+
usePluginRoute,
|
|
376
|
+
} from '@tego/plugin-sdk/react';
|
|
377
|
+
import { EmptyState${n?", Loading":""} } from '@tego/ui';
|
|
378
|
+
import { FileQuestion${n?", Lock":""}, TriangleAlert } from 'lucide-react';
|
|
379
|
+
import { useState } from 'react';
|
|
380
|
+
import { getI18n, I18nextProvider, useTranslation } from 'react-i18next';
|
|
381
|
+
|
|
382
|
+
import { Navigation } from './components/navigation';
|
|
383
|
+
import { parseRoute } from './lib/route';
|
|
384
|
+
import { HomeScreen } from './screens/home';
|
|
385
|
+
${a}
|
|
386
|
+
/**
|
|
387
|
+
* The plugin's root, in the order the shell's own providers would have been:
|
|
388
|
+
* translations first (the error screen needs them), then the error boundary
|
|
389
|
+
* (the shell's cannot see into this React root), then data and access.
|
|
390
|
+
*/
|
|
391
|
+
export function App({ ctx }: { ctx: PluginHostContext }) {
|
|
392
|
+
const [queryClient] = useState(
|
|
393
|
+
() => new QueryClient({ defaultOptions: { queries: { staleTime: 10_000 } } })
|
|
394
|
+
);
|
|
395
|
+
return (
|
|
396
|
+
<I18nextProvider i18n={getI18n()} defaultNS={pluginI18nNamespace(ctx.pluginId)}>
|
|
397
|
+
<PluginErrorBoundary fallback={(_error, reset) => <Crashed onRetry={reset} />}>
|
|
398
|
+
<QueryClientProvider client={queryClient}>
|
|
399
|
+
<div className='${e} flex h-full min-h-0 flex-col bg-background'>
|
|
400
|
+
${i}
|
|
401
|
+
</div>
|
|
402
|
+
</QueryClientProvider>
|
|
403
|
+
</PluginErrorBoundary>
|
|
404
|
+
</I18nextProvider>
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function Shell() {
|
|
409
|
+
const route = parseRoute(usePluginRoute().path);${r}
|
|
410
|
+
return (
|
|
411
|
+
<>
|
|
412
|
+
<Navigation current={route.page} />
|
|
413
|
+
<main className='min-h-0 flex-1 overflow-auto p-6'>
|
|
414
|
+
{${n?"route.page === 'home' ? <HomeScreen /> : route.page === 'settings' ? <SettingsScreen /> : <NotFound />":"route.page === 'home' ? <HomeScreen /> : <NotFound />"}}
|
|
415
|
+
</main>
|
|
416
|
+
</>
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function NotFound() {
|
|
421
|
+
const { t } = useTranslation();
|
|
422
|
+
return <EmptyState icon={<FileQuestion />} title={t('${t}.errors.not_found')} />;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function Crashed({ onRetry }: { onRetry: () => void }) {
|
|
426
|
+
const { t } = useTranslation();
|
|
427
|
+
return (
|
|
428
|
+
<EmptyState
|
|
429
|
+
icon={<TriangleAlert />}
|
|
430
|
+
title={t('${t}.crashed.title')}
|
|
431
|
+
description={t('${t}.crashed.description')}
|
|
432
|
+
action={{ label: t('${t}.crashed.retry'), onClick: onRetry }}
|
|
433
|
+
/>
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
`}o(ne,"appTsx");function oe({withAccess:e}){return`/**
|
|
437
|
+
* The in-plugin path (below /p/<id>) as a page. Pure, so it is tested without
|
|
438
|
+
* a browser; the nav keys in tego.plugin.json use the same names.
|
|
439
|
+
*/
|
|
440
|
+
export type Route = { page: ${e?"'home' | 'settings'":"'home'"} } | { page: 'notFound' };
|
|
441
|
+
|
|
442
|
+
export function parseRoute(path: string): Route {
|
|
443
|
+
const segment = path.split('/').filter(Boolean)[0] ?? 'home';
|
|
444
|
+
if (segment === 'home') return { page: 'home' };${e?`
|
|
445
|
+
if (segment === 'settings') return { page: 'settings' };`:""}
|
|
446
|
+
return { page: 'notFound' };
|
|
447
|
+
}
|
|
448
|
+
`}o(oe,"routeTs");function se({withAccess:e}){return`import { describe, expect, it } from 'vitest';
|
|
449
|
+
|
|
450
|
+
import { parseRoute } from './route';
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Business Cases \u2014 \u63D2\u4EF6\u5185\u8DEF\u7531
|
|
454
|
+
*
|
|
455
|
+
* ROUTE-001 \u6839\u8DEF\u5F84\u4E0E /home \u90FD\u662F\u9996\u9875
|
|
456
|
+
* ROUTE-002 ${e?"/settings \u662F\u8BBE\u7F6E\u9875\uFF1B":""}\u672A\u77E5\u8DEF\u5F84\u662F notFound\uFF0C\u4E0D\u4F1A\u843D\u5230\u9996\u9875
|
|
457
|
+
*/
|
|
458
|
+
describe('parseRoute', () => {
|
|
459
|
+
it('[ROUTE-001] opens the home page at the root', () => {
|
|
460
|
+
expect(parseRoute('/')).toEqual({ page: 'home' });
|
|
461
|
+
expect(parseRoute('/home')).toEqual({ page: 'home' });
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('[ROUTE-002] knows its pages and nothing else', () => {
|
|
465
|
+
${e?` expect(parseRoute('/settings')).toEqual({ page: 'settings' });
|
|
466
|
+
`:""} expect(parseRoute('/elsewhere')).toEqual({ page: 'notFound' });
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
`}o(se,"routeTest");function ie(){return`import { fileURLToPath } from 'node:url';
|
|
470
|
+
|
|
471
|
+
import { assertLocaleParity } from '@tego/plugin-sdk/testing';
|
|
472
|
+
import { describe, it } from 'vitest';
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Business Cases \u2014 \u591A\u8BED\u8A00
|
|
476
|
+
*
|
|
477
|
+
* I18N-001 \u5341\u79CD\u8BED\u8A00\u9F50\u5168\uFF0C\u952E\u4E0E zh-CN \u4E00\u81F4\uFF0C\u5360\u4F4D\u7B26\u4E00\u81F4\uFF0C\u6CA1\u6709\u7A7A\u503C\uFF08\u4E0A\u4F20\u4F1A\u62D2\u6536\u4E0D\u4E00\u81F4\u7684\u5305\uFF09
|
|
478
|
+
*/
|
|
479
|
+
describe('locales', () => {
|
|
480
|
+
it('[I18N-001] ships complete, consistent translations', () =>
|
|
481
|
+
assertLocaleParity(fileURLToPath(new URL('../i18n', import.meta.url))));
|
|
482
|
+
});
|
|
483
|
+
`}o(ie,"i18nTest");function re({ns:e,withAccess:t}){return`${t?`import { useAccess } from '@tego/plugin-access-ui';
|
|
484
|
+
`:""}import { useHost } from '@tego/plugin-sdk/react';
|
|
485
|
+
import { Button } from '@tego/ui';
|
|
486
|
+
import { useTranslation } from 'react-i18next';
|
|
487
|
+
|
|
488
|
+
import type { Route } from '../lib/route';
|
|
489
|
+
|
|
490
|
+
const PAGES = [
|
|
491
|
+
{ key: 'home', labelKey: '${e}.nav.home' },${t?`
|
|
492
|
+
{ key: 'settings', labelKey: '${e}.nav.settings' },`:""}
|
|
493
|
+
] as const;
|
|
494
|
+
|
|
495
|
+
export function Navigation({ current }: { current: Route['page'] }) {
|
|
496
|
+
const { t } = useTranslation();
|
|
497
|
+
const host = useHost();${t?`
|
|
498
|
+
const access = useAccess();`:""}
|
|
499
|
+
return (
|
|
500
|
+
<nav className='flex shrink-0 items-center gap-1 border-b border-border px-4 py-2'>
|
|
501
|
+
{PAGES${t?".filter(page => access.sees(page.key))":""}.map(page => (
|
|
502
|
+
<Button
|
|
503
|
+
key={page.key}
|
|
504
|
+
size='sm'
|
|
505
|
+
variant={current === page.key ? 'secondary' : 'ghost'}
|
|
506
|
+
onClick={() => host.navigate('/' + page.key)}
|
|
507
|
+
>
|
|
508
|
+
{t(page.labelKey)}
|
|
509
|
+
</Button>
|
|
510
|
+
))}
|
|
511
|
+
</nav>
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
`}o(re,"navigationTsx");function ae({ns:e}){return`import { EmptyState } from '@tego/ui';
|
|
515
|
+
import { Puzzle } from 'lucide-react';
|
|
516
|
+
import { useTranslation } from 'react-i18next';
|
|
517
|
+
|
|
518
|
+
export function HomeScreen() {
|
|
519
|
+
const { t } = useTranslation();
|
|
520
|
+
return (
|
|
521
|
+
<EmptyState
|
|
522
|
+
icon={<Puzzle />}
|
|
523
|
+
title={t('${e}.home.title')}
|
|
524
|
+
description={t('${e}.home.description')}
|
|
525
|
+
/>
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
`}o(ae,"staticHomeTsx");function ue(){return`import { createApiClient, useHost } from '@tego/plugin-sdk/react';
|
|
529
|
+
import { useMemo } from 'react';
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* The backend, through \`ctx.api\` (the shell adds the credentials). Errors are
|
|
533
|
+
* PluginApiError with the backend's \`code\`, translated by \`describeApiError\`.
|
|
534
|
+
*/
|
|
535
|
+
export function useApi() {
|
|
536
|
+
const host = useHost();
|
|
537
|
+
return useMemo(() => createApiClient(host.api), [host]);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export interface Note {
|
|
541
|
+
id: string;
|
|
542
|
+
body: string;
|
|
543
|
+
}
|
|
544
|
+
`}o(ue,"apiTs");function ce({ns:e,withAccess:t}){return`import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
545
|
+
${t?`import { Can } from '@tego/plugin-access-ui';
|
|
546
|
+
`:""}import { describeApiError, useLiveUpdates } from '@tego/plugin-sdk/react';
|
|
547
|
+
import { Button, EmptyState, Input, Loading } from '@tego/ui';
|
|
548
|
+
import { NotebookPen, Trash2 } from 'lucide-react';
|
|
549
|
+
import { useState } from 'react';
|
|
550
|
+
import { useTranslation } from 'react-i18next';
|
|
551
|
+
|
|
552
|
+
import { useApi, type Note } from '../lib/api';
|
|
553
|
+
|
|
554
|
+
const NOTES = ['${e}', 'notes'] as const;
|
|
555
|
+
|
|
556
|
+
export function HomeScreen() {
|
|
557
|
+
const { t } = useTranslation();
|
|
558
|
+
const api = useApi();
|
|
559
|
+
const queryClient = useQueryClient();
|
|
560
|
+
const [draft, setDraft] = useState('');
|
|
561
|
+
|
|
562
|
+
const notes = useQuery({
|
|
563
|
+
queryKey: NOTES,
|
|
564
|
+
queryFn: () => api.get<{ notes: Note[] }>('/notes'),
|
|
565
|
+
});
|
|
566
|
+
// Other people's (and employees') writes arrive as events from the backend.
|
|
567
|
+
useLiveUpdates(() => void queryClient.invalidateQueries({ queryKey: NOTES }));
|
|
568
|
+
|
|
569
|
+
const add = useMutation({
|
|
570
|
+
mutationFn: (body: string) => api.post<{ id: string }>('/notes', { body }),
|
|
571
|
+
onSuccess: () => {
|
|
572
|
+
setDraft('');
|
|
573
|
+
void queryClient.invalidateQueries({ queryKey: NOTES });
|
|
574
|
+
},
|
|
575
|
+
});
|
|
576
|
+
const remove = useMutation({
|
|
577
|
+
mutationFn: (id: string) => api.delete('/notes/' + encodeURIComponent(id)),
|
|
578
|
+
onSuccess: () => void queryClient.invalidateQueries({ queryKey: NOTES }),
|
|
579
|
+
});
|
|
580
|
+
const failure = add.error ?? remove.error ?? notes.error;
|
|
581
|
+
const errorText = (error: unknown) => {
|
|
582
|
+
const { key, message } = describeApiError(error, '${e}');
|
|
583
|
+
return key ? t(key, { defaultValue: message }) : message;
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
return (
|
|
587
|
+
<div className='mx-auto flex max-w-2xl flex-col gap-4'>
|
|
588
|
+
<form
|
|
589
|
+
className='flex gap-2'
|
|
590
|
+
onSubmit={event => {
|
|
591
|
+
event.preventDefault();
|
|
592
|
+
if (draft.trim()) add.mutate(draft.trim());
|
|
593
|
+
}}
|
|
594
|
+
>
|
|
595
|
+
<Input
|
|
596
|
+
value={draft}
|
|
597
|
+
placeholder={t('${e}.home.placeholder')}
|
|
598
|
+
onChange={event => setDraft(event.target.value)}
|
|
599
|
+
/>
|
|
600
|
+
<Button type='submit' disabled={add.isPending || !draft.trim()}>
|
|
601
|
+
{t('${e}.home.add')}
|
|
602
|
+
</Button>
|
|
603
|
+
</form>
|
|
604
|
+
{failure && (
|
|
605
|
+
<p role='alert' className='text-sm text-destructive'>
|
|
606
|
+
{errorText(failure)}
|
|
607
|
+
</p>
|
|
608
|
+
)}
|
|
609
|
+
{notes.isLoading ? (
|
|
610
|
+
<Loading />
|
|
611
|
+
) : notes.data?.notes.length ? (
|
|
612
|
+
<ul className='flex flex-col divide-y divide-border rounded-card border border-border bg-card'>
|
|
613
|
+
{notes.data.notes.map(note => (
|
|
614
|
+
<li key={note.id} className='flex items-center gap-2 px-4 py-3 text-sm'>
|
|
615
|
+
<span className='min-w-0 flex-1 break-words'>{note.body}</span>
|
|
616
|
+
${t?`<Can permission='note.delete'>
|
|
617
|
+
<Button
|
|
618
|
+
size='icon'
|
|
619
|
+
variant='ghost'
|
|
620
|
+
aria-label={t('${e}.home.delete')}
|
|
621
|
+
onClick={() => remove.mutate(note.id)}
|
|
622
|
+
>
|
|
623
|
+
<Trash2 />
|
|
624
|
+
</Button>
|
|
625
|
+
</Can>`:`<Button
|
|
626
|
+
size='icon'
|
|
627
|
+
variant='ghost'
|
|
628
|
+
aria-label={t('${e}.home.delete')}
|
|
629
|
+
onClick={() => remove.mutate(note.id)}
|
|
630
|
+
>
|
|
631
|
+
<Trash2 />
|
|
632
|
+
</Button>`}
|
|
633
|
+
</li>
|
|
634
|
+
))}
|
|
635
|
+
</ul>
|
|
636
|
+
) : (
|
|
637
|
+
<EmptyState icon={<NotebookPen />} title={t('${e}.home.empty')} size='sm' />
|
|
638
|
+
)}
|
|
639
|
+
</div>
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
`}o(ce,"notesHomeTsx");function de({ns:e}){return`import { AccessSettings } from '@tego/plugin-access-ui';
|
|
643
|
+
import { useTranslation } from 'react-i18next';
|
|
644
|
+
|
|
645
|
+
/** Who may do what in this plugin; the access UI shows each person what they may manage. */
|
|
646
|
+
export function SettingsScreen() {
|
|
647
|
+
const { t } = useTranslation();
|
|
648
|
+
return (
|
|
649
|
+
<section className='mx-auto flex max-w-4xl flex-col gap-4'>
|
|
650
|
+
<h1 className='text-lg font-semibold'>{t('${e}.settings.access')}</h1>
|
|
651
|
+
<AccessSettings />
|
|
652
|
+
</section>
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
`}o(de,"settingsTsx");function le(e){return{$schema:"../node_modules/@tego/plugin-sdk/access-catalog.schema.json",version:1,groups:[{key:"notes",labelKey:`${e}.access.group.notes`}],scopes:{},permissions:{"note.view":{group:"notes",labelKey:`${e}.access.permission.view`},"note.create":{group:"notes",labelKey:`${e}.access.permission.create`,implies:["note.view"]},"note.delete":{group:"notes",labelKey:`${e}.access.permission.delete`,implies:["note.view"]}},menus:{home:"note.view"},roles:{admin:{nameKey:`${e}.access.role.admin`,descriptionKey:`${e}.access.role.adminDescription`,grants:{"note.view":!0,"note.create":!0,"note.delete":!0,"access.manage":!0}},member:{nameKey:`${e}.access.role.member`,descriptionKey:`${e}.access.role.memberDescription`,grants:{"note.view":!0,"note.create":!0}}},hostRoleBindings:{admin:["admin"],super_admin:["admin"]},defaults:{unassigned:"member"},assignment:{principals:["user","unit"]}}}o(le,"standardAccessCatalog");var pe={nav:{settings:"\u8BBE\u7F6E"},home:{placeholder:"\u5199\u4E00\u6761\u7B14\u8BB0",add:"\u6DFB\u52A0",empty:"\u8FD8\u6CA1\u6709\u7B14\u8BB0",delete:"\u5220\u9664"},settings:{title:"\u8BBE\u7F6E",access:"\u6743\u9650"},crashed:{title:"\u9875\u9762\u51FA\u9519\u4E86",description:"\u8FD9\u4E00\u9875\u6CA1\u80FD\u663E\u793A\u51FA\u6765\u3002\u4F60\u53EF\u4EE5\u91CD\u8BD5\uFF0C\u95EE\u9898\u6301\u7EED\u65F6\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u3002",retry:"\u91CD\u8BD5"},errors:{invalid_json:"\u8BF7\u6C42\u683C\u5F0F\u4E0D\u6B63\u786E\u3002",invalid_body:"\u586B\u5199\u7684\u5185\u5BB9\u4E0D\u5B8C\u6574\u6216\u683C\u5F0F\u4E0D\u5BF9\u3002",permission_required:"\u4F60\u6CA1\u6709\u6267\u884C\u8FD9\u4E2A\u64CD\u4F5C\u7684\u6743\u9650\u3002",not_found:"\u6CA1\u6709\u627E\u5230\uFF0C\u6216\u4F60\u65E0\u6743\u67E5\u770B\u3002",no_route:"\u8FD9\u4E2A\u529F\u80FD\u6682\u4E0D\u53EF\u7528\u3002",rate_limited:"\u64CD\u4F5C\u592A\u9891\u7E41\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002",internal:"\u64CD\u4F5C\u6CA1\u6709\u5B8C\u6210\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002"},access:{group:{notes:"\u7B14\u8BB0"},permission:{view:"\u67E5\u770B\u7B14\u8BB0",create:"\u6DFB\u52A0\u7B14\u8BB0",delete:"\u5220\u9664\u7B14\u8BB0"},role:{admin:"\u7BA1\u7406\u5458",adminDescription:"\u7BA1\u7406\u5168\u90E8\u7B14\u8BB0\u4E0E\u6743\u9650",member:"\u6210\u5458",memberDescription:"\u67E5\u770B\u4E0E\u6DFB\u52A0\u7B14\u8BB0"}}},me={nav:{settings:"\u8A2D\u5B9A"},home:{placeholder:"\u5BEB\u4E00\u5247\u7B46\u8A18",add:"\u65B0\u589E",empty:"\u9084\u6C92\u6709\u7B46\u8A18",delete:"\u522A\u9664"},settings:{title:"\u8A2D\u5B9A",access:"\u6B0A\u9650"},crashed:{title:"\u9801\u9762\u51FA\u932F\u4E86",description:"\u9019\u4E00\u9801\u6C92\u80FD\u986F\u793A\u51FA\u4F86\u3002\u4F60\u53EF\u4EE5\u91CD\u8A66\uFF0C\u554F\u984C\u6301\u7E8C\u6642\u8ACB\u806F\u7D61\u7BA1\u7406\u54E1\u3002",retry:"\u91CD\u8A66"},errors:{invalid_json:"\u8ACB\u6C42\u683C\u5F0F\u4E0D\u6B63\u78BA\u3002",invalid_body:"\u586B\u5BEB\u7684\u5167\u5BB9\u4E0D\u5B8C\u6574\u6216\u683C\u5F0F\u4E0D\u5C0D\u3002",permission_required:"\u4F60\u6C92\u6709\u57F7\u884C\u9019\u500B\u64CD\u4F5C\u7684\u6B0A\u9650\u3002",not_found:"\u6C92\u6709\u627E\u5230\uFF0C\u6216\u4F60\u7121\u6B0A\u67E5\u770B\u3002",no_route:"\u9019\u500B\u529F\u80FD\u66AB\u4E0D\u53EF\u7528\u3002",rate_limited:"\u64CD\u4F5C\u592A\u983B\u7E41\uFF0C\u8ACB\u7A0D\u5F8C\u518D\u8A66\u3002",internal:"\u64CD\u4F5C\u6C92\u6709\u5B8C\u6210\uFF0C\u8ACB\u7A0D\u5F8C\u91CD\u8A66\u3002"},access:{group:{notes:"\u7B46\u8A18"},permission:{view:"\u67E5\u770B\u7B46\u8A18",create:"\u65B0\u589E\u7B46\u8A18",delete:"\u522A\u9664\u7B46\u8A18"},role:{admin:"\u7BA1\u7406\u54E1",adminDescription:"\u7BA1\u7406\u5168\u90E8\u7B46\u8A18\u8207\u6B0A\u9650",member:"\u6210\u54E1",memberDescription:"\u67E5\u770B\u8207\u65B0\u589E\u7B46\u8A18"}}},T={nav:{settings:"Settings"},home:{placeholder:"Write a note",add:"Add",empty:"No notes yet",delete:"Delete"},settings:{title:"Settings",access:"Access"},crashed:{title:"Something went wrong",description:"This page couldn't be shown. Try again, or contact your admin if it keeps happening.",retry:"Try again"},errors:{invalid_json:"The request was malformed.",invalid_body:"Some fields are missing or invalid.",permission_required:"You don't have permission to do that.",not_found:"Not found, or you can't see it.",no_route:"This feature isn't available.",rate_limited:"Too many requests; try again shortly.",internal:"That didn't go through. Try again later."},access:{group:{notes:"Notes"},permission:{view:"View notes",create:"Add notes",delete:"Delete notes"},role:{admin:"Admin",adminDescription:"Manages all notes and access",member:"Member",memberDescription:"Views and adds notes"}}},ge={nav:{settings:"\u8A2D\u5B9A"},home:{placeholder:"\u30E1\u30E2\u3092\u66F8\u304F",add:"\u8FFD\u52A0",empty:"\u30E1\u30E2\u306F\u307E\u3060\u3042\u308A\u307E\u305B\u3093",delete:"\u524A\u9664"},settings:{title:"\u8A2D\u5B9A",access:"\u6A29\u9650"},crashed:{title:"\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F",description:"\u3053\u306E\u30DA\u30FC\u30B8\u3092\u8868\u793A\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002\u518D\u8A66\u884C\u3057\u3001\u89E3\u6C7A\u3057\u306A\u3044\u5834\u5408\u306F\u7BA1\u7406\u8005\u306B\u9023\u7D61\u3057\u3066\u304F\u3060\u3055\u3044\u3002",retry:"\u518D\u8A66\u884C"},errors:{invalid_json:"\u30EA\u30AF\u30A8\u30B9\u30C8\u306E\u5F62\u5F0F\u304C\u6B63\u3057\u304F\u3042\u308A\u307E\u305B\u3093\u3002",invalid_body:"\u5165\u529B\u5185\u5BB9\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u308B\u304B\u3001\u5F62\u5F0F\u304C\u6B63\u3057\u304F\u3042\u308A\u307E\u305B\u3093\u3002",permission_required:"\u3053\u306E\u64CD\u4F5C\u3092\u884C\u3046\u6A29\u9650\u304C\u3042\u308A\u307E\u305B\u3093\u3002",not_found:"\u898B\u3064\u304B\u3089\u306A\u3044\u304B\u3001\u8868\u793A\u3059\u308B\u6A29\u9650\u304C\u3042\u308A\u307E\u305B\u3093\u3002",no_route:"\u3053\u306E\u6A5F\u80FD\u306F\u5229\u7528\u3067\u304D\u307E\u305B\u3093\u3002",rate_limited:"\u30EA\u30AF\u30A8\u30B9\u30C8\u304C\u591A\u3059\u304E\u307E\u3059\u3002\u3057\u3070\u3089\u304F\u3057\u3066\u304B\u3089\u518D\u8A66\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002",internal:"\u51E6\u7406\u3092\u5B8C\u4E86\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002\u5F8C\u3067\u518D\u8A66\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"},access:{group:{notes:"\u30E1\u30E2"},permission:{view:"\u30E1\u30E2\u3092\u8868\u793A",create:"\u30E1\u30E2\u3092\u8FFD\u52A0",delete:"\u30E1\u30E2\u3092\u524A\u9664"},role:{admin:"\u7BA1\u7406\u8005",adminDescription:"\u3059\u3079\u3066\u306E\u30E1\u30E2\u3068\u6A29\u9650\u3092\u7BA1\u7406",member:"\u30E1\u30F3\u30D0\u30FC",memberDescription:"\u30E1\u30E2\u306E\u8868\u793A\u3068\u8FFD\u52A0"}}},he={nav:{settings:"\uC124\uC815"},home:{placeholder:"\uBA54\uBAA8 \uC791\uC131",add:"\uCD94\uAC00",empty:"\uC544\uC9C1 \uBA54\uBAA8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4",delete:"\uC0AD\uC81C"},settings:{title:"\uC124\uC815",access:"\uAD8C\uD55C"},crashed:{title:"\uBB38\uC81C\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4",description:"\uC774 \uD398\uC774\uC9C0\uB97C \uD45C\uC2DC\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uACE0, \uACC4\uC18D\uB418\uBA74 \uAD00\uB9AC\uC790\uC5D0\uAC8C \uBB38\uC758\uD558\uC138\uC694.",retry:"\uB2E4\uC2DC \uC2DC\uB3C4"},errors:{invalid_json:"\uC694\uCCAD \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",invalid_body:"\uC785\uB825 \uB0B4\uC6A9\uC774 \uB204\uB77D\uB418\uC5C8\uAC70\uB098 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",permission_required:"\uC774 \uC791\uC5C5\uC744 \uC218\uD589\uD560 \uAD8C\uD55C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",not_found:"\uCC3E\uC744 \uC218 \uC5C6\uAC70\uB098 \uBCFC \uAD8C\uD55C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",no_route:"\uC774 \uAE30\uB2A5\uC740 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",rate_limited:"\uC694\uCCAD\uC774 \uB108\uBB34 \uB9CE\uC2B5\uB2C8\uB2E4. \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.",internal:"\uC791\uC5C5\uC744 \uC644\uB8CC\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. \uB098\uC911\uC5D0 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694."},access:{group:{notes:"\uBA54\uBAA8"},permission:{view:"\uBA54\uBAA8 \uBCF4\uAE30",create:"\uBA54\uBAA8 \uCD94\uAC00",delete:"\uBA54\uBAA8 \uC0AD\uC81C"},role:{admin:"\uAD00\uB9AC\uC790",adminDescription:"\uBAA8\uB4E0 \uBA54\uBAA8\uC640 \uAD8C\uD55C \uAD00\uB9AC",member:"\uBA64\uBC84",memberDescription:"\uBA54\uBAA8 \uBCF4\uAE30 \uBC0F \uCD94\uAC00"}}},fe={nav:{settings:"Einstellungen"},home:{placeholder:"Notiz schreiben",add:"Hinzuf\xFCgen",empty:"Noch keine Notizen",delete:"L\xF6schen"},settings:{title:"Einstellungen",access:"Zugriff"},crashed:{title:"Etwas ist schiefgelaufen",description:"Diese Seite konnte nicht angezeigt werden. Versuche es erneut oder wende dich an deinen Admin.",retry:"Erneut versuchen"},errors:{invalid_json:"Die Anfrage war fehlerhaft.",invalid_body:"Einige Felder fehlen oder sind ung\xFCltig.",permission_required:"Dazu fehlt dir die Berechtigung.",not_found:"Nicht gefunden oder nicht sichtbar f\xFCr dich.",no_route:"Diese Funktion ist nicht verf\xFCgbar.",rate_limited:"Zu viele Anfragen; versuche es gleich noch einmal.",internal:"Das hat nicht geklappt. Versuche es sp\xE4ter erneut."},access:{group:{notes:"Notizen"},permission:{view:"Notizen ansehen",create:"Notizen hinzuf\xFCgen",delete:"Notizen l\xF6schen"},role:{admin:"Admin",adminDescription:"Verwaltet alle Notizen und Zugriffe",member:"Mitglied",memberDescription:"Sieht und erstellt Notizen"}}},Ee={nav:{settings:"Ajustes"},home:{placeholder:"Escribe una nota",add:"A\xF1adir",empty:"A\xFAn no hay notas",delete:"Eliminar"},settings:{title:"Ajustes",access:"Acceso"},crashed:{title:"Algo sali\xF3 mal",description:"No se pudo mostrar esta p\xE1gina. Int\xE9ntalo de nuevo o contacta con tu administrador.",retry:"Reintentar"},errors:{invalid_json:"La solicitud no tiene un formato v\xE1lido.",invalid_body:"Faltan campos o no son v\xE1lidos.",permission_required:"No tienes permiso para hacer eso.",not_found:"No se encontr\xF3 o no puedes verlo.",no_route:"Esta funci\xF3n no est\xE1 disponible.",rate_limited:"Demasiadas solicitudes; int\xE9ntalo en un momento.",internal:"No se pudo completar. Int\xE9ntalo m\xE1s tarde."},access:{group:{notes:"Notas"},permission:{view:"Ver notas",create:"A\xF1adir notas",delete:"Eliminar notas"},role:{admin:"Administrador",adminDescription:"Gestiona todas las notas y el acceso",member:"Miembro",memberDescription:"Ve y a\xF1ade notas"}}},Ce={nav:{settings:"Param\xE8tres"},home:{placeholder:"\xC9crire une note",add:"Ajouter",empty:"Aucune note pour le moment",delete:"Supprimer"},settings:{title:"Param\xE8tres",access:"Acc\xE8s"},crashed:{title:"Un probl\xE8me est survenu",description:"Cette page n'a pas pu s'afficher. R\xE9essayez ou contactez votre administrateur.",retry:"R\xE9essayer"},errors:{invalid_json:"La requ\xEAte n'est pas valide.",invalid_body:"Des champs sont manquants ou invalides.",permission_required:"Vous n'avez pas l'autorisation de faire cela.",not_found:"Introuvable, ou vous ne pouvez pas le voir.",no_route:"Cette fonctionnalit\xE9 n'est pas disponible.",rate_limited:"Trop de requ\xEAtes ; r\xE9essayez dans un instant.",internal:"L'op\xE9ration n'a pas abouti. R\xE9essayez plus tard."},access:{group:{notes:"Notes"},permission:{view:"Voir les notes",create:"Ajouter des notes",delete:"Supprimer des notes"},role:{admin:"Administrateur",adminDescription:"G\xE8re toutes les notes et les acc\xE8s",member:"Membre",memberDescription:"Voit et ajoute des notes"}}},ye={nav:{settings:"Impostazioni"},home:{placeholder:"Scrivi una nota",add:"Aggiungi",empty:"Ancora nessuna nota",delete:"Elimina"},settings:{title:"Impostazioni",access:"Accesso"},crashed:{title:"Qualcosa \xE8 andato storto",description:"Impossibile mostrare questa pagina. Riprova o contatta l'amministratore.",retry:"Riprova"},errors:{invalid_json:"La richiesta non \xE8 valida.",invalid_body:"Alcuni campi mancano o non sono validi.",permission_required:"Non hai il permesso per farlo.",not_found:"Non trovato, o non puoi vederlo.",no_route:"Questa funzione non \xE8 disponibile.",rate_limited:"Troppe richieste; riprova tra poco.",internal:"Operazione non riuscita. Riprova pi\xF9 tardi."},access:{group:{notes:"Note"},permission:{view:"Vedere le note",create:"Aggiungere note",delete:"Eliminare note"},role:{admin:"Amministratore",adminDescription:"Gestisce tutte le note e gli accessi",member:"Membro",memberDescription:"Vede e aggiunge note"}}},be={nav:{settings:"Configura\xE7\xF5es"},home:{placeholder:"Escreva uma nota",add:"Adicionar",empty:"Ainda n\xE3o h\xE1 notas",delete:"Excluir"},settings:{title:"Configura\xE7\xF5es",access:"Acesso"},crashed:{title:"Algo deu errado",description:"N\xE3o foi poss\xEDvel mostrar esta p\xE1gina. Tente novamente ou fale com o administrador.",retry:"Tentar novamente"},errors:{invalid_json:"A solicita\xE7\xE3o est\xE1 malformada.",invalid_body:"Alguns campos est\xE3o faltando ou s\xE3o inv\xE1lidos.",permission_required:"Voc\xEA n\xE3o tem permiss\xE3o para fazer isso.",not_found:"N\xE3o encontrado, ou voc\xEA n\xE3o pode v\xEA-lo.",no_route:"Este recurso n\xE3o est\xE1 dispon\xEDvel.",rate_limited:"Muitas solicita\xE7\xF5es; tente novamente em instantes.",internal:"N\xE3o foi poss\xEDvel concluir. Tente novamente mais tarde."},access:{group:{notes:"Notas"},permission:{view:"Ver notas",create:"Adicionar notas",delete:"Excluir notas"},role:{admin:"Administrador",adminDescription:"Gerencia todas as notas e acessos",member:"Membro",memberDescription:"V\xEA e adiciona notas"}}},Ae={"zh-CN":pe,"zh-TW":me,en:T,ja:ge,ko:he,de:fe,es:Ee,fr:Ce,it:ye,pt:be};function Be(e,t){const n=Ae[e]??T,{nav:s,settings:a,access:i,...r}=n;return t?{...r,nav:s,settings:a,access:i}:r}o(Be,"standardStrings");var f=["standard","minimal"];function p(e){return e.template==="standard"&&e.parts.includes("frontend")&&e.parts.includes("backend")}o(p,"standardAccess");var N={"@tego/plugin-access-ui":"@zhama/tego-plugin-access-ui","@tego/plugin-sdk":"@zhama/tego-plugin-sdk","@tego/ui":"@zhama/tego-ui"};function E(e,t,n){if(n==="workspace")return"workspace:*";const s=N[e];if(!s)throw new Error(`${e} is not a published SDK package`);return`npm:${s}@${t.hostApiRange}`}o(E,"sdkSpec");var C={semi:!0,trailingComma:"es5",singleQuote:!0,printWidth:80,tabWidth:2,useTabs:!1,bracketSpacing:!0,bracketSameLine:!1,arrowParens:"avoid",endOfLine:"lf",quoteProps:"as-needed",jsxSingleQuote:!0,proseWrap:"preserve"},ve=`node_modules/
|
|
2
656
|
build/
|
|
3
657
|
dist/
|
|
4
658
|
frontend/dist/
|
|
@@ -6,9 +660,10 @@ backend/dist/
|
|
|
6
660
|
claw/dist/
|
|
7
661
|
coverage/
|
|
8
662
|
.turbo/
|
|
9
|
-
|
|
663
|
+
.tego-dev/
|
|
664
|
+
`,xe=`allowBuilds:
|
|
10
665
|
esbuild: true
|
|
11
|
-
`,
|
|
666
|
+
`,D={"tsconfig.base.json":{$schema:"https://json.schemastore.org/tsconfig",compilerOptions:{strict:!0,useUnknownInCatchVariables:!0,allowJs:!0,skipLibCheck:!0,forceConsistentCasingInFileNames:!0,resolveJsonModule:!0,isolatedModules:!0,verbatimModuleSyntax:!0,declaration:!0,declarationMap:!0,esModuleInterop:!0,incremental:!1,noUncheckedIndexedAccess:!0,moduleDetection:"force",exactOptionalPropertyTypes:!0,target:"ES2022",lib:["ES2022","DOM","DOM.Iterable"],module:"ESNext",moduleResolution:"bundler"},exclude:["node_modules"]},"tsconfig.frontend.json":{$schema:"https://json.schemastore.org/tsconfig",extends:"./tsconfig.base.json",compilerOptions:{target:"ES2020",lib:["ES2020","DOM","DOM.Iterable"],module:"ESNext",moduleResolution:"bundler",jsx:"react-jsx",noEmit:!1,declaration:!0,declarationMap:!0,outDir:"dist"}},"tsconfig.backend.json":{$schema:"https://json.schemastore.org/tsconfig",extends:"./tsconfig.base.json",compilerOptions:{target:"ES2022",lib:["ES2022"],module:"ESNext",moduleResolution:"bundler",types:["node"],noEmit:!1,declaration:!0,declarationMap:!0,outDir:"dist"}}};function g(e,t){return t==="workspace"?`@tego/tsconfig/plugin-${e}.json`:`../tsconfig.${e}.json`}o(g,"tsconfigPreset");var Fe=`import js from '@eslint/js';
|
|
12
667
|
import prettierConfig from 'eslint-config-prettier';
|
|
13
668
|
import prettier from 'eslint-plugin-prettier';
|
|
14
669
|
import reactHooks from 'eslint-plugin-react-hooks';
|
|
@@ -40,8 +695,8 @@ export default tseslint.config(
|
|
|
40
695
|
},
|
|
41
696
|
prettierConfig
|
|
42
697
|
);
|
|
43
|
-
`,
|
|
44
|
-
`}
|
|
698
|
+
`,Te={"@eslint/js":"^9.16.0","eslint-config-prettier":"^10.1.8","eslint-plugin-prettier":"^5.5.4","eslint-plugin-react-hooks":"^5.2.0",globals:"^15.13.0","typescript-eslint":"^8.17.0"};function Ne(){return[{path:".prettierrc.json",contents:u(C)},{path:".gitignore",contents:ve},{path:"pnpm-workspace.yaml",contents:xe},...Object.entries(D).map(([e,t])=>({path:e,contents:u(t)}))]}o(Ne,"standaloneRootFiles");var De={"@typescript/native-preview":"7.0.0-dev.20260519.1",eslint:"^9.39.1",prettier:"^3.7.4",typescript:"^5.9.3"},_e=["zh-CN","zh-TW","en","ja","ko","de","es","fr","it","pt"];function y(e){return`@tego/plugin-${e.replace(/^tego-/,"")}`}o(y,"packageNameFor");function b(e){return e.replace(/^tego-/,"")}o(b,"keyNamespaceFor");function u(e){return`${JSON.stringify(e,null,2)}
|
|
699
|
+
`}o(u,"json");function ke(e){const t=b(e.id),n=e.parts.includes("frontend"),s=e.parts.includes("backend"),a=e.parts.includes("claw"),i={};n&&(i.frontend={icon:"puzzle",nav:[{key:"home",labelKey:`${t}.nav.home`,icon:"layout-dashboard",order:0,path:"/home"}]}),s&&(i.backend={limits:{memoryMb:256,requestTimeoutMs:15e3}}),a&&(i.claw={hooks:{}});const r={manifestVersion:1,id:e.id,version:"0.1.0",displayName:{"zh-CN":e.displayNameZh,en:e.displayNameEn},description:{"zh-CN":e.descriptionZh,en:e.descriptionEn},author:"Tego"};return(n||s)&&(r.hostApiRange=e.hostApiRange),r.scope=e.scope,s&&(r.declares={database:!0}),r.parts=i,p(e)&&(i.frontend.nav.push({key:"settings",labelKey:`${t}.nav.settings`,icon:"settings",order:1,path:"/settings"}),r.access={catalog:"shared/access.json"},r.permissions={required:["directory.read"]},r.declares={database:!0,events:!0}),{path:"tego.plugin.json",contents:u(r)}}o(ke,"manifest");function we(e,t){const n=e.parts.includes("frontend"),s=e.parts.includes("backend"),a=e.parts.includes("claw"),i=["tsgo --noEmit -p shared/tsconfig.json"];n&&i.push("tsgo --noEmit -p frontend/tsconfig.json"),s&&i.push("tsgo --noEmit -p backend/tsconfig.json"),a&&i.push("tsgo --noEmit -p claw/tsconfig.json");const r={"@tego/plugin-sdk":E("@tego/plugin-sdk",e,t),"@types/node":"^22.10.1",vitest:"^4.1.10"};t==="workspace"&&Object.assign(r,{"@tego/eslint-config":"workspace:*","@tego/tsconfig":"workspace:*"}),n&&Object.assign(r,{"@tanstack/react-query":"^5.62.7","@tego/ui":E("@tego/ui",e,t),"@types/react":"^19.2.1","@types/react-dom":"^19.2.1","lucide-react":"^0.469.0",react:"^19.0.0","react-dom":"^19.0.0","react-i18next":"15.4.1",vite:"^6.3.5"}),s&&Object.assign(r,{"@types/pg":"^8.11.10",hono:"^4.10.7",pg:"^8.13.1"}),a&&(r.typebox="1.3.16"),e.template==="standard"&&(n&&(r.tailwindcss="^3.4.19"),s&&(r.zod="^3.24.1"),p(e)&&(r["@tego/plugin-access-ui"]=E("@tego/plugin-access-ui",e,t))),t==="npm"&&Object.assign(r,De,Te);const c={name:y(e.id),version:"0.1.0",private:!0,type:"module",description:e.descriptionEn,scripts:{build:"tego-plugin-pack",watch:"tego-plugin-pack --watch",dev:"tego-plugin dev",doctor:"tego-plugin doctor","type-check":i.join(" && "),lint:"eslint .","lint:fix":"eslint . --fix",test:"vitest run"},devDependencies:Se(r)};return{path:"package.json",contents:u(c)}}o(we,"rootPackageJson");function Se(e){const t={};for(const n of Object.keys(e).sort())t[n]=e[n];return t}o(Se,"sortedRecord");function Oe(e){return e==="npm"?{path:"eslint.config.js",contents:Fe}:{path:"eslint.config.js",contents:`import baseConfig from '@tego/eslint-config';
|
|
45
700
|
|
|
46
701
|
export default [
|
|
47
702
|
...baseConfig,
|
|
@@ -49,8 +704,8 @@ export default [
|
|
|
49
704
|
ignores: ['build/**', 'dist/**', '**/dist/**', '.turbo/**'],
|
|
50
705
|
},
|
|
51
706
|
];
|
|
52
|
-
`}}
|
|
53
|
-
`}]}
|
|
707
|
+
`}}o(Oe,"eslintConfig");function Le(e){return[{path:"shared/tsconfig.json",contents:u({extends:g("backend",e),compilerOptions:{noEmit:!0,declaration:!1,declarationMap:!1},include:["**/*.ts"]})},{path:"shared/index.ts",contents:`export {};
|
|
708
|
+
`}]}o(Le,"sharedFiles");function Pe(){return{path:"vitest.config.ts",contents:`import { defineConfig } from 'vitest/config';
|
|
54
709
|
|
|
55
710
|
/**
|
|
56
711
|
* Node environment, no jsdom: components are not unit tested here by policy
|
|
@@ -75,7 +730,7 @@ export default defineConfig({
|
|
|
75
730
|
hookTimeout: 30_000,
|
|
76
731
|
},
|
|
77
732
|
});
|
|
78
|
-
`}}
|
|
733
|
+
`}}o(Pe,"vitestConfig");function Re(e,t){if(t==="npm")return Ie(e);const n=y(e.id);return{path:"README.md",contents:`# ${e.displayNameEn}
|
|
79
734
|
|
|
80
735
|
${e.descriptionEn}
|
|
81
736
|
|
|
@@ -94,7 +749,7 @@ pnpm -F ${n} build
|
|
|
94
749
|
|
|
95
750
|
Run \`pnpm install\` once after generating, so the new package links into the
|
|
96
751
|
workspace and \`@tego/plugin-sdk\` resolves.
|
|
97
|
-
`}}
|
|
752
|
+
`}}o(Re,"readme");function Ie(e){return{path:"README.md",contents:`# ${e.displayNameEn}
|
|
98
753
|
|
|
99
754
|
${e.descriptionEn}
|
|
100
755
|
|
|
@@ -115,7 +770,7 @@ npm run build # writes the uploadable archive
|
|
|
115
770
|
|
|
116
771
|
\`hostApiRange\` in \`tego.plugin.json\` and the SDK ranges in \`package.json\`
|
|
117
772
|
move together: raise both when the plugin starts using a newer host API.
|
|
118
|
-
`}}
|
|
773
|
+
`}}o(Ie,"standaloneReadme");function $e(e,t){const n=b(e.id),s=`import { readFile, readdir } from 'node:fs/promises';
|
|
119
774
|
import { resolve } from 'node:path';
|
|
120
775
|
|
|
121
776
|
import { tegoPluginFrontend } from '@tego/plugin-sdk/vite';
|
|
@@ -144,7 +799,7 @@ config.plugins!.push({
|
|
|
144
799
|
},
|
|
145
800
|
});
|
|
146
801
|
export default config;
|
|
147
|
-
`,a=
|
|
802
|
+
`,a=u({extends:g("frontend",t),compilerOptions:{rootDir:"..",noEmit:!0,declaration:!1,declarationMap:!1,types:["node"]},include:["src/**/*","vite.config.ts"],exclude:["node_modules"]}),i=`import { definePluginFrontend } from '@tego/plugin-sdk/frontend';
|
|
148
803
|
import { createRoot } from 'react-dom/client';
|
|
149
804
|
|
|
150
805
|
import { App } from './app';
|
|
@@ -226,7 +881,7 @@ export function App({ ctx }: { ctx: PluginHostContext }) {
|
|
|
226
881
|
</PluginErrorBoundary>
|
|
227
882
|
);
|
|
228
883
|
}
|
|
229
|
-
`,
|
|
884
|
+
`,c=e.template==="standard",d=c?[{path:"frontend/tsconfig.json",contents:a},...Z({id:e.id,ns:n,withBackend:e.parts.includes("backend"),withAccess:p(e)})]:[{path:"frontend/vite.config.ts",contents:s},{path:"frontend/tsconfig.json",contents:a},{path:"frontend/src/index.tsx",contents:i},{path:"frontend/src/app.tsx",contents:r}];for(const l of _e){const h=l==="zh-CN",x={nav:{home:h?"\u9996\u9875":"Home"},home:{title:h?e.displayNameZh:e.displayNameEn,description:h?e.descriptionZh:e.descriptionEn}},R={[n]:c?_(x,Be(l,p(e))):x};d.push({path:`frontend/i18n/${l}.json`,contents:u(R)})}return d}o($e,"frontendFiles");function _(e,t){const n={...e};for(const[s,a]of Object.entries(t)){const i=n[s];n[s]=i&&typeof i=="object"&&a&&typeof a=="object"?_(i,a):a}return n}o(_,"mergeStrings");function je(e,t){const n=u({extends:g("backend",e),compilerOptions:{noEmit:!0,declaration:!1,declarationMap:!1,rootDir:".."},include:["src/**/*"],exclude:["node_modules"]}),s=`import { serveBackend } from '@tego/plugin-sdk/server-runtime';
|
|
230
885
|
|
|
231
886
|
import { backend } from './app';
|
|
232
887
|
|
|
@@ -237,7 +892,7 @@ import { backend } from './app';
|
|
|
237
892
|
* headers; everything below it is a plain fetch handler.
|
|
238
893
|
*/
|
|
239
894
|
serveBackend(backend);
|
|
240
|
-
|
|
895
|
+
`,a=`import { definePluginBackend } from '@tego/plugin-sdk/server';
|
|
241
896
|
import type { PluginHostApi, PluginUserContext } from '@tego/plugin-sdk/server';
|
|
242
897
|
import { Hono } from 'hono';
|
|
243
898
|
|
|
@@ -281,7 +936,7 @@ export const backend = definePluginBackend({
|
|
|
281
936
|
return app.fetch(request, { host, user: ctx });
|
|
282
937
|
},
|
|
283
938
|
});
|
|
284
|
-
|
|
939
|
+
`,i=`import { dirname, join } from 'node:path';
|
|
285
940
|
import { fileURLToPath } from 'node:url';
|
|
286
941
|
|
|
287
942
|
import {
|
|
@@ -378,7 +1033,7 @@ describe('scaffold backend', () => {
|
|
|
378
1033
|
expect(await listed.json()).toEqual({ notes: [] });
|
|
379
1034
|
});
|
|
380
1035
|
});
|
|
381
|
-
|
|
1036
|
+
`,r=`-- The backend part's schema.
|
|
382
1037
|
--
|
|
383
1038
|
-- Applied by the host once, inside a transaction whose \`search_path\` is this
|
|
384
1039
|
-- install's own schema and nothing else. So nothing here is schema-qualified,
|
|
@@ -394,8 +1049,8 @@ CREATE TABLE notes (
|
|
|
394
1049
|
body text NOT NULL,
|
|
395
1050
|
created_at timestamptz NOT NULL DEFAULT now()
|
|
396
1051
|
);
|
|
397
|
-
|
|
398
|
-
"extends": "${
|
|
1052
|
+
`;return t?.template==="standard"?[{path:"backend/tsconfig.json",contents:n},...V({id:t.id,withAccess:p(t)})]:[{path:"backend/tsconfig.json",contents:n},{path:"backend/src/index.ts",contents:s},{path:"backend/src/app.ts",contents:a},{path:"backend/src/app.test.ts",contents:i},{path:"backend/migrations/0001_init.sql",contents:r}]}o(je,"backendFiles");function Ue(e,t){const n=u({id:e.id,name:e.displayNameEn,description:e.descriptionEn,enabledByDefault:!0,activation:{onStartup:!0},configSchema:{type:"object",additionalProperties:!1,properties:{}}}),s=u({name:e.id,private:!0,type:"module",openclaw:{extensions:["./index.js"]},version:"0.1.0"}),a=`{
|
|
1053
|
+
"extends": "${g("backend",t)}",
|
|
399
1054
|
"compilerOptions": {
|
|
400
1055
|
"rootDir": "..",
|
|
401
1056
|
"noEmit": true,
|
|
@@ -411,7 +1066,7 @@ CREATE TABLE notes (
|
|
|
411
1066
|
"include": ["src/**/*", "types/**/*"],
|
|
412
1067
|
"exclude": ["node_modules"]
|
|
413
1068
|
}
|
|
414
|
-
`,
|
|
1069
|
+
`,i=`/**
|
|
415
1070
|
* The slice of claw's plugin SDK this extension uses.
|
|
416
1071
|
*
|
|
417
1072
|
* Declared here rather than imported because a plugin builds outside claw's
|
|
@@ -520,4 +1175,4 @@ export default definePluginEntry({
|
|
|
520
1175
|
// refuses the registration.
|
|
521
1176
|
},
|
|
522
1177
|
});
|
|
523
|
-
`;return[{path:"claw/openclaw.plugin.json",contents:n},{path:"claw/package.json",contents:
|
|
1178
|
+
`;return[{path:"claw/openclaw.plugin.json",contents:n},{path:"claw/package.json",contents:s},{path:"claw/tsconfig.json",contents:a},{path:"claw/types/openclaw-plugin-sdk.d.ts",contents:i},{path:"claw/src/index.ts",contents:r}]}o(Ue,"clawFiles");function A(e,{dependencies:t="workspace"}={}){const n=[ke(e),we(e,t),Oe(t),...Le(t),Pe(),Re(e,t)];return t==="npm"&&n.push(...Ne()),e.parts.includes("frontend")&&n.push(...$e(e,t)),e.parts.includes("backend")&&n.push(...je(t,e)),p(e)&&n.push({path:"shared/access.json",contents:u(le(b(e.id)))}),e.parts.includes("claw")&&n.push(...Ue(e,t)),n}o(A,"buildPluginFiles");var k=/^[a-z0-9][a-z0-9._-]*$/,B=["frontend","backend","claw"],v=["avatar","tenant"],w="lowercase letters and digits, may contain . _ -, starting with a letter or digit";function S(e,t){if(typeof e!="object"||e===null||Array.isArray(e))throw new Error("answers must be a JSON object");const n=e,s=o(d=>{const l=n[d];if(typeof l!="string"||l.trim()==="")throw new Error(`${d} is required`);return l.trim()},"text"),a=s("id");if(!k.test(a))throw new Error(`id must be ${w}`);const i=n.parts;if(!Array.isArray(i)||i.length===0||i.some(d=>!B.includes(d)))throw new Error(`parts must be a non-empty list of ${B.join(", ")}`);const r=n.scope??"avatar";if(!v.includes(r))throw new Error(`scope must be one of ${v.join(", ")}`);const c=n.template??"standard";if(!f.includes(c))throw new Error(`template must be one of ${f.join(", ")}`);return{id:a,displayNameZh:s("displayNameZh"),displayNameEn:s("displayNameEn"),descriptionZh:s("descriptionZh"),descriptionEn:s("descriptionEn"),parts:[...new Set(i)],scope:r,hostApiRange:t,template:c}}o(S,"validatePluginAnswers");function qe(e,t){return P(m(e,"plugins"),t,A(t))}o(qe,"writePlugin");async function O(e,t){return P(e,t,await L(A(t,{dependencies:"npm"})))}o(O,"writeStandalonePlugin");var Me=/\.(?:[cm]?[jt]sx?|json|css)$/;async function L(e){const t=await import("prettier");return Promise.all(e.map(async n=>Me.test(n.path)?{...n,contents:await t.format(n.contents,{...C,filepath:n.path})}:n))}o(L,"formatPluginFiles");function P(e,t,n){const s=m(e,t.id);if(j(s))throw new Error(`${s} already exists \u2014 pick another id or remove it first`);F(e,{recursive:!0});const a=U(m(e,`.${t.id}-`));try{for(const i of n){const r=m(a,i.path);F(H(r),{recursive:!0}),q(r,i.contents)}M(a,s)}catch(i){throw z(a,{recursive:!0,force:!0}),i}return{directory:s,files:n.map(i=>i.path)}}o(P,"writePluginDirectory");async function ze(e,t){let n;try{n=JSON.parse(e)}catch(a){throw new Error(`answers must be JSON: ${a instanceof Error?a.message:String(a)}`)}const s=S(n,`^${$}`);return O(G(t.cwd,t.parent),s)}o(ze,"createStandalonePlugin");export{w as PLUGIN_ID_HINT,k as PLUGIN_ID_PATTERN,B as PLUGIN_PARTS,v as PLUGIN_SCOPES,f as PLUGIN_TEMPLATES,N as PUBLISHED_SDK_PACKAGES,C as STANDALONE_PRETTIER,D as STANDALONE_TSCONFIG_FILES,A as buildPluginFiles,ze as createStandalonePlugin,L as formatPluginFiles,y as packageNameFor,S as validatePluginAnswers,qe as writePlugin,O as writeStandalonePlugin};
|