@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.
Files changed (99) hide show
  1. package/README.md +50 -3
  2. package/agent/RULES.md +123 -0
  3. package/agent/guides/access.md +39 -0
  4. package/agent/guides/backend.md +76 -0
  5. package/agent/guides/claw.md +18 -0
  6. package/agent/guides/desktop-web.md +48 -0
  7. package/agent/guides/frontend.md +83 -0
  8. package/agent/guides/host-api-versions.md +34 -0
  9. package/agent/guides/i18n.md +17 -0
  10. package/agent/guides/manifest.md +26 -0
  11. package/agent/guides/migrations.md +17 -0
  12. package/agent/guides/storage.md +18 -0
  13. package/agent/guides/testing.md +17 -0
  14. package/agent/guides/ui.md +22 -0
  15. package/agent/reference/plugin-sdk-api.md +561 -0
  16. package/bin/tego-plugin-pack.mjs +1 -1
  17. package/bin/tego-plugin.mjs +4 -2
  18. package/dist/chunk-2NTAWYCG.js +1 -0
  19. package/dist/chunk-2P6UEAPV.cjs +1 -0
  20. package/dist/chunk-45Y4CR4R.cjs +1 -0
  21. package/dist/{chunk-THMWRUAT.cjs → chunk-4LKRXDYY.cjs} +13 -12
  22. package/dist/{chunk-7XHF3YSK.js → chunk-56QOZQTY.js} +1 -1
  23. package/dist/{chunk-C2OCOPZQ.cjs → chunk-6HNG5OZI.cjs} +1 -1
  24. package/dist/chunk-DEGQDJF6.js +1 -0
  25. package/dist/chunk-EIVA5BR6.js +1 -0
  26. package/dist/chunk-FCYAAOHC.js +20 -0
  27. package/dist/chunk-HZ4YWRYX.cjs +5 -0
  28. package/dist/chunk-I5Z3ECC4.js +5 -0
  29. package/dist/chunk-INCA3GFW.js +1 -0
  30. package/dist/chunk-IVJECPGG.js +1 -0
  31. package/dist/chunk-MWMGJJ74.cjs +1 -0
  32. package/dist/chunk-N4SXZZPY.cjs +1 -0
  33. package/dist/chunk-SKNPNPJP.cjs +1 -0
  34. package/dist/dev.cjs +358 -0
  35. package/dist/dev.d.cts +144 -0
  36. package/dist/dev.d.ts +144 -0
  37. package/dist/dev.js +358 -0
  38. package/dist/doctor.cjs +8 -0
  39. package/dist/doctor.d.cts +62 -0
  40. package/dist/doctor.d.ts +62 -0
  41. package/dist/doctor.js +8 -0
  42. package/dist/frontend.d.cts +33 -1
  43. package/dist/frontend.d.ts +33 -1
  44. package/dist/hono.cjs +1 -0
  45. package/dist/hono.d.cts +84 -0
  46. package/dist/hono.d.ts +84 -0
  47. package/dist/hono.js +1 -0
  48. package/dist/index.cjs +1 -1
  49. package/dist/index.d.cts +8 -3
  50. package/dist/index.d.ts +8 -3
  51. package/dist/index.js +1 -1
  52. package/dist/{manifest-types-DQARCQJt.d.cts → manifest-types-CUTNfJTa.d.cts} +1 -1
  53. package/dist/{manifest-types-DQARCQJt.d.ts → manifest-types-CUTNfJTa.d.ts} +1 -1
  54. package/dist/manifest.cjs +1 -1
  55. package/dist/manifest.d.cts +55 -2
  56. package/dist/manifest.d.ts +55 -2
  57. package/dist/manifest.js +1 -1
  58. package/dist/migrations-sync.cjs +1 -0
  59. package/dist/migrations-sync.d.cts +20 -0
  60. package/dist/migrations-sync.d.ts +20 -0
  61. package/dist/migrations-sync.js +1 -0
  62. package/dist/pack.cjs +1 -1
  63. package/dist/pack.d.cts +8 -2
  64. package/dist/pack.d.ts +8 -2
  65. package/dist/pack.js +1 -1
  66. package/dist/publish.cjs +1 -1
  67. package/dist/publish.d.cts +1 -1
  68. package/dist/publish.d.ts +1 -1
  69. package/dist/publish.js +1 -1
  70. package/dist/react.cjs +1 -0
  71. package/dist/react.d.cts +101 -0
  72. package/dist/react.d.ts +101 -0
  73. package/dist/react.js +1 -0
  74. package/dist/scaffold.cjs +672 -17
  75. package/dist/scaffold.d.cts +42 -18
  76. package/dist/scaffold.d.ts +42 -18
  77. package/dist/scaffold.js +674 -19
  78. package/dist/server-runtime.cjs +1 -1
  79. package/dist/server-runtime.d.cts +27 -2
  80. package/dist/server-runtime.d.ts +27 -2
  81. package/dist/server-runtime.js +1 -1
  82. package/dist/testing.cjs +6 -2
  83. package/dist/testing.d.cts +19 -1
  84. package/dist/testing.d.ts +19 -1
  85. package/dist/testing.js +6 -2
  86. package/dist/verify-FF2RD2MV.cjs +1 -0
  87. package/dist/verify-W7ZTUCO4.js +1 -0
  88. package/dist/vite.cjs +1 -1
  89. package/dist/vite.d.cts +27 -1
  90. package/dist/vite.d.ts +27 -1
  91. package/dist/vite.js +1 -1
  92. package/package.json +37 -3
  93. package/dist/chunk-3KUOBGJN.js +0 -19
  94. package/dist/chunk-3WA3M347.cjs +0 -1
  95. package/dist/chunk-5CU33JVY.js +0 -1
  96. package/dist/chunk-7K37PVGV.js +0 -5
  97. package/dist/chunk-BTHXQWHT.js +0 -1
  98. package/dist/chunk-ROFUQLOZ.cjs +0 -1
  99. package/dist/chunk-W7C357A6.cjs +0 -5
package/dist/scaffold.cjs CHANGED
@@ -1,4 +1,658 @@
1
- "use strict";var u=Object.defineProperty;var i=(e,t)=>u(e,"name",{value:t,configurable:!0});var chunkC2OCOPZQ_cjs=require("./chunk-C2OCOPZQ.cjs"),fs=require("fs"),path=require("path"),PUBLISHED_SDK_PACKAGES={"@tego/plugin-access-ui":"@zhama/tego-plugin-access-ui","@tego/plugin-sdk":"@zhama/tego-plugin-sdk","@tego/ui":"@zhama/tego-ui"};function sdkSpec(e,t,n){if(n==="workspace")return"workspace:*";const o=PUBLISHED_SDK_PACKAGES[e];if(!o)throw new Error(`${e} is not a published SDK package`);return`npm:${o}@${t.hostApiRange}`}i(sdkSpec,"sdkSpec");var STANDALONE_PRETTIER={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"},STANDALONE_GITIGNORE=`node_modules/
1
+ "use strict";var g=Object.defineProperty;var o=(e,t)=>g(e,"name",{value:t,configurable:!0});var chunk6HNG5OZI_cjs=require("./chunk-6HNG5OZI.cjs"),fs=require("fs"),path=require("path"),ACCESS_SQL=`-- 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");`,NOTES_MIGRATION=`-- 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 standardBackendFiles(e){return[{path:"backend/src/index.ts",contents:INDEX_TS},{path:"backend/src/app.ts",contents:appTs(e)},{path:"backend/src/app.test.ts",contents:appTest(e)},{path:"backend/migrations/0001_init.sql",contents:NOTES_MIGRATION},...e.withAccess?[{path:"backend/src/access.ts",contents:ACCESS_TS},{path:"backend/migrations/0002_access.sql",contents:ACCESS_SQL}]:[]]}o(standardBackendFiles,"standardBackendFiles");var INDEX_TS=`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
+ `,ACCESS_TS=`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 appTs({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(appTs,"appTs");function appTest({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(appTest,"appTest");function standardFrontendFiles(e){return[{path:"frontend/vite.config.ts",contents:viteConfig(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:indexTsx()},{path:"frontend/src/app.tsx",contents:appTsx(e)},{path:"frontend/src/lib/route.ts",contents:routeTs(e)},{path:"frontend/src/lib/route.test.ts",contents:routeTest(e)},{path:"frontend/src/i18n.test.ts",contents:i18nTest()},{path:"frontend/src/components/navigation.tsx",contents:navigationTsx(e)},{path:"frontend/src/screens/home.tsx",contents:e.withBackend?notesHomeTsx(e):staticHomeTsx(e)},...e.withBackend?[{path:"frontend/src/lib/api.ts",contents:apiTs()}]:[],...e.withAccess?[{path:"frontend/src/screens/settings.tsx",contents:settingsTsx(e)}]:[]]}o(standardFrontendFiles,"standardFrontendFiles");function viteConfig({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(viteConfig,"viteConfig");function indexTsx(){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(indexTsx,"indexTsx");function appTsx({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(appTsx,"appTsx");function routeTs({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(routeTs,"routeTs");function routeTest({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(routeTest,"routeTest");function i18nTest(){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(i18nTest,"i18nTest");function navigationTsx({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(navigationTsx,"navigationTsx");function staticHomeTsx({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(staticHomeTsx,"staticHomeTsx");function apiTs(){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(apiTs,"apiTs");function notesHomeTsx({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(notesHomeTsx,"notesHomeTsx");function settingsTsx({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(settingsTsx,"settingsTsx");function standardAccessCatalog(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(standardAccessCatalog,"standardAccessCatalog");var ZH={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"}}},ZH_TW={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"}}},EN={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"}}},JA={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"}}},KO={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"}}},DE={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"}}},ES={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"}}},FR={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"}}},IT={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"}}},PT={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"}}},STRINGS={"zh-CN":ZH,"zh-TW":ZH_TW,en:EN,ja:JA,ko:KO,de:DE,es:ES,fr:FR,it:IT,pt:PT};function standardStrings(e,t){const n=STRINGS[e]??EN,{nav:s,settings:a,access:i,...r}=n;return t?{...r,nav:s,settings:a,access:i}:r}o(standardStrings,"standardStrings");var PLUGIN_TEMPLATES=["standard","minimal"];function standardAccess(e){return e.template==="standard"&&e.parts.includes("frontend")&&e.parts.includes("backend")}o(standardAccess,"standardAccess");var PUBLISHED_SDK_PACKAGES={"@tego/plugin-access-ui":"@zhama/tego-plugin-access-ui","@tego/plugin-sdk":"@zhama/tego-plugin-sdk","@tego/ui":"@zhama/tego-ui"};function sdkSpec(e,t,n){if(n==="workspace")return"workspace:*";const s=PUBLISHED_SDK_PACKAGES[e];if(!s)throw new Error(`${e} is not a published SDK package`);return`npm:${s}@${t.hostApiRange}`}o(sdkSpec,"sdkSpec");var STANDALONE_PRETTIER={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"},STANDALONE_GITIGNORE=`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/
663
+ .tego-dev/
9
664
  `,STANDALONE_PNPM_WORKSPACE=`allowBuilds:
10
665
  esbuild: true
11
- `,STANDALONE_TSCONFIG_FILES={"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 tsconfigPreset(e,t){return t==="workspace"?`@tego/tsconfig/plugin-${e}.json`:`../tsconfig.${e}.json`}i(tsconfigPreset,"tsconfigPreset");var STANDALONE_ESLINT_CONFIG=`import js from '@eslint/js';
666
+ `,STANDALONE_TSCONFIG_FILES={"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 tsconfigPreset(e,t){return t==="workspace"?`@tego/tsconfig/plugin-${e}.json`:`../tsconfig.${e}.json`}o(tsconfigPreset,"tsconfigPreset");var STANDALONE_ESLINT_CONFIG=`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
- `,STANDALONE_LINT_DEPENDENCIES={"@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 standaloneRootFiles(){return[{path:".prettierrc.json",contents:json(STANDALONE_PRETTIER)},{path:".gitignore",contents:STANDALONE_GITIGNORE},{path:"pnpm-workspace.yaml",contents:STANDALONE_PNPM_WORKSPACE},...Object.entries(STANDALONE_TSCONFIG_FILES).map(([e,t])=>({path:e,contents:json(t)}))]}i(standaloneRootFiles,"standaloneRootFiles");var STANDALONE_TOOLCHAIN={"@typescript/native-preview":"7.0.0-dev.20260519.1",eslint:"^9.39.1",prettier:"^3.7.4",typescript:"^5.9.3"},SUPPORTED_LOCALES=["zh-CN","zh-TW","en","ja","ko","de","es","fr","it","pt"];function packageNameFor(e){return`@tego/plugin-${e.replace(/^tego-/,"")}`}i(packageNameFor,"packageNameFor");function keyNamespaceFor(e){return e.replace(/^tego-/,"")}i(keyNamespaceFor,"keyNamespaceFor");function json(e){return`${JSON.stringify(e,null,2)}
44
- `}i(json,"json");function manifest(e){const t=keyNamespaceFor(e.id),n=e.parts.includes("frontend"),o=e.parts.includes("backend"),a=e.parts.includes("claw"),s={};n&&(s.frontend={icon:"puzzle",nav:[{key:"home",labelKey:`${t}.nav.home`,icon:"layout-dashboard",order:0,path:"/home"}]}),o&&(s.backend={limits:{memoryMb:256,requestTimeoutMs:15e3}}),a&&(s.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||o)&&(r.hostApiRange=e.hostApiRange),r.scope=e.scope,o&&(r.declares={database:!0}),r.parts=s,{path:"tego.plugin.json",contents:json(r)}}i(manifest,"manifest");function rootPackageJson(e,t){const n=e.parts.includes("frontend"),o=e.parts.includes("backend"),a=e.parts.includes("claw"),s=["tsgo --noEmit -p shared/tsconfig.json"];n&&s.push("tsgo --noEmit -p frontend/tsconfig.json"),o&&s.push("tsgo --noEmit -p backend/tsconfig.json"),a&&s.push("tsgo --noEmit -p claw/tsconfig.json");const r={"@tego/plugin-sdk":sdkSpec("@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":sdkSpec("@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"}),o&&Object.assign(r,{"@types/pg":"^8.11.10",hono:"^4.10.7",pg:"^8.13.1"}),a&&(r.typebox="1.3.16"),t==="npm"&&Object.assign(r,STANDALONE_TOOLCHAIN,STANDALONE_LINT_DEPENDENCIES);const c={name:packageNameFor(e.id),version:"0.1.0",private:!0,type:"module",description:e.descriptionEn,scripts:{build:"tego-plugin-pack",watch:"tego-plugin-pack --watch","type-check":s.join(" && "),lint:"eslint .","lint:fix":"eslint . --fix",test:"vitest run"},devDependencies:sortedRecord(r)};return{path:"package.json",contents:json(c)}}i(rootPackageJson,"rootPackageJson");function sortedRecord(e){const t={};for(const n of Object.keys(e).sort())t[n]=e[n];return t}i(sortedRecord,"sortedRecord");function eslintConfig(e){return e==="npm"?{path:"eslint.config.js",contents:STANDALONE_ESLINT_CONFIG}:{path:"eslint.config.js",contents:`import baseConfig from '@tego/eslint-config';
698
+ `,STANDALONE_LINT_DEPENDENCIES={"@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 standaloneRootFiles(){return[{path:".prettierrc.json",contents:json(STANDALONE_PRETTIER)},{path:".gitignore",contents:STANDALONE_GITIGNORE},{path:"pnpm-workspace.yaml",contents:STANDALONE_PNPM_WORKSPACE},...Object.entries(STANDALONE_TSCONFIG_FILES).map(([e,t])=>({path:e,contents:json(t)}))]}o(standaloneRootFiles,"standaloneRootFiles");var STANDALONE_TOOLCHAIN={"@typescript/native-preview":"7.0.0-dev.20260519.1",eslint:"^9.39.1",prettier:"^3.7.4",typescript:"^5.9.3"},SUPPORTED_LOCALES=["zh-CN","zh-TW","en","ja","ko","de","es","fr","it","pt"];function packageNameFor(e){return`@tego/plugin-${e.replace(/^tego-/,"")}`}o(packageNameFor,"packageNameFor");function keyNamespaceFor(e){return e.replace(/^tego-/,"")}o(keyNamespaceFor,"keyNamespaceFor");function json(e){return`${JSON.stringify(e,null,2)}
699
+ `}o(json,"json");function manifest(e){const t=keyNamespaceFor(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,standardAccess(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:json(r)}}o(manifest,"manifest");function rootPackageJson(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":sdkSpec("@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":sdkSpec("@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"),standardAccess(e)&&(r["@tego/plugin-access-ui"]=sdkSpec("@tego/plugin-access-ui",e,t))),t==="npm"&&Object.assign(r,STANDALONE_TOOLCHAIN,STANDALONE_LINT_DEPENDENCIES);const u={name:packageNameFor(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:sortedRecord(r)};return{path:"package.json",contents:json(u)}}o(rootPackageJson,"rootPackageJson");function sortedRecord(e){const t={};for(const n of Object.keys(e).sort())t[n]=e[n];return t}o(sortedRecord,"sortedRecord");function eslintConfig(e){return e==="npm"?{path:"eslint.config.js",contents:STANDALONE_ESLINT_CONFIG}:{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
- `}}i(eslintConfig,"eslintConfig");function sharedFiles(e){return[{path:"shared/tsconfig.json",contents:json({extends:tsconfigPreset("backend",e),compilerOptions:{noEmit:!0,declaration:!1,declarationMap:!1},include:["**/*.ts"]})},{path:"shared/index.ts",contents:`export {};
53
- `}]}i(sharedFiles,"sharedFiles");function vitestConfig(){return{path:"vitest.config.ts",contents:`import { defineConfig } from 'vitest/config';
707
+ `}}o(eslintConfig,"eslintConfig");function sharedFiles(e){return[{path:"shared/tsconfig.json",contents:json({extends:tsconfigPreset("backend",e),compilerOptions:{noEmit:!0,declaration:!1,declarationMap:!1},include:["**/*.ts"]})},{path:"shared/index.ts",contents:`export {};
708
+ `}]}o(sharedFiles,"sharedFiles");function vitestConfig(){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
- `}}i(vitestConfig,"vitestConfig");function readme(e,t){if(t==="npm")return standaloneReadme(e);const n=packageNameFor(e.id);return{path:"README.md",contents:`# ${e.displayNameEn}
733
+ `}}o(vitestConfig,"vitestConfig");function readme(e,t){if(t==="npm")return standaloneReadme(e);const n=packageNameFor(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
- `}}i(readme,"readme");function standaloneReadme(e){return{path:"README.md",contents:`# ${e.displayNameEn}
752
+ `}}o(readme,"readme");function standaloneReadme(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
- `}}i(standaloneReadme,"standaloneReadme");function frontendFiles(e,t){const n=keyNamespaceFor(e.id),o=`import { readFile, readdir } from 'node:fs/promises';
773
+ `}}o(standaloneReadme,"standaloneReadme");function frontendFiles(e,t){const n=keyNamespaceFor(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=json({extends:tsconfigPreset("frontend",t),compilerOptions:{rootDir:"..",noEmit:!0,declaration:!1,declarationMap:!1,types:["node"]},include:["src/**/*","vite.config.ts"],exclude:["node_modules"]}),s=`import { definePluginFrontend } from '@tego/plugin-sdk/frontend';
802
+ `,a=json({extends:tsconfigPreset("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
- `,c=[{path:"frontend/vite.config.ts",contents:o},{path:"frontend/tsconfig.json",contents:a},{path:"frontend/src/index.tsx",contents:s},{path:"frontend/src/app.tsx",contents:r}];for(const l of SUPPORTED_LOCALES){const d=l==="zh-CN",p={[n]:{nav:{home:d?"\u9996\u9875":"Home"},home:{title:d?e.displayNameZh:e.displayNameEn,description:d?e.descriptionZh:e.descriptionEn}}};c.push({path:`frontend/i18n/${l}.json`,contents:json(p)})}return c}i(frontendFiles,"frontendFiles");function backendFiles(e){return[{path:"backend/tsconfig.json",contents:json({extends:tsconfigPreset("backend",e),compilerOptions:{noEmit:!0,declaration:!1,declarationMap:!1,rootDir:".."},include:["src/**/*"],exclude:["node_modules"]})},{path:"backend/src/index.ts",contents:`import { serveBackend } from '@tego/plugin-sdk/server-runtime';
884
+ `,u=e.template==="standard",c=u?[{path:"frontend/tsconfig.json",contents:a},...standardFrontendFiles({id:e.id,ns:n,withBackend:e.parts.includes("backend"),withAccess:standardAccess(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 d of SUPPORTED_LOCALES){const l=d==="zh-CN",p={nav:{home:l?"\u9996\u9875":"Home"},home:{title:l?e.displayNameZh:e.displayNameEn,description:l?e.descriptionZh:e.descriptionEn}},m={[n]:u?mergeStrings(p,standardStrings(d,standardAccess(e))):p};c.push({path:`frontend/i18n/${d}.json`,contents:json(m)})}return c}o(frontendFiles,"frontendFiles");function mergeStrings(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"?mergeStrings(i,a):a}return n}o(mergeStrings,"mergeStrings");function backendFiles(e,t){const n=json({extends:tsconfigPreset("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
- `},{path:"backend/src/app.ts",contents:`import { definePluginBackend } from '@tego/plugin-sdk/server';
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
- `},{path:"backend/src/app.test.ts",contents:`import { dirname, join } from 'node:path';
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
- `},{path:"backend/migrations/0001_init.sql",contents:`-- The backend part's schema.
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,7 +1049,7 @@ CREATE TABLE notes (
394
1049
  body text NOT NULL,
395
1050
  created_at timestamptz NOT NULL DEFAULT now()
396
1051
  );
397
- `}]}i(backendFiles,"backendFiles");function clawFiles(e,t){const n=json({id:e.id,name:e.displayNameEn,description:e.descriptionEn,enabledByDefault:!0,activation:{onStartup:!0},configSchema:{type:"object",additionalProperties:!1,properties:{}}}),o=json({name:e.id,private:!0,type:"module",openclaw:{extensions:["./index.js"]},version:"0.1.0"}),a=`{
1052
+ `;return t?.template==="standard"?[{path:"backend/tsconfig.json",contents:n},...standardBackendFiles({id:t.id,withAccess:standardAccess(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(backendFiles,"backendFiles");function clawFiles(e,t){const n=json({id:e.id,name:e.displayNameEn,description:e.descriptionEn,enabledByDefault:!0,activation:{onStartup:!0},configSchema:{type:"object",additionalProperties:!1,properties:{}}}),s=json({name:e.id,private:!0,type:"module",openclaw:{extensions:["./index.js"]},version:"0.1.0"}),a=`{
398
1053
  "extends": "${tsconfigPreset("backend",t)}",
399
1054
  "compilerOptions": {
400
1055
  "rootDir": "..",
@@ -411,7 +1066,7 @@ CREATE TABLE notes (
411
1066
  "include": ["src/**/*", "types/**/*"],
412
1067
  "exclude": ["node_modules"]
413
1068
  }
414
- `,s=`/**
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:o},{path:"claw/tsconfig.json",contents:a},{path:"claw/types/openclaw-plugin-sdk.d.ts",contents:s},{path:"claw/src/index.ts",contents:r}]}i(clawFiles,"clawFiles");function buildPluginFiles(e,{dependencies:t="workspace"}={}){const n=[manifest(e),rootPackageJson(e,t),eslintConfig(t),...sharedFiles(t),vitestConfig(),readme(e,t)];return t==="npm"&&n.push(...standaloneRootFiles()),e.parts.includes("frontend")&&n.push(...frontendFiles(e,t)),e.parts.includes("backend")&&n.push(...backendFiles(t)),e.parts.includes("claw")&&n.push(...clawFiles(e,t)),n}i(buildPluginFiles,"buildPluginFiles");var PLUGIN_ID_PATTERN=/^[a-z0-9][a-z0-9._-]*$/,PLUGIN_PARTS=["frontend","backend","claw"],PLUGIN_SCOPES=["avatar","tenant"],PLUGIN_ID_HINT="lowercase letters and digits, may contain . _ -, starting with a letter or digit";function validatePluginAnswers(e,t){if(typeof e!="object"||e===null||Array.isArray(e))throw new Error("answers must be a JSON object");const n=e,o=i(c=>{const l=n[c];if(typeof l!="string"||l.trim()==="")throw new Error(`${c} is required`);return l.trim()},"text"),a=o("id");if(!PLUGIN_ID_PATTERN.test(a))throw new Error(`id must be ${PLUGIN_ID_HINT}`);const s=n.parts;if(!Array.isArray(s)||s.length===0||s.some(c=>!PLUGIN_PARTS.includes(c)))throw new Error(`parts must be a non-empty list of ${PLUGIN_PARTS.join(", ")}`);const r=n.scope??"avatar";if(!PLUGIN_SCOPES.includes(r))throw new Error(`scope must be one of ${PLUGIN_SCOPES.join(", ")}`);return{id:a,displayNameZh:o("displayNameZh"),displayNameEn:o("displayNameEn"),descriptionZh:o("descriptionZh"),descriptionEn:o("descriptionEn"),parts:[...new Set(s)],scope:r,hostApiRange:t}}i(validatePluginAnswers,"validatePluginAnswers");function writePlugin(e,t){return writePluginDirectory(path.join(e,"plugins"),t,buildPluginFiles(t))}i(writePlugin,"writePlugin");function writeStandalonePlugin(e,t){return writePluginDirectory(e,t,buildPluginFiles(t,{dependencies:"npm"}))}i(writeStandalonePlugin,"writeStandalonePlugin");function writePluginDirectory(e,t,n){const o=path.join(e,t.id);if(fs.existsSync(o))throw new Error(`${o} already exists \u2014 pick another id or remove it first`);fs.mkdirSync(e,{recursive:!0});const a=fs.mkdtempSync(path.join(e,`.${t.id}-`));try{for(const s of n){const r=path.join(a,s.path);fs.mkdirSync(path.dirname(r),{recursive:!0}),fs.writeFileSync(r,s.contents)}fs.renameSync(a,o)}catch(s){throw fs.rmSync(a,{recursive:!0,force:!0}),s}return{directory:o,files:n.map(s=>s.path)}}i(writePluginDirectory,"writePluginDirectory");function createStandalonePlugin(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 o=validatePluginAnswers(n,`^${chunkC2OCOPZQ_cjs.HOST_API_VERSION}`);return writeStandalonePlugin(path.resolve(t.cwd,t.parent),o)}i(createStandalonePlugin,"createStandalonePlugin"),exports.PLUGIN_ID_HINT=PLUGIN_ID_HINT,exports.PLUGIN_ID_PATTERN=PLUGIN_ID_PATTERN,exports.PLUGIN_PARTS=PLUGIN_PARTS,exports.PLUGIN_SCOPES=PLUGIN_SCOPES,exports.PUBLISHED_SDK_PACKAGES=PUBLISHED_SDK_PACKAGES,exports.STANDALONE_TSCONFIG_FILES=STANDALONE_TSCONFIG_FILES,exports.buildPluginFiles=buildPluginFiles,exports.createStandalonePlugin=createStandalonePlugin,exports.packageNameFor=packageNameFor,exports.validatePluginAnswers=validatePluginAnswers,exports.writePlugin=writePlugin,exports.writeStandalonePlugin=writeStandalonePlugin;
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(clawFiles,"clawFiles");function buildPluginFiles(e,{dependencies:t="workspace"}={}){const n=[manifest(e),rootPackageJson(e,t),eslintConfig(t),...sharedFiles(t),vitestConfig(),readme(e,t)];return t==="npm"&&n.push(...standaloneRootFiles()),e.parts.includes("frontend")&&n.push(...frontendFiles(e,t)),e.parts.includes("backend")&&n.push(...backendFiles(t,e)),standardAccess(e)&&n.push({path:"shared/access.json",contents:json(standardAccessCatalog(keyNamespaceFor(e.id)))}),e.parts.includes("claw")&&n.push(...clawFiles(e,t)),n}o(buildPluginFiles,"buildPluginFiles");var PLUGIN_ID_PATTERN=/^[a-z0-9][a-z0-9._-]*$/,PLUGIN_PARTS=["frontend","backend","claw"],PLUGIN_SCOPES=["avatar","tenant"],PLUGIN_ID_HINT="lowercase letters and digits, may contain . _ -, starting with a letter or digit";function validatePluginAnswers(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(c=>{const d=n[c];if(typeof d!="string"||d.trim()==="")throw new Error(`${c} is required`);return d.trim()},"text"),a=s("id");if(!PLUGIN_ID_PATTERN.test(a))throw new Error(`id must be ${PLUGIN_ID_HINT}`);const i=n.parts;if(!Array.isArray(i)||i.length===0||i.some(c=>!PLUGIN_PARTS.includes(c)))throw new Error(`parts must be a non-empty list of ${PLUGIN_PARTS.join(", ")}`);const r=n.scope??"avatar";if(!PLUGIN_SCOPES.includes(r))throw new Error(`scope must be one of ${PLUGIN_SCOPES.join(", ")}`);const u=n.template??"standard";if(!PLUGIN_TEMPLATES.includes(u))throw new Error(`template must be one of ${PLUGIN_TEMPLATES.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:u}}o(validatePluginAnswers,"validatePluginAnswers");function writePlugin(e,t){return writePluginDirectory(path.join(e,"plugins"),t,buildPluginFiles(t))}o(writePlugin,"writePlugin");async function writeStandalonePlugin(e,t){return writePluginDirectory(e,t,await formatPluginFiles(buildPluginFiles(t,{dependencies:"npm"})))}o(writeStandalonePlugin,"writeStandalonePlugin");var FORMATTED=/\.(?:[cm]?[jt]sx?|json|css)$/;async function formatPluginFiles(e){const t=await import("prettier");return Promise.all(e.map(async n=>FORMATTED.test(n.path)?{...n,contents:await t.format(n.contents,{...STANDALONE_PRETTIER,filepath:n.path})}:n))}o(formatPluginFiles,"formatPluginFiles");function writePluginDirectory(e,t,n){const s=path.join(e,t.id);if(fs.existsSync(s))throw new Error(`${s} already exists \u2014 pick another id or remove it first`);fs.mkdirSync(e,{recursive:!0});const a=fs.mkdtempSync(path.join(e,`.${t.id}-`));try{for(const i of n){const r=path.join(a,i.path);fs.mkdirSync(path.dirname(r),{recursive:!0}),fs.writeFileSync(r,i.contents)}fs.renameSync(a,s)}catch(i){throw fs.rmSync(a,{recursive:!0,force:!0}),i}return{directory:s,files:n.map(i=>i.path)}}o(writePluginDirectory,"writePluginDirectory");async function createStandalonePlugin(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=validatePluginAnswers(n,`^${chunk6HNG5OZI_cjs.HOST_API_VERSION}`);return writeStandalonePlugin(path.resolve(t.cwd,t.parent),s)}o(createStandalonePlugin,"createStandalonePlugin"),exports.PLUGIN_ID_HINT=PLUGIN_ID_HINT,exports.PLUGIN_ID_PATTERN=PLUGIN_ID_PATTERN,exports.PLUGIN_PARTS=PLUGIN_PARTS,exports.PLUGIN_SCOPES=PLUGIN_SCOPES,exports.PLUGIN_TEMPLATES=PLUGIN_TEMPLATES,exports.PUBLISHED_SDK_PACKAGES=PUBLISHED_SDK_PACKAGES,exports.STANDALONE_PRETTIER=STANDALONE_PRETTIER,exports.STANDALONE_TSCONFIG_FILES=STANDALONE_TSCONFIG_FILES,exports.buildPluginFiles=buildPluginFiles,exports.createStandalonePlugin=createStandalonePlugin,exports.formatPluginFiles=formatPluginFiles,exports.packageNameFor=packageNameFor,exports.validatePluginAnswers=validatePluginAnswers,exports.writePlugin=writePlugin,exports.writeStandalonePlugin=writeStandalonePlugin;